알림 서비스, 공유 또는 Widget Extension이 포함된 iOS 프로젝트에서는 흔히 다음과 같은 문제가 발생합니다. 메인 앱의 빌드와 아카이브는 모두 성공하지만, 내보내기·설치 또는 배포 검증 단계에서야 Extension이 유효하지 않다는 오류가 나타납니다. 원인은 대개 소스 코드가 아니라 .app과 .appex 사이의 버전 번호, 배포 대상, 임베딩 위치 또는 서명 불일치입니다. 클라우드 Mac의 아카이브 작업 직후에 검증 단계를 추가하면 빌드 결과물이 빌드 노드를 벗어나기 전에 명확한 판정을 내릴 수 있습니다.
먼저 아카이브 인수 기준 정의하기
App Extension은 독립적으로 배포되는 결과물이 아닙니다. 메인 앱의 PlugIns 디렉터리에 있어야 하며, 메인 앱과 검증 가능한 임베딩 관계를 구성해야 합니다. CI에서는 최소한 다음 항목을 검사해야 합니다.
| 검사 항목 | 메인 앱 | App Extension | 권장 규칙 |
|---|---|---|---|
CFBundleVersion |
필수 | 필수 | 완전히 일치 |
CFBundleShortVersionString |
필수 | 필수 | 완전히 일치 |
MinimumOSVersion |
필수 | 필수 | 팀에서 통일한 경우 일치 |
NSExtension |
불필요 | 필수 | Dictionary가 존재하고 읽을 수 있어야 함 |
| 코드 서명 | 유효 | 유효 | 엄격한 검증 통과 |
| 임베딩 위치 | Applications |
PlugIns/*.appex |
다른 위치에 흩어진 복사본은 허용하지 않음 |
최소 OS 버전이 모든 프로젝트에서 반드시 같아야 하는 것은 아닙니다. 하지만 팀에서 서로 다른 배포 대상을 의도적으로 관리하지 않는다면, 일치를 요구하는 편이 구성 불일치를 더 쉽게 발견할 수 있습니다. 실제로 차이가 필요하다면 스크립트에서 조용히 무시하지 말고 허용 범위를 저장소에 명시해야 합니다.
“빌드 성공”은 각 Target이 결과물을 생성할 수 있다는 의미일 뿐입니다. “아카이브 적합” 판정을 받으려면 이러한 결과물이 올바른 관계로 구성되어 배포 가능한 앱을 이룬다는 사실까지 입증해야 합니다.
검사할 단일 xcarchive 생성하기
검증 단계에서는 DerivedData 중간 디렉터리가 아니라 아카이브를 읽어야 합니다. DerivedData에는 이전 빌드의 .appex가 남아 있을 수 있으며, 최종 임베딩 구조도 반영하지 못합니다. 먼저 고정된 아카이브 경로를 삭제한 다음 Release 아카이브를 한 번 생성합니다.
set -euo pipefail
ARCHIVE_PATH="$PWD/build/App.xcarchive"
rm -rf "$ARCHIVE_PATH"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE_PATH" \
clean archive
서로 다른 브랜치가 같은 아카이브 디렉터리를 공유하지 않도록 해야 합니다. 병렬 작업에서는 커밋 해시나 CI 작업 번호로 독립된 경로를 만들 수 있지만, 후속 스크립트에는 확정된 .xcarchive 하나만 전달해야 합니다.
먼저 임베딩 결과 확인하기
아카이브가 완료되면 메인 앱은 일반적으로 Products/Applications에 있고, Extension은 메인 앱 번들 내부의 PlugIns에 있습니다. 먼저 find로 실제 결과를 확인합니다.
find build/App.xcarchive/Products/Applications \
\( -name '*.app' -o -name '*.appex' \) \
-print
프로젝트에 Extension Target이 선언되어 있는데도 .appex를 찾을 수 없다면, 먼저 메인 앱 Target의 Embed App Extensions 빌드 단계와 현재 Scheme에서 해당 Extension이 빌드되는지 확인합니다. 아카이브를 “완성”하려고 다른 디렉터리에서 Extension을 복사해 오면 안 됩니다.
스크립트로 메타데이터와 서명 검증하기
다음 Bash 스크립트는 macOS 기본 도구와 호환됩니다. 메인 앱을 찾은 뒤 각 Extension의 Info.plist를 읽어 버전과 최소 OS 버전을 비교하고, 각각의 서명도 검증합니다.
#!/bin/bash
set -euo pipefail
ARCHIVE="${1:?usage: validate-extensions.sh App.xcarchive}"
APP=$(find "$ARCHIVE/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)
if [ -z "$APP" ]; then
printf '%s
' "main app not found"
exit 1
fi
read_plist() {
/usr/libexec/PlistBuddy -c "Print :$2" "$1/Info.plist"
}
host_build=$(read_plist "$APP" CFBundleVersion)
host_version=$(read_plist "$APP" CFBundleShortVersionString)
host_minimum=$(read_plist "$APP" MinimumOSVersion)
count=0
failed=0
while IFS= read -r extension; do
count=$((count + 1))
ext_build=$(read_plist "$extension" CFBundleVersion)
ext_version=$(read_plist "$extension" CFBundleShortVersionString)
ext_minimum=$(read_plist "$extension" MinimumOSVersion)
[ "$ext_build" = "$host_build" ] || failed=1
[ "$ext_version" = "$host_version" ] || failed=1
[ "$ext_minimum" = "$host_minimum" ] || failed=1
/usr/libexec/PlistBuddy \
-c "Print :NSExtension" \
"$extension/Info.plist" >/dev/null
codesign --verify --strict --verbose=2 "$extension"
done < <(find "$APP/PlugIns" -maxdepth 1 -name '*.appex' -print)
[ "$count" -gt 0 ] || {
printf '%s
' "no app extensions found"
exit 1
}
codesign --verify --deep --strict --verbose=2 "$APP"
[ "$failed" -eq 0 ] || {
printf '%s
' "extension metadata mismatch"
exit 1
}
스크립트를 ci/validate-extensions.sh로 저장하고 chmod +x를 실행한 다음, 아카이브 단계 직후에 호출합니다. 저장소에서 “Extension이 있는” Scheme과 “Extension이 없는” Scheme을 모두 생성한다면, 매개변수로 예상 Extension 개수를 선언해야 하며 Extension이 0개인 상황을 일괄적으로 성공 처리해서는 안 됩니다.
버전 정보를 빌드 설정으로 단일화하기
가장 흔한 불일치는 여러 Info.plist를 수동으로 관리할 때 발생합니다. 메인 앱의 Build Number를 변경해도 Extension에는 이전 값이 남을 수 있으며, 이 때문에 소스 코드 컴파일이 실패하지는 않습니다. 더 안정적인 방법은 모든 Target에서 동일한 빌드 변수를 사용하는 것입니다.
MARKETING_VERSION = 4.8.0
CURRENT_PROJECT_VERSION = 8204
IPHONEOS_DEPLOYMENT_TARGET = 17.0
plist의 해당 값에는 $(MARKETING_VERSION), $(CURRENT_PROJECT_VERSION), $(IPHONEOS_DEPLOYMENT_TARGET)을 사용합니다. 구성을 .xcconfig로 관리한다면 메인 앱과 Extension이 공통 기본 파일을 포함하도록 하고, 실제로 다른 값이 필요한 키만 소수의 Target 전용 파일에서 재정의해야 합니다.
아카이브 후 재서명으로 문제를 숨기지 않기
codesign --deep은 전체 번들을 검증하는 데 적합하지만, 범용 복구 명령으로 사용해서는 안 됩니다. 아카이브 후 재귀적으로 다시 서명하면 기존 임베딩 관계가 변경될 수 있고, CI 결과물이 프로젝트 구성과 분리될 수도 있습니다. 서명 검증에 실패하면 Target의 서명 설정, Build Phases 및 내보내기 구성을 수정한 뒤 전체 아카이브를 다시 생성해야 합니다.
실패 시 증거를 순서대로 보존하기
검증 단계가 실패하더라도 작업 디렉터리를 즉시 정리하지 마십시오. 먼저 다음 정보를 보존합니다.
.xcarchive안에 있는 모든.app과.appex의 상대 경로- 각 번들의 버전 번호, 최소 OS 버전 및 Bundle Identifier
codesign --verify --strict --verbose=2의 전체 출력- 이번 작업의 Scheme, Configuration 및 실제로 확장된 빌드 설정
- 아카이브 작업에 사용된 커밋 식별자와 Xcode 버전
다음 명령으로 빌드 설정을 내보내면 메인 앱과 Extension에 실제로 적용된 변수를 비교하기 쉽습니다.
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-showBuildSettings > build/build-settings.txt
최종 검증 단계는 두 가지를 충족해야 합니다. 실패 메시지에서 어떤 .appex의 어떤 키가 일치하지 않는지 바로 확인할 수 있어야 하고, 수정은 결과물 생성 이후가 아니라 프로젝트 구성에서 이루어져야 합니다. 그러면 클라우드 Mac 노드가 바뀌더라도 인수 기준은 저장소와 함께 유지되며, 아카이브 결과도 반복해서 검사할 수 있습니다.
자주 묻는 질문
Xcode 아카이브가 성공했는데도 App Extension이 실패하는 이유는 무엇인가요?
아카이브는 주로 타깃의 빌드 가능 여부를 확인합니다. 내보내기와 설치 단계에서는 버전, 최소 OS, 확장 선언, 임베딩 관계와 서명까지 검사합니다.
앱과 App Extension의 빌드 번호는 같아야 하나요?
CFBundleVersion은 일치해야 합니다. CFBundleShortVersionString도 같은 중앙 빌드 설정에서 생성하면 릴리스 추적이 쉬워집니다.
아카이브 후 Extension을 다시 서명해도 되나요?
권장하지 않습니다. 빌드 설정, 임베딩 단계 또는 서명 구성을 수정한 뒤 깨끗한 작업 공간에서 새 아카이브를 생성해야 합니다.
전용 물리 노드
개발 및 빌드 작업을 전용 클라우드 Mac에서 실행하세요
작업에 맞는 모델, 노드 지역 및 대여 기간을 선택하세요. 각 인스턴스는 전용 물리 머신으로 제공되며 가상 머신 환경이 아닙니다.