프로모

Xcode Product → Archive 전 과정 해부: CI가 여기서 멈추는 이유

CI Xcode Archive · iOS 출시
2026-06-15 읽는 데 약 14분

로컬에서 Product → Archive 한 번 누르면 Organizer에서 IPA를 뽑을 수 있습니다. CI에서 xcodebuild archive를 돌리면 같은 구간에서 20분째 멈추거나, 타임아웃되거나, 에러 없이 끝나는 경우가 흔합니다. Xcode를 못 쓰는 게 아니라, Archive를 관측 가능한 파이프라인으로 보지 않고 블랙박스 명령 한 줄로 취급했을 때 벌어지는 일입니다.

이 글은 Build → Archive → Sign → Export 각 단계가 실제로 무엇을 하는지, xcodebuild로 메뉴 동작을 어떻게 재현하는지, CI에서 자주 막히는 7가지 유형의 증상·조치표를 단계별로 정리합니다.

이 글의 핵심

  1. Archive ≠ Build: Archive는 Release + Any iOS Device로 고정되고 .xcarchive를 만듭니다. 로컬 Debug가 통과했다고 Archive가 된 건 아닙니다.
  2. CI 병목은 네 구간에 몰립니다: Scheme/설정, 컴파일 캐시, 서명·키체인, exportArchive——로그에서 구간을 먼저 잡고 손대세요.
  3. xcodebuild archive는 메뉴 Archive와 동등하지만, CI에서는 -scheme, -configuration Release, -destination 'generic/platform=iOS'를 반드시 명시해야 합니다.
  4. 「멈춘 것처럼 보임」은 대부분 키체인 승인 대기, 프로비저닝 프로파일 네트워크 fetch, 메모리 swap입니다——xcodebuild가 죽은 게 아닙니다.
  5. 클라우드 Mac + 셀프호스팅 Runner에 DerivedData·전용 키체인을 고정하면, 매번 콜드 Archive하는 호스팅 Runner보다 안정성이 한 단계 올라갑니다.
Xcode Archive와 CI 파이프라인 개요
로컬 Archive에는 GUI 피드백이 있습니다. CI에서는 같은 체인을 로그 구간으로 나눠 읽지 않으면 「Job이 40분 돌았다」만 보입니다.

1. Archive와 Build, 뭐가 다른가

팀에서 CI 실패를 「xcodebuild가 터졌다」로 묶어버리는 경우가 많습니다. 실제로 Cmd+B BuildProduct → Archive는 완전히 다른 경로입니다.

항목 Build (⌘B) Archive
Configuration Debug (로컬 개발) Release (스토어/TestFlight)
Destination 시뮬레이터 또는 연결된 기기 Any iOS Device (arm64)
산출물 DerivedData 안의 .app .xcarchive + IPA보내기 가능
서명 개발용 서명으로 일부만 통과 가능 배포 인증서 + 프로비저닝 프로파일 필수
최적화 낮음, 빌드 빠름 전체 최적화, 컴파일 시간 크게 증가

그래서 「로컬 3분에 빌드되는데 CI Archive는 20분 넘게 실패」는 이상한 게 아닙니다. 로컬은 Debug + 시뮬레이터, CI는 Release + 기기 아키텍처 + 전체 서명 체인을 돌리는 경우가 대부분입니다. 비교 대상을 맞춘 뒤에 최적화를 논하세요. 컴파일이 느린 것 자체는 CI에서 xcodebuild가 로컬보다 2–3배 느린 이유를 참고하세요.

2. 메뉴에서 CLI로: Product → Archive의 동등 명령

Xcode GUI에서 Product → Archive를 누르면, 내부적으로는 대략 다음과 같습니다.

  1. Shared Scheme 선택, Configuration은 Release;
  2. Destination을 Any iOS Device로 (시뮬레이터 불가);
  3. xcodebuild archive 실행, 산출물을 ARCHIVE_PATH에 기록;
  4. (선택) Organizer의 Distribute Appxcodebuild -exportArchive에 해당.

CI에서 쓰는 최소 Archive 명령 예시입니다 (경로는 프로젝트에 맞게 바꾸세요).

xcodebuild archive \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
  CODE_SIGN_STYLE=Manual \
  DEVELOPMENT_TEAM=XXXXXXXXXX \
  | tee archive.log

자주 빠지는 파라미터 세 가지:

  • -destination 'generic/platform=iOS': 없으면 xcodebuild가 시뮬레이터를 고를 수 있고, Archive가 실패하거나 이상하게 동작합니다.
  • -archivePath: 쓰기 가능한 경로여야 합니다. 호스팅 Runner 임시 디렉터리 권한 문제로 「빌드는 됐는데 저장 실패」가 나옵니다.
  • Scheme은 Shared이고 Git에 커밋: CI checkout 후 로컬 전용 Scheme은 없습니다. scheme not found의 흔한 원인입니다.

공식 문서: Apple — Building your app, Distributing your app.

3. 4단계 파이프라인 (한눈에)

┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│ 1. Resolve   │ →  │ 2. Archive   │ →  │ 3. Sign      │ →  │ 4. Export    │
│ SPM/Pods     │    │ Release 전량   │    │ 인증서/프로파일 │    │ IPA / Upload │
│ DerivedData  │    │ → .xcarchive │    │ 키체인 접근     │    │ TestFlight   │
└──────────────┘    └──────────────┘    └──────────────┘    └──────────────┘
     pod install          xcodebuild           codesign           exportArchive
     5–15분 흔함          archive 8–25분       무인 환경 병목        plist 오류 흔함

CI가 「여기서 멈춘다」고 할 때 먼저 물어보세요: 로그 마지막 줄이 어느 단계인가? CompileSwift에 멈춰 있으면 컴파일, CodeSign이면 서명, exportArchive면보내기 설정입니다. 구간을 섞어서 말하면 캐시만 반복 지우게 됩니다.

4. 단계 1: Resolve & Compile (CI가 더 느린 이유)

Archive 이전 의존성 해석·컴파일은 로컬은 「따뜻한」 상태, CI는 「차가운」 상태인 경우가 많습니다.

  • CocoaPods / SPM: CI에서 매번 pod install하면 로그가 Installing …로 가득 차고, Archive 전에 10분이 날아갑니다. Flutter iOS CI 캐시 전파 체인을 참고하세요.
  • DerivedData 콜드 스타트: Release 전량 빌드 + 캐시 없음이면 Pod 네이티브 코드까지 전부 다시 컴파일합니다. 셀프호스팅 Runner에서 DERIVED_DATA_PATH를 SSD에 고정하면 두 번째 Archive는 절반 가까이 줄어드는 경우가 많습니다.
  • 메모리 압박: Archive 피크 메모리는 Debug보다 큽니다. 16GB 머신에서 Job을 겹치면 swap이 발생하고, CPU는 낮은데 진행이 안 되는 것처럼 보입니다. Runner 메모리·swap 관리를 확인하세요.

판별법: 로그에 CompileC / SwiftCompile이 많고, 건드리지 않은 서드파티 라이브러리를 대상으로 하면 서명보다 캐시를 먼저 고치세요.

5. 단계 2: Archive (.xcarchive 생성)

xcodebuild archive 성공 후 -archivePath에 다음 구조가 있어야 합니다.

MyApp.xcarchive/
  Info.plist
  Products/Applications/MyApp.app
  dSYMs/...

흔한 실패 패턴:

  • Scheme의 Archive 액션에 타깃 미선택: 로컬 다른 Scheme으로는 빌드되지만, CI가 지정한 Scheme의 Archive가 비어 있습니다.
  • 멀티 Target / Extension 누락: 메인 App은 통과했는데 Notification Service Extension 서명이 실패해 전체 Archive가 깨집니다.
  • Build number / 버전 충돌: CI가 CFBundleVersion을 올리지 않아 업로드 단계에서 터지고, Archive가 느리다고 오해하는 경우.

검수: Archive 직후 ls -la "$ARCHIVE_PATH"plutil -p "$ARCHIVE_PATH/Info.plist"ApplicationProperties가 있는지 확인하세요. export 실패 후에야 Archive를 의심하지 말고, 끝나자마자 검증하세요.

6. 단계 3: Code Sign (CI 최대 병목)

로컬 Archive 때 Xcode가 키체인 승인 팝업을 띄우고, 「항상 허용」을 누르면 파이프라인이 통과합니다. 무인 CI에는 이 단계가 없습니다. 그래서:

  • errSecInternalComponent, User interaction is not allowed: 개인키가 로그인 키체인에 있고 CI 사용자에게 GUI 승인이 없습니다.
  • 프로비저닝 프로파일 만료 / Bundle ID 불일치: 로그가 CodeSign에서 멈추고 Provisioning profile … doesn't match가 나옵니다.
  • -allowProvisioningUpdates 오남용: Apple ID 대화형 로그인이 필요해 Headless 환경에서 무한 대기합니다.
  • 키체인 미해제: Job 시작에 security unlock-keychain / set-key-partition-list가 없습니다.

클라우드 Mac·셀프호스팅 Runner 권장: CI 전용 키체인 파일(로그인 키체인 아님)을 만들고 배포 인증서 + 개인키를 import한 뒤, Job 시작 시 해제·기본 키체인으로 설정합니다. 전체 체인은 클라우드 Mac iOS CI: codesign과 키체인 경계를 보세요.

# Job 시작 예시 (비밀번호는 CI Secret)
KEYCHAIN=$RUNNER_TEMP/ci-signing.keychain-db
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security import dist.p12 -k "$KEYCHAIN" -P "$P12_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security list-keychains -s "$KEYCHAIN" login.keychain-db

fastlane match나 Xcode Cloud로 자격 증명을 관리할 때도 원칙은 같습니다: CI 프로세스가 개인키에 무인으로 접근해야 하지, 「인증서가 어딘가에 있다」는 식으로는 안 됩니다. match 문서: fastlane match.

7. 단계 4: Export IPA (exportArchive)

Archive만으로는 .xcarchive만 생깁니다. TestFlight 업로드에는 Export가 필요합니다. 메뉴의 Distribute App은 다음에 해당합니다.

xcodebuild -exportArchive \
  -archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
  -exportPath "$RUNNER_TEMP/export" \
  -exportOptionsPlist ExportOptions.plist

ExportOptions.plistmethod가보내기 유형을 정합니다 (app-store, ad-hoc, development 등). CI에서 자주 나는 문제:

  • method와 프로파일 타입 불일치: development 프로파일인데 app-store를 넣은 경우.
  • teamID / signingCertificate 누락: Manual 서명에서 export 단계에서야 터지고 Archive가 느리다고 착각합니다.
  • 업로드와 export를 한 step에: altool / Transporter 네트워크 지연이 「Archive 타임아웃」으로 잡힙니다.

archive, export, upload를 세 step으로 나누고 각각 시간을 기록하세요. 업로드는 xcrun altool --upload-app 또는 macOS 배포용 xcrun notarytool을 Archive와 분리하는 편이 낫습니다.

8. CI가 멈추는 7가지 유형 대응표

증상 / 로그 키워드 단계 우선 조치
오랫동안 새 로그 없음, CPU 낮음 Sign 키체인 해제 여부 확인; GUI가 필요한 provisioning 자동 업데이트 비활성화
scheme 'Foo' not found 사전 Scheme Shared + 커밋; CI에서 -list로 확인
No profiles for … were found Sign Bundle ID·프로파일 저장소/match pull 성공 여부 확인
미수정 Pod에 대한 CompileC 다발 Compile DerivedData 영구화; Job 안에서 clean 지양
pod install > 8분 Resolve ios/Pods 캐시; pod install --deployment
플랫폼이 Job kill (에러 스택 없음) 전역 timeout 상향; swap·디스크 full 확인 (디스크 관리)
exportArchive failed + plist 관련 Export export만 분리 실행; ExportOptions.plist의 method 확인

온콜 때는 로그를 뒤에서부터error:를 찾고 위 표와 대조하세요——flutter clean부터 돌리는 것보다 한 시간은 줄어듭니다.

9. Runbook: 구간별 타이밍부터, 증상별 수정

  1. 로컬에서 Release Archive 재현: Product → Scheme → Edit Scheme → Archive를 Release로; Destination은 Any iOS Device. 로컬이 안 되면 CI도 의미 없습니다.
  2. CI 네 구간 타이밍: pod installarchiveexportArchiveupload, 각각 step summary에 기록.
  3. 전체 로그 보존: 로그를 너무 조용하게 만드는 도구는 서명 실패 때 원본 CodeSign 줄이 필요합니다.
  4. Xcode 버전 고정: xcode-select 또는 sudo xcode-select -s로 경로 잠금; .xcode-version·workflow macos-15와 맞추기.
  5. 48시간 검수: 같은 commit으로 Archive 두 번——두 번째가 눈에 띄게 빨라야 캐시가 맞습니다. 여전히 느리면 메모리·디스크를 의심.

출시 직전 임시 빌드 머신이 필요하면 원격 Mac 출시 스프린트 단기 임대로 호스팅 Runner Archive 타임아웃과 싸우지 않는 편이 낫습니다.

10. 복사해서 쓰는 CI 명령 템플릿 (GitHub Actions 예시)

jobs:
  archive-ios:
    runs-on: [self-hosted, macOS, ios]  # 또는 macos-14 호스팅
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4

      - name: Install pods
        run: pod install --deployment
        working-directory: ios

      - name: Archive
        run: |
          set -o pipefail
          xcodebuild archive \
            -workspace ios/MyApp.xcworkspace \
            -scheme MyApp \
            -configuration Release \
            -destination 'generic/platform=iOS' \
            -archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
            | tee archive.log
        env:
          DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer

      - name: Export IPA
        run: |
          xcodebuild -exportArchive \
            -archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
            -exportPath "$RUNNER_TEMP/export" \
            -exportOptionsPlist ios/ExportOptions.plist

셀프호스팅 Runner 라벨 라우팅, launchd 상시 실행, CI 사용자 격리는 Mac mini 셀프호스팅 Runner 구축Flutter + GitHub Actions 아키텍처 총괄을 참고하세요.

11. 결론과 다음 단계

Product → Archive는 「조금 더 빌드」가 아니라 Release 전량 빌드 + 아카이브 + 배포급 서명 + (선택)보내기의 전체 체인입니다. CI가 여기서 멈출 때 Xcode가 고장난 경우는 드물고, 대부분 Debug 경험을 Release에 그대로 적용하거나, 서명을 로컬 GUI 전용으로 두거나, export와 archive를 한 블랙박스 step에 넣은 결과입니다.

실행 가능한 다음 단계: 로컬 Archive 통과 → CI 네 구간 타이밍 → 전용 키체인 + 영구 캐시 → 그다음 머신 추가·스펙 업. Apple 플랫폼 팀에게 클라우드 Mac 셀프호스팅 Runner는 호스팅 Runner 캐시를 반복 튜닝하는 것보다 총 시간이 줄어드는 경우가 많습니다. Archive가 필요한 건 일회성 최고 속 칩이 아니라 안정적인 핫 경로입니다.

클라우드 Mac으로 Archive 전체 체인을 통과시키기

전용 M4 베어메탈에 xcodebuild archive, 전용 서명 키체인, 영구 DerivedData에 적합합니다. 일일 임대로 Archive 소요·안정성을 먼저 검증하고, 만족하면 월 임대로 상주 Runner를 운영하세요.

Mac 임대 플랜 구성 · M4 사양 확인 · 개통·검수 체크리스트