엔지니어링 실무

클라우드 Mac에서 iOS 설치·실행 스모크 게이트 구축하기

클라우드 Mac에서 iOS 설치·실행 스모크 게이트 구축하기

iOS 파이프라인은 컴파일 단계를 모두 통과하고도 시뮬레이터에 설치할 때야 패키지 구조 문제를 드러내거나, 실행 직후 종료될 수 있습니다. 전체 UI 테스트를 가장 먼저 실행하면 피드백이 느려질 뿐 아니라 기본적인 결함이 타임아웃으로 위장되기도 합니다. 더 안정적인 방법은 클라우드 Mac에서 2~3분 안에 완료되는 설치·실행 스모크 게이트를 추가하는 것입니다. 런타임을 고정하고, 방금 생성한 .app을 설치한 뒤, 대상 프로세스가 살아 있는지 확인하고 실행 로그를 보관합니다.

게이트에서 확인해야 할 사항

이 게이트는 비즈니스 기능을 검증하지 않습니다. 시뮬레이터가 정상적으로 부팅되는지, 산출물을 설치할 수 있는지, 지정한 Bundle ID를 실행할 수 있는지, 관찰 시간 동안 프로세스가 계속 실행되는지라는 네 가지 이진 질문에만 답합니다. 어느 하나라도 실패하면 후속 UI 테스트 리소스를 더 이상 사용하지 않아야 합니다.

검사 항목 성공 조건 실패 시 보관할 정보
부팅 bootstatus가 정상적으로 종료됨 기기 UDID, 런타임 버전
설치 simctl install이 0을 반환함 App 경로, 명령 출력
실행 simctl launch가 PID를 반환함 Bundle ID, 종료 코드
생존 관찰 시간 후에도 프로세스를 조회할 수 있음 시스템 로그, 충돌 기록

스모크 게이트의 가치는 더 많은 항목을 검사하는 데 있지 않습니다. “산출물이 존재한다”와 “산출물을 실행할 수 있다”를 서로 분리된 두 가지 결론으로 만드는 데 있습니다.

작업에는 시뮬레이터 UDID를 명시적으로 전달하고 booted에 의존하지 않는 것이 좋습니다. 공유 클라우드 Mac에는 다른 작업이 부팅한 기기가 남아 있을 수 있으므로, 모호한 대상을 사용하면 로그와 설치 결과가 서로 섞일 수 있습니다. NowMini 노드에서 선택 가능한 구체적인 구성은 콘솔에서 확인해야 합니다. 파이프라인 자체는 설치된 Xcode와 해당 시뮬레이터 런타임에만 의존하면 됩니다.

빌드 산출물과 대상 기기 고정하기

먼저 빌드 출력을 작업 디렉터리로 모아 DerivedData에서 경로를 추측하지 않도록 합니다. Scheme, 구성, Bundle ID, UDID는 모두 환경 변수로 전달하고 작업 시작 시 검사해야 합니다.

set -euo pipefail

: "${SIM_UDID:?SIM_UDID is required}"
: "${APP_BUNDLE_ID:?APP_BUNDLE_ID is required}"

WORK_DIR="${RUNNER_TEMP:-$PWD/.smoke}"
APP_PATH="$WORK_DIR/build/Sample.app"
LOG_DIR="$WORK_DIR/logs"

rm -rf "$WORK_DIR"
mkdir -p "$LOG_DIR"

xcodebuild \
  -workspace Sample.xcworkspace \
  -scheme Sample \
  -configuration Debug \
  -sdk iphonesimulator \
  -destination "platform=iOS Simulator,id=$SIM_UDID" \
  CONFIGURATION_BUILD_DIR="$WORK_DIR/build" \
  build

find로 첫 번째 .app을 선택하지 마십시오. 프로젝트에서 호스트 App, 테스트 호스트, 확장 기능을 동시에 생성할 수 있으며 나열 순서도 일정하지 않습니다. 제품 이름을 알고 있다면 경로를 직접 구성하고 설치 전에 Info.plist를 검증합니다.

test -d "$APP_PATH"
BUILT_ID=$(/usr/libexec/PlistBuddy -c \
  "Print :CFBundleIdentifier" "$APP_PATH/Info.plist")
test "$BUILT_ID" = "$APP_BUNDLE_ID"

이 단계에서는 잘못된 Scheme 지정, 빌드 구성 치환 오류, 설치 명령이 테스트 호스트를 선택한 문제 등을 미리 식별할 수 있습니다.

부팅하고 설치한 뒤 프로세스 검증하기

기기를 부팅할 때는 bootstatus -b를 사용해 시스템이 실제로 준비될 때까지 기다려야 합니다. boot만 실행한 직후 설치를 시작하면 부하가 높은 상황에서 서비스가 아직 준비되지 않아 간헐적으로 실패할 수 있습니다.

xcrun simctl boot "$SIM_UDID" 2>/dev/null || true
xcrun simctl bootstatus "$SIM_UDID" -b

xcrun simctl uninstall "$SIM_UDID" "$APP_BUNDLE_ID" \
  2>/dev/null || true
xcrun simctl install "$SIM_UDID" "$APP_PATH"

LAUNCH_OUTPUT=$(xcrun simctl launch \
  "$SIM_UDID" "$APP_BUNDLE_ID" \
  -SmokeTestMode YES)

printf '%s
' "$LAUNCH_OUTPUT"
PID=$(printf '%s
' "$LAUNCH_OUTPUT" | awk -F': ' 'NF == 2 {print $2}')
test -n "$PID"

실행 인수를 사용하면 App이 최초 실행 안내를 건너뛰거나, 애니메이션을 비활성화하거나, 로컬 픽스처를 사용하도록 만들 수 있습니다. 다만 이러한 동작은 애플리케이션 코드에 명시적으로 구현되어 있어야 하며, 임의의 인수가 자동으로 적용된다고 가정해서는 안 됩니다. 실행 후에는 짧은 관찰 시간을 둔 다음 시뮬레이터 내부에서 프로세스를 조회합니다. launch의 반환 코드만 검사하면 실행 후 1초 이내에 종료되는 문제를 놓칠 수 있습니다.

sleep 8
xcrun simctl spawn "$SIM_UDID" launchctl print \
  "gui/$(id -u)/$APP_BUNDLE_ID" >/dev/null

일부 애플리케이션은 프로세스 레이블이 Bundle ID와 완전히 일치하지 않습니다. 이 경우 알고 있는 프로세스 이름을 조회하거나, 애플리케이션 시작 시 테스트에서만 읽을 수 있는 준비 완료 표시를 기록해야 합니다. 불안정한 상태를 감추기 위해 고정 대기 시간만 늘려서는 안 됩니다.

실패 원인을 파악할 수 있는 증거 보관하기

정리 작업도 중요하지만 증거를 먼저 보관해야 합니다. 최소 증거 세트에는 빌드 출력, 기기 설명, 설치 및 실행 출력, 대상 프로세스 전후의 통합 로그가 포함됩니다. 로그 범위는 호스트 전체의 과거 기록이 아니라 현재 작업을 중심으로 설정해야 합니다.

xcrun simctl list devices -j > "$LOG_DIR/devices.json"

xcrun simctl spawn "$SIM_UDID" log show \
  --style compact \
  --last 2m \
  --predicate "process == 'Sample'" \
  > "$LOG_DIR/sample-launch.log" 2>&1 || true

흔한 실수는 표준 오류만 보관하는 것입니다. 실행 직후 종료되는 경우 실제 원인은 동적 라이브러리 로드 실패, 리소스 경로 오류, 처리되지 않은 예외처럼 시뮬레이터 시스템 로그에 기록될 수 있습니다. 로그에 토큰, 요청 헤더, 사용자 데이터가 포함되어 있다면 보관 전에 민감 정보를 제거하고 파이프라인 산출물의 접근 범위를 제한해야 합니다.

환경 장애와 애플리케이션 장애 구분하기

bootstatus 실패는 일반적으로 기기 환경 문제에 해당합니다. install 실패 시에는 패키지 구조, 런타임 호환성, 디스크 공간을 우선 확인합니다. launch는 성공했지만 프로세스가 사라졌다면 애플리케이션 로그를 먼저 확인합니다. 이렇게 분류한 후 재시도 여부를 결정해야 합니다. 애플리케이션이 같은 방식으로 연속 실패한다면 여러 번 자동 재실행해서는 안 됩니다. 실제 피드백만 늦어질 뿐입니다.

상태를 정리하고 파이프라인에 연결하기

작업의 성공 여부와 관계없이 애플리케이션을 종료하고 이번 작업에서 사용한 시뮬레이터를 종료해야 합니다. shell의 trap을 사용하면 마무리 작업이 항상 실행되도록 할 수 있습니다.

cleanup() {
  xcrun simctl terminate "$SIM_UDID" "$APP_BUNDLE_ID" \
    2>/dev/null || true
  xcrun simctl uninstall "$SIM_UDID" "$APP_BUNDLE_ID" \
    2>/dev/null || true
  xcrun simctl shutdown "$SIM_UDID" \
    2>/dev/null || true
}
trap cleanup EXIT

작업마다 완전히 동일한 초기 상태가 필요하다면 종료 후 simctl erase를 실행할 수 있습니다. 다만 실행 시간이 늘어나고 사전 설정 데이터도 삭제됩니다. 더 일반적인 절충안은 App을 제거하고 작업 디렉터리를 정리하며, 각 파이프라인에 독립적인 기기를 할당하는 것입니다. 권한 상태가 관련된 경우에는 대상 서비스를 명시적으로 재설정합니다.

마지막으로 이 게이트를 컴파일 이후, 단위 테스트 또는 UI 테스트 이전에 배치합니다. 스크립트는 명확한 0이 아닌 종료 코드를 반환해야 하며, 로그 보관 단계는 항상 실행되도록 설정해야 합니다. 그러면 설치 실패가 테스트 타임아웃으로 위장되지 않고, 실행 직후 종료되는 문제로 인해 실행기 전체가 낭비되지 않습니다. 또한 실패할 때마다 개발자가 동일한 런타임과 동일한 산출물로 재현하는 데 충분한 증거를 남길 수 있습니다.

자주 묻는 질문

xcodebuild 성공만 확인하면 왜 부족한가요?

빌드 성공은 산출물이 생성됐다는 뜻일 뿐 설치와 실행을 보장하지 않습니다. 번들 구조, 최소 OS 버전, 내장 리소스 문제는 설치 또는 시작 단계에서 드러날 수 있습니다.

스모크 게이트 실패 시 어떤 자료를 남겨야 하나요?

xcodebuild 출력, 시뮬레이터 UDID와 런타임 버전, 앱 경로, simctl install 및 launch 종료 코드, 실패 시점 전후의 대상 프로세스 로그를 보관해야 합니다.

NowMini M4

전용 물리 노드에서 다음 작업 실행

M4, 16GB RAM, 256GB SSD를 탑재한 클라우드 Mac을 사용하고, 작업 주기에 맞춰 싱가포르, 도쿄, 서울 또는 홍콩 노드를 선택하세요.

클라우드 Mac 지금 대여하기