클라우드 Mac CI에서 Xcode Destination 고정하기

클라우드 Mac CI에서 Xcode Destination 고정하기

동일한 xcodebuild test 명령을 클라우드 Mac에서 몇 주 동안 연속 실행하다 보면 어느 날 갑자기 Destination을 사용할 수 없다는 오류가 발생하거나, 새로 설치된 런타임의 동명 시뮬레이터가 선택될 수 있습니다. 프로젝트 자체는 바뀌지 않았지만 Xcode가 대상 기기를 해석한 결과가 달라진 것입니다. 안정적인 방법은 특정 UDID를 CI에 영구적으로 고정하는 것이 아니라 아카이브, 컴파일 검사, 시뮬레이터 테스트를 명확히 구분하고 작업을 시작할 때마다 대상을 해석해 기록하는 것입니다.

세 가지 빌드 대상부터 구분하기

Xcode의 Destination은 “이번 작업이 어느 플랫폼과 기기를 대상으로 하는가”를 지정하며 Scheme과 같은 개념이 아닙니다. 하나의 공유 Scheme을 범용 iOS 아카이브, 특정 시뮬레이터 테스트, 컴파일 전용 검증에 사용할 수는 있지만, 세 작업이 하나의 모호한 명령을 공유해서는 안 됩니다.

작업권장 대상구체적인 UDID 필요 여부
Archive 생성generic/platform=iOS아니요
시뮬레이터 테스트특정 iOS Simulator
컴파일 검증산출물 플랫폼에 따라 범용 또는 특정 대상 선택테스트 내용에 따라 다름

아카이브 작업에 name=iPhone 16을 지정하면 불필요하게 시뮬레이터 런타임 의존성이 생깁니다. 반대로 테스트 작업에 platform=iOS Simulator만 지정하면 Xcode가 여러 후보를 찾을 수 있으며, 최종 선택은 설치된 런타임과 기기 생성 순서의 영향을 받습니다.

범용 대상은 “이 프로젝트가 해당 플랫폼용 산출물을 생성할 수 있다”는 사실을 검증하고, 특정 대상은 “테스트가 명확히 지정된 기기 환경에서 실행될 수 있다”는 사실을 검증합니다. 하나의 Destination에 두 가지 의미를 동시에 부여하지 마세요.

작업 시작 전에 사용 가능한 대상 확인하기

먼저 Xcode 경로를 고정한 다음 프로젝트가 사용 가능한 대상을 직접 보고하도록 합니다. 이렇게 하면 “Scheme이 해당 플랫폼을 지원하지 않는 경우”와 “현재 호스트에 일치하는 기기가 없는 경우”를 구분할 수 있어, 처음부터 캐시를 삭제하는 일을 피할 수 있습니다.

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
WORKSPACE="App.xcworkspace"
SCHEME="App"

xcodebuild \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -showdestinations

출력은 작업 첨부 파일로 보관해야 합니다. 목록에 iOS Simulator가 전혀 없다면 simctl erase all을 반복해서 실행하지 말고 Scheme의 Supported Destinations, 대상 플랫폼, 테스트 Target부터 확인하세요. 기기는 존재하지만 unavailable로 표시된다면 해당 runtime이 현재 Xcode에서 여전히 지원되는지 확인합니다.

툴체인 정보도 함께 기록하기

Destination 문제는 기본 Xcode가 전환될 때 자주 발생합니다. 실행할 때마다 최소한 다음 정보를 기록하세요.

xcodebuild -version
xcrun simctl list runtimes
xcrun simctl list devices available

이 출력에는 프로젝트 비밀 정보가 포함되지 않으므로 빌드 진단 정보로 보관하기에 적합합니다. 여러 클라우드 Mac에서 작업을 병렬로 실행할 때도 오류가 특정 툴체인에서만 발생하는지 확인할 수 있습니다.

UDID를 고정하지 말고 시뮬레이터를 동적으로 해석하기

UDID는 현재 호스트에 생성된 기기 인스턴스에 속합니다. 시뮬레이터를 삭제한 뒤 다시 만들거나, 런타임을 교체하거나, 작업 노드를 초기화하면 UDID가 바뀔 수 있습니다. CI는 기기 이름과 사용 가능 상태를 기준으로 검색하고 결과가 하나뿐인지 확인해야 합니다.

다음 스크립트는 simctl JSON에서 사용 가능한 iPhone 16 한 대를 선택합니다. 실제 파이프라인에서는 서로 다른 두 시스템 버전에 동명 기기가 존재하는 상황을 피하기 위해 대상 runtime 식별자도 매개변수로 받아야 합니다.

SIM_NAME="${SIM_NAME:-iPhone 16}"

SIM_UDID="$(
  xcrun simctl list devices available -j |
  python3 -c '
import json, sys
name = sys.argv[1]
data = json.load(sys.stdin)
matches = []
for runtime, devices in data["devices"].items():
    for device in devices:
        if device.get("isAvailable") and device.get("name") == name:
            matches.append((runtime, device["udid"]))
if len(matches) != 1:
    for runtime, udid in matches:
        print(f"{runtime} {udid}", file=sys.stderr)
    raise SystemExit(f"expected one simulator named {name}, found {len(matches)}")
print(matches[0][1])
' "$SIM_NAME"
)"

printf 'Resolved simulator: %s (%s)\n' "$SIM_NAME" "$SIM_UDID"

핵심은 후보가 하나로 확정되지 않으면 즉시 실패하도록 하는 것입니다. 배열의 첫 번째 항목을 조용히 선택하면 작업은 계속될 수 있지만, 환경의 모호성이 재현하기 어려운 테스트 결과로 이어집니다.

테스트 명령과 아카이브 명령 분리하기

UDID를 구한 뒤에는 기기를 먼저 부팅하고 초기화가 끝날 때까지 기다린 다음 테스트를 실행합니다. bootstatus -b를 사용하면 시스템 서비스가 준비되기 전에 테스트가 먼저 실행되는 상황을 방지할 수 있습니다.

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

xcodebuild test \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -destination "platform=iOS Simulator,id=$SIM_UDID" \
  -resultBundlePath "Artifacts/TestResults.xcresult"

아카이브 명령에서는 시뮬레이터를 전혀 참조하지 않습니다.

xcodebuild archive \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -configuration Release \
  -destination "generic/platform=iOS" \
  -archivePath "Artifacts/App.xcarchive"

이렇게 분리하면 권한 경계도 더 명확해집니다. 테스트 단계는 시뮬레이터 상태를 관리하고, 아카이브 단계는 프로젝트, 서명 구성, 출력 경로에만 관여합니다. 두 단계가 동일한 SetMini 클라우드 Mac에서 실행되더라도 로그 구간은 각각 독립적으로 유지해야 합니다.

해석 결과를 검증 가능한 기록으로 남기기

명령이 성공하는 것만으로는 충분하지 않습니다. 파이프라인은 Scheme, DEVELOPER_DIR, 기기 이름, UDID, runtime, 작업 유형, 결과 번들 경로를 일반 텍스트나 JSON으로 기록해야 합니다. 문제를 조사할 때는 먼저 이러한 사실을 비교한 뒤 코드 변경 여부를 검토하세요.

종료 단계에서는 이번 작업이 부팅한 기기만 종료하는 것이 좋습니다.

cleanup() {
  if [[ -n "${SIM_UDID:-}" ]]; then
    xcrun simctl shutdown "$SIM_UDID" 2>/dev/null || true
  fi
}
trap cleanup EXIT

공유 노드에서 모든 시뮬레이터를 무조건 종료하지 말고, erase all을 일상적인 정리 수단으로 사용하지도 마세요. 전자는 병렬 작업을 중단시키고, 후자는 상태 오염의 원인을 가리는 동시에 이후 초기화 시간을 늘립니다.

최종 점검 항목은 다섯 가지로 정리할 수 있습니다. 아카이브에는 범용 대상을 사용하고, 테스트에는 이번 작업에서 동적으로 해석한 UDID를 사용하며, 후보는 반드시 하나여야 합니다. 실행 전에 Xcode와 runtime을 기록하고, 종료할 때는 이번 작업이 소유한 기기만 처리합니다. 이렇게 구성하면 Destination은 암묵적인 환경 상태가 아니라 검사 가능하고 재현 가능한 빌드 입력이 됩니다.

자주 묻는 질문

iOS 아카이브에 특정 시뮬레이터가 필요한가요?

필요하지 않습니다. 아카이브에는 generic/platform=iOS를 사용해 런타임 갱신 후 바뀔 수 있는 시뮬레이터와 작업을 분리합니다.

시뮬레이터 UDID를 CI 설정에 계속 저장해도 되나요?

권장하지 않습니다. 각 작업 시작 시 기기 모델과 런타임으로 UDID를 다시 찾고, 실제 사용한 값을 로그에 기록해야 합니다.

같은 이름의 시뮬레이터가 여러 개면 어떻게 처리하나요?

사용 가능한 기기와 원하는 런타임으로 먼저 필터링합니다. 후보가 여전히 여러 개라면 목록을 출력하고 작업을 실패시켜야 합니다.

전용 물리 노드

언제든지 접속할 수 있는 클라우드 Mac에 엔지니어링 작업을 담아보세요

두 가지 Apple Silicon 구성과 네 곳의 데이터센터 중에서 선택하세요. 리소스는 다른 테넌트와 공유되지 않으며, 실제 사용 가능 여부는 콘솔에서 실시간으로 확인할 수 있습니다.

요금제 선택