대부분의 Capacitor CI/CD 실패는 몇 가지 똑같은 원인에서 비롯됩니다: iOS code 서명, 누락되거나 오래된 macOS 툴링, 네이티브 프로젝트에 반영되지 않은 웹 빌드, 재사용된 빌드 번호, 업로드 시점에만 실패하는 스토어 요구 사항. 각각은 알려진 원인과 한 번만 적용하면 되는 해결책이 있습니다. 이 가이드는 파이프라인 단계별로 함정을 묶고, 마주하게 될 오류와 발생 원인, 바꿔야 할 점을 설명합니다.
파이프라인을 처음부터 설정하는 중이라면, 먼저 Capacitor 앱용 CI/CD 설정 을 읽고, 이 목록으로 파이프라인을 강화하세요.
단계 1: 웹 레이어 빌드
함정: 네이티브 앱이 오래된 웹 빌드를 배포함
증상: CI는 성공하고 앱은 설치되지만, 어제의 UI가 표시됩니다.
원인: cap sync 에 있는 내용을 webDir 그 시점에 그대로 복사합니다. 파이프라인이 실행되면 cap sync 웹 빌드 전에, 또는 웹 빌드가 다른 폴더에 출력하는 경우 webDir 에서 capacitor.config.ts네이티브 프로젝트에 오래된 파일이 남게 됩니다.
해결 방법: 항상 이 순서로 단계를 실행하고, 출력 폴더가 비어 있으면 실패하도록 설정하세요.
bun install --frozen-lockfile
bun run build
test -f dist/index.html || { echo "web build missing"; exit 1; }
bunx cap sync
다음을 확인하세요: webDir 번들러 출력과 일치합니다 (dist Vite의 경우, www Angular과 Ionic를 사용하는 경우, build 일부 React 설정에서).
함정: 설정에 dev server URL이 남아 있는 경우
증상: 릴리스 빌드에서 화면이 하얗게 뜨거나 로드를 시도합니다 http://192.168.x.x:5173.
원인: server.url 에서 capacitor.config.ts live reload용으로 설정되어 커밋되었습니다.
해결 방법: 커밋하지 마세요 server.url. CI에서 절대 설정하지 않는 환경 변수에서 읽어오세요:
import type { CapacitorConfig } from '@capacitor/cli'
const config: CapacitorConfig = {
appId: 'com.example.app',
appName: 'Example',
webDir: 'dist',
...(process.env.LIVE_RELOAD_URL && {
server: { url: process.env.LIVE_RELOAD_URL, cleartext: true },
}),
}
export default config
함정: 잘못된 시점에 환경 변수가 고정되는 경우
증상: 프로덕션 앱이 스테이징 API과 통신하게 됩니다.
원인: Vite, webpack, Angular는 빌드 시점에 환경 변수를 인라인으로 삽입합니다. 해당 값이 존재할 때 bun run build 실행된 것이 바이너리에 포함되며, 동일한 job에서 빌드된 모든 live update에도 적용됩니다.
해결 방법: 웹 빌드 전에 job별로 환경별 변수를 설정하고, 스테이징과 프로덕션을 위한 별도의 bundle을 빌드하세요. 빌드 후 이를 교체하려 하지 마세요. cap sync.
주의사항: lockfile 불일치
증상: CI의 플러그인 버전이 로컬과 다르면 심볼 누락으로 native 컴파일이 실패합니다.
해결 방법: lockfile을 커밋하고 다음 명령어로 설치하세요: bun install --frozen-lockfile (또는 npm ci). 고정 @capacitor/core, @capacitor/ios, @capacitor/android, 그리고 @capacitor/cli 동일하게 고정하세요. 버전 불일치는 native 에러의 흔한 원인입니다. 자세한 내용은 Capacitor 버전 불일치 오류 해결하기.
2단계: native 툴체인
주의사항: 잘못된 Node, JDK 또는 Xcode 버전
증상: Unsupported class file major version, The engine "node" is incompatible, 또는 SDK 기능 관련 Xcode 에러.
원인: 호스팅 러너 이미지가 변경되고, Capacitor 8은 엄격한 최소 사양을 요구합니다.
| 도구 | Capacitor 8 요구 사항 |
|---|---|
| Node.js | 22 이상 |
| JDK | 21 |
| iOS 프로파일링 | 26 이상 |
| iOS 배포 대상 | 15.0 |
Android minSdkVersion / targetSdkVersion |
24 / 36 |
해결 방법: pipeline에서 모든 버전을 명시적으로 고정하고, 신뢰에 맡기지 마세요 latest:
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '26'
함정: 더 이상 Apple이 수락하지 않는 Xcode로 빌드하는 것
증상: 앱이 지원되지 않는 SDK로 빌드되었다는 메시지와 함께 업로드가 실패합니다.
원인: 2026년 4월 28일부터 App Store Connect는 Xcode 26과 iOS 26 SDK를 요구합니다. 셀프호스팅 Mac과 구형 러너 이미지는 여전히 Xcode 16을 기본으로 사용합니다.
해결 방법: 다음 중 하나를 선택하세요 macos-26 이미지를 사용하거나 러너에 Xcode 26을 설치하세요. 자세한 내용은 Capacitor 앱을 위한 Apple의 Xcode 26 요구사항.
함정: CocoaPods와 SPM 혼동
증상: xcodebuild: error: 'App.xcworkspace' does not exist또는 pods를 찾을 수 없는 오류가 발생합니다.
원인: 새로운 Capacitor 8 프로젝트는 Swift Package Manager를 사용하며 빌드합니다 ios/App/App.xcodeproj. 이전 프로젝트는 CocoaPods를 사용하며 빌드합니다 ios/App/App.xcworkspace 후 pod install. 이전 튜토리얼에서 복사한 파이프라인은 워크스페이스를 가정합니다.
해결 방법: 프로젝트가 어떤 것을 사용하는지 확인하고 올바른 파일을 빌드하세요. 마이그레이션 중이라면 다음을 참조하세요 Capacitor 앱을 SPM으로 마이그레이션하는 방법.
함정: Gradle 업그레이드 후 Android 플러그인 빌드가 깨집니다
증상: Namespace not specified, package attribute is deprecated, 또는 플러그인의 build.gradle Android Studio 업데이트 후.
해결 방법: Android Gradle Plugin을 android/build.gradle에서 고정하고, 브랜치에서 업그레이드하며, 플러그인을 먼저 업데이트하세요. 구체적인 오류는 AGP 9에서 Capacitor 플러그인 빌드 오류 해결하기.
3단계: Code 서명
함정: iOS 서명은 Mac에서만 동작
증상: No signing certificate "iOS Distribution" found, No profiles for 'com.example.app' were found, 또는 errSecInternalComponent.
원인: Mac 키체인에는 인증서가 들어 있고 Xcode가 프로필을 대신 다운로드합니다. CI 러너에는 둘 다 없고, 키체인은 비대화형 세션에서 잠겨 있습니다.
해결 방법: 인증서를 잠금 해제된 임시 키체인으로 가져오고, Xcode 16 이상이 프로필을 찾는 위치에 설치하세요:
security create-keychain -p "$KEYCHAIN_PASSWORD" ci.keychain
security set-keychain-settings -lut 21600 ci.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" ci.keychain
security import dist.p12 -k ci.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" ci.keychain
security list-keychains -d user -s ci.keychain login.keychain
PROFILES="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
mkdir -p "$PROFILES"
cp app.mobileprovision "$PROFILES/"
아이덴티티 이름을 사용하세요 Apple Distribution, 레거시 이름이 아닌 iOS Distribution. fastlane의 setup_ci 및 match 자동화가 이를 처리합니다. Capgo Build는 인증서와 프로필을 환경 변수로 받아 자체 머신에서 키체인 작업을 처리하므로, Linux 작업은 절대 security.
함정: 만료되거나 폐기된 인증서
증상: 1년간 잘 작동하던 pipeline이 하룻밤 사이에 실패합니다.
원인: Apple 배포 인증서와 프로비저닝 프로필은 1년 후 만료됩니다. 팀원이 Xcode에서 새 인증서를 생성하면 CI에서 사용하는 프로필도 무효화될 수 있습니다.
해결 방법: 만료일을 팀 캘린더에 등록하고, CI용 공유 배포 인증서를 하나만 유지하며, 빌드 전에 확인하세요. bunx @capgo/cli@latest build prescan --platform ios 업로드 전에 인증서 만료일, 비밀번호, 프로필 페어링을 확인합니다.
함정: Apple ID 로그인과 2단계 인증 프롬프트
증상: fastlane이 6자리 code를 기다리며 멈춥니다.
해결 방법: App Store Connect API 키를 사용하세요 (.p8, 키 ID, 발급자 ID)를 Apple ID와 비밀번호 대신 사용합니다. 2단계 인증이 필요하지 않으며 App Manager로 범위를 제한할 수 있습니다. 올바른 키를 사용했는데도 인증이 계속 실패하면 runner의 시계를 확인하세요. 토큰은 로컬 시간으로 서명되므로 타임스탬프가 어긋난 토큰은 Apple이 거부합니다.
함정: 디코딩되지 않는 base64 시크릿
증상: MAC verification failed, invalid keystore format, 또는 base64: invalid input.
원인: base64 값을 복사할 때 줄 바꿈이 추가되거나, .p12 macOS가 읽을 수 없는 OpenSSL 3 기본값으로 생성된 경우입니다.
해결 방법: 한 줄로 인코딩해서 사용하세요. -legacy 생성할 때 .p12 OpenSSL 3 사용:
base64 -i dist.p12 | tr -d '\n' > dist.p12.b64
openssl pkcs12 -export -legacy -inkey key.pem -in cert.pem -out dist.p12
주의사항: Android keystore 분실
증상: keystore를 가진 사람이 없으면 업데이트에 서명할 수 없습니다.
해결 방법: Play App Signing을 사용하면 키스토어가 업로드 키가 되며, Play Console 지원팀에 요청하여 새 키를 등록할 수 있습니다. 키스토어는 CI 시크릿과 오프라인 백업에 저장하세요. 한 대의 노트북에만 두지 마세요. Android 키스토어 생성기 처음 시작하는 경우 새로 생성합니다.
4단계: 파이프라인 설계
함정: 모든 pull request마다 네이티브 빌드
증상: 느린 PR 체크와 높은 macOS 분당 비용.
원인: 호스팅된 macOS 러너에서의 iOS 빌드는 대부분의 CI 플랜에서 가장 비싼 분당 비용이며, 대부분의 커밋은 JavaScript만 수정합니다.
해결 방법: 모든 PR에서 lint, 테스트, 웹 빌드를 실행하세요. 네이티브 빌드는 릴리스 태그 또는 병합 시에만 실행하세요 main. PR 미리보기에는 바이너리 빌드 대신 Capgo 채널에 웹 번들을 배포하세요. 방법은 Capacitor 앱을 위한 CI/CD 플랫폼 비교.
함정: iOS와 Android를 순차적으로 빌드
해결 방법: 두 플랫폼이 병렬로 빌드되도록 매트릭스를 사용하고, fail-fast: false iOS 서명 문제가 좋은 Android 빌드를 취소하지 않도록 합니다.
strategy:
fail-fast: false
matrix:
platform: [ios, android]
함정: 캐싱이 없거나 잘못된 캐시
증상: 모든 빌드에서 Gradle 의존성과 CocoaPods를 처음부터 다운로드하거나, 오래된 캐시에서 프로덕션 설정이 스테이징 빌드에 포함될 수 있습니다.
해결 방법: 캐시 ~/.gradle/caches, ~/.gradle/wrapper, 그리고 ios/App/Pods lockfile을 키로 사용합니다. 환경별로 캐시를 분리하세요. Capgo Build에서는 앱별 빌드 캐시를 다음과 같이 나눌 수 있습니다. --cache-key prod 및 --cache-key staging, 또는 ...로 건너뛰기 --no-cache 클린 빌드를 위해.
함정: 모노레포 경로
증상: could not find capacitor.config 또는 네이티브 프로젝트에 없는 플러그인.
해결 방법: 앱 패키지에서 Capacitor 명령을 실행하고, 도구가 hoisted된 경로를 가리키도록 설정하세요. node_modules. Capgo CLI은(는) 다음을 받습니다 --path 및 --node-modules 이를 위해.
5단계: 스토어 제출
함정: 빌드 번호 재사용
증상: iOS에서는 “The bundle version must be higher than the previously uploaded version”, Google Play에서는 “Version code has already been used” 오류가 발생합니다.
해결 방법: CI에서 빌드 번호를 생성합니다. fastlane을 사용하면 최신 TestFlight 빌드를 읽고 1을 더합니다. Capgo Build의 경우 이것이 기본 동작입니다. App Store Connect에서 최신 빌드 번호를 가져오거나 가장 높은 versionCode Google Play 빌드 번호를 가져와 1을 더합니다.
함정: TestFlight에서 build가 멈추는 경우
증상: 업로드는 성공하지만 테스터는 빌드를 볼 수 없습니다.
원인: 수출 규정 준수 답변 누락.
해결 방법: 한 번만 선언하세요: ios/App/App/Info.plist 표준 암호화만 사용하는 경우:
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
주의사항: privacy manifest 거절
증상: 필수 사유 API 선언 누락(ITMS-91053)에 대한 Apple의 이메일.
해결 방법: 다음을 추가하세요: PrivacyInfo.xcprivacy 앱 타겟에 추가하고 자체 manifest를 포함하는 플러그인을 업데이트하세요. 자세한 내용은 iOS 앱을 위한 privacy manifest 가이드.
주의사항: 잘못된 아티팩트 유형
해결 방법: Google Play에는 AAB가 필요합니다 (bundleRelease, APK가 아닙니다. bundleRelease 여전히 성공하며, release 서명 설정이 없으면 Play에서 거부하는 unsigned AAB가 생성되므로 먼저 설정하세요 signingConfigs.release 에서 android/app/build.gradle iOS는 개발용 또는 ad hoc IPA가 아닌 App Store export가 필요합니다. 빌드 단계에서 export 방식을 확인하세요.
6단계: Live Updates
함정: 새 네이티브 빌드가 필요한 live update 배포
증상: OTA 업데이트 후 설치된 바이너리에 없는 플러그인 메서드를 호출하면 앱이 크래시됩니다.
원인: 웹 번들이 사용자 기기의 앱에 컴파일된 플러그인 버전보다 최신 버전에 의존하는 경우입니다.
해결 방법: 파이프라인이 결정하게 하세요. build needed 네이티브 의존성이 채널에 라이브된 내용과 일치하면 0을, 새 바이너리가 필요하면 1을 반환합니다:
if bunx @capgo/cli@latest build needed com.example.app --channel production; then
bunx @capgo/cli@latest bundle upload com.example.app --channel production
else
bunx cap sync
bunx @capgo/cli@latest build request com.example.app --platform ios
bunx @capgo/cli@latest build request com.example.app --platform android
fi
또한 아래 파일이 ios/, android/, 또는 capacitor.config.* 변경될 때 네이티브 경로를 강제하세요. 전체 패턴은 live update 또는 Native Build 자동 선택이며, 호환성 규칙은 네이티브 호환성에 있습니다.
가장 많은 함정을 제거하는 해결책
If you only change one thing, move iOS compilation and signing out of your CI runners. Keychain setup, Xcode upgrades, macOS costs, and profile installation all disappear from your pipeline when a Linux job hands the prepared project to Capgo 빌드:
bun install --frozen-lockfile && bun run build
bunx cap sync ios
bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
1, 5, 6단계의 함정은 러너가 아니라 프로젝트에 관한 것이므로 여전히 적용됩니다. 실패하는 작업 디버깅에 대한 자세한 내용은 Capacitor CI/CD 파이프라인의 빌드 실패 해결.
빠른 참조
| 오류 | 함정 | 섹션 |
|---|---|---|
| 새 빌드에서 구형 UI | cap sync 웹 빌드 전 |
1단계 |
No profiles for ... were found |
프로파일이 설치되지 않거나 불일치 | 3단계 |
errSecInternalComponent |
잠긴 키체인 | 3단계 |
MAC verification failed |
잘못된 비밀번호 또는 OpenSSL 3 .p12 |
3단계 |
Unsupported class file major version |
잘못된 JDK | 2단계 |
| 업로드 시 SDK 버전이 너무 오래됨 | Xcode 26보다 오래된 버전 | 2단계 |
| Bundle 버전이 더 높아야 함 | 중복된 빌드 번호 | 5단계 |
| code 버전은 이미 사용 중입니다 | 재사용됨 versionCode |
5단계 |
| live update 후 크래시 발생 | OTA로 배포된 Native 변경 사항 | 6단계 |