이 글의 핵심
- Archive ≠ Build: Archive는 Release + Any iOS Device로 고정되고
.xcarchive를 만듭니다. 로컬 Debug가 통과했다고 Archive가 된 건 아닙니다. - CI 병목은 네 구간에 몰립니다: Scheme/설정, 컴파일 캐시, 서명·키체인, exportArchive——로그에서 구간을 먼저 잡고 손대세요.
xcodebuild archive는 메뉴 Archive와 동등하지만, CI에서는-scheme,-configuration Release,-destination 'generic/platform=iOS'를 반드시 명시해야 합니다.- 「멈춘 것처럼 보임」은 대부분 키체인 승인 대기, 프로비저닝 프로파일 네트워크 fetch, 메모리 swap입니다——xcodebuild가 죽은 게 아닙니다.
- 클라우드 Mac + 셀프호스팅 Runner에 DerivedData·전용 키체인을 고정하면, 매번 콜드 Archive하는 호스팅 Runner보다 안정성이 한 단계 올라갑니다.
1. Archive와 Build, 뭐가 다른가
팀에서 CI 실패를 「xcodebuild가 터졌다」로 묶어버리는 경우가 많습니다. 실제로 Cmd+B Build와 Product → 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를 누르면, 내부적으로는 대략 다음과 같습니다.
- Shared Scheme 선택, Configuration은 Release;
- Destination을 Any iOS Device로 (시뮬레이터 불가);
xcodebuild archive실행, 산출물을ARCHIVE_PATH에 기록;- (선택) Organizer의 Distribute App →
xcodebuild -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.plist의 method가보내기 유형을 정합니다 (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: 구간별 타이밍부터, 증상별 수정
- 로컬에서 Release Archive 재현: Product → Scheme → Edit Scheme → Archive를 Release로; Destination은 Any iOS Device. 로컬이 안 되면 CI도 의미 없습니다.
- CI 네 구간 타이밍:
pod install→archive→exportArchive→upload, 각각 step summary에 기록. - 전체 로그 보존: 로그를 너무 조용하게 만드는 도구는 서명 실패 때 원본
CodeSign줄이 필요합니다. - Xcode 버전 고정:
xcode-select또는sudo xcode-select -s로 경로 잠금;.xcode-version·workflowmacos-15와 맞추기. - 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를 운영하세요.