동일한 Xcode 아카이브를 배포 프로세스에 전달하더라도 최종적으로 각 기기에 설치되는 내용은 완전히 같지 않습니다. App Thinning은 기기 성능과 지원 범위에 따라 실행 파일 슬라이스와 리소스 변형을 선택합니다. 일반적인 빌드에서는 아카이브 성공 여부만 확인하기 쉬워 리소스 태그 오류, 불필요한 아키텍처 포함, 내장 프레임워크 구성 변경 또는 크기 보고서 미생성과 같은 문제를 놓칠 수 있습니다. 더 안정적인 방법은 클라우드 Mac에서 ‘아카이브’와 ‘기기 변형 검증’을 서로 독립된 두 단계로 분리하는 것입니다.
이 글에서는 기존 프로젝트와 배포 가능한 아카이브 구성을 그대로 사용합니다. 서명 체계를 변경하지 않으며 매번 다시 컴파일할 필요도 없습니다. 하나의 .xcarchive에서 App Thinning 결과를 내보내고, 구조 명세를 생성하며, 명확하게 정의된 이상이 발견되면 CI가 0이 아닌 종료 코드를 반환하도록 하는 것이 목표입니다.
검증 입력 먼저 고정하기
검증을 시작하기 전에 Xcode 경로, 앱 버전, 빌드 번호와 아카이브 위치를 기록합니다. 작업이 대화형 Shell의 기본 도구 체인에 의존해서는 안 됩니다. 클라우드 Mac에는 여러 Xcode 버전이 함께 설치되어 있을 수 있으므로 CI에서 현재 파이프라인이 승인한 버전을 명시적으로 선택해야 합니다.
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
ARCHIVE_PATH="$PWD/output/App.xcarchive"
EXPORT_DIR="$PWD/output/thinning"
REPORT_DIR="$PWD/output/evidence"
xcodebuild -version
test -d "$ARCHIVE_PATH"
rm -rf "$EXPORT_DIR" "$REPORT_DIR"
mkdir -p "$EXPORT_DIR" "$REPORT_DIR"
그런 다음 아카이브의 애플리케이션 정보를 읽어 CI가 이전 작업에서 남은 산출물을 잘못 가져오지 않았는지 확인합니다.
INFO="$ARCHIVE_PATH/Info.plist"
/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleIdentifier" "$INFO"
/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleShortVersionString" "$INFO"
/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleVersion" "$INFO"
작업 디렉터리를 작업 번호에 연결하고 작업 시작 시 빈 디렉터리를 만드는 것이 좋습니다. 여러 동시 실행 작업이 아카이브를 공유하게 되는 전역 고정 output 디렉터리는 사용하지 마십시오.
App Thinning 검증 입력은 일반적인 아카이브 검사를 이미 통과한 단일
.xcarchive여야 합니다. 입력 자체가 확실하지 않다면 이후 단계에서 기기 변형을 아무리 세밀하게 비교해도 의미가 없습니다.
호환되는 모든 기기 변형 내보내기
별도의 ExportOptions.plist를 준비합니다. method는 팀에서 현재 사용하는 Xcode 및 기존 배포 프로세스와 일치해야 합니다. 아래 예시의 핵심은 thinning을 호환되는 모든 변형을 내보내도록 설정하는 것입니다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store-connect</string>
<key>thinning</key>
<string><thin-for-all-variants></string>
<key>stripSwiftSymbols</key>
<true/>
</dict>
</plist>
내보내기를 실행할 때 결과와 로그를 모두 현재 작업 디렉터리에 저장합니다.
xcodebuild \
-exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_DIR" \
-exportOptionsPlist "$PWD/ExportOptions.plist" \
2>&1 | tee "$REPORT_DIR/export.log"
find "$EXPORT_DIR" -maxdepth 3 -type f -print \
| LC_ALL=C sort \
> "$REPORT_DIR/export-files.txt"
기존 파이프라인에서 다른 내보내기 방식을 사용한다면 예시를 그대로 따르기 위해 변경하지 마십시오. 기존 method를 유지하고 격리된 브랜치에서만 thinning 설정을 검증한 뒤, 실제 사용한 plist도 함께 보관합니다.
아키텍처와 내장 콘텐츠 확인하기
내보내기에 성공했다고 해서 결과가 올바른 것은 아닙니다. 먼저 설치 패키지를 찾아 하나씩 압축을 푼 다음 주 실행 파일의 아키텍처를 검사합니다. 스크립트에서 애플리케이션 디렉터리 이름이 고정되어 있다고 가정해서는 안 되며, Payload에서 .app을 찾아야 합니다.
mkdir -p "$REPORT_DIR/unpacked"
while IFS= read -r ipa; do
name="$(basename "$ipa" .ipa)"
dest="$REPORT_DIR/unpacked/$name"
mkdir -p "$dest"
ditto -x -k "$ipa" "$dest"
app="$(find "$dest/Payload" -maxdepth 1 -name '*.app' -type d | head -n 1)"
test -n "$app"
executable="$(/usr/libexec/PlistBuddy \
-c 'Print :CFBundleExecutable' "$app/Info.plist")"
printf '%s: ' "$name"
lipo -archs "$app/$executable"
find "$app/Frameworks" -maxdepth 1 -type d -name '*.framework' 2>/dev/null \
| sed "s#^$app/##" \
| LC_ALL=C sort \
> "$REPORT_DIR/$name-frameworks.txt"
done < <(find "$EXPORT_DIR" -type f -name '*.ipa' | LC_ALL=C sort)
현재 iOS 기기용 배포 결과에서 주 애플리케이션에 시뮬레이터 아키텍처가 포함되어 있으면 즉시 실패 처리해야 합니다. 프레임워크 목록은 저장소에 보관된 승인 기준선과 비교하는 데 적합합니다. 새 프레임워크가 추가되었다고 해서 반드시 오류인 것은 아니지만, 디버그 구성 요소나 임시 의존성이 배포 패키지에 포함되지 않도록 코드 리뷰를 거쳐야 합니다.
설치 패키지 전체의 해시는 비교하지 않기
기기별 변형에는 원래 서로 다른 리소스와 실행 파일 슬라이스가 포함될 수 있으며, 서명 데이터 역시 바이너리 차이를 만듭니다. 모든 IPA의 SHA-256이 같아야 한다고 요구하면 정상적인 슬리밍 결과까지 실패로 잘못 판정하게 됩니다.
다음 항목을 비교하는 편이 더 효과적입니다.
- 주 실행 파일에 허용된 아키텍처
Frameworks와PlugIns의 상대 경로 목록- 반드시 존재해야 하는 현지화, 글꼴 및 데이터 파일
- 최종 산출물에 포함되어서는 안 되는 디버그 구성, 테스트 리소스 및 소스 파일
- 각 변형의 압축 해제 크기와 과거 기준선 대비 변화량
리소스와 크기를 검증 증거로 만들기
먼저 내보내기 프로세스에서 크기 보고서가 실제로 생성되었는지 확인한 다음, 각 변형의 파일 목록을 생성합니다. 파일 크기는 리소스 가지치기의 영향을 받으므로 기준선은 변형별로 따로 보관해야 합니다.
SIZE_REPORT="$(find "$EXPORT_DIR" -type f \
-name 'App Thinning Size Report.txt' | head -n 1)"
test -n "$SIZE_REPORT"
cp "$SIZE_REPORT" "$REPORT_DIR/"
for app in "$REPORT_DIR"/unpacked/*/Payload/*.app; do
variant="$(basename "$(dirname "$(dirname "$app")")")"
(
cd "$app"
find . -type f -print0 \
| xargs -0 stat -f '%z %N' \
| LC_ALL=C sort
) > "$REPORT_DIR/$variant-files-and-sizes.txt"
done
크기 게이트에 절대 변하지 않는 단일 절댓값만 설정해서는 안 됩니다. 팀이 승인한 엄격한 상한과 최근 안정 기준선 대비 증가 폭이라는 두 가지 판단 기준을 함께 유지하는 것이 좋습니다. 엄격한 상한은 명백한 크기 폭증을 막고, 상대 기준선은 단일 커밋으로 발생한 비정상적인 증가를 포착합니다.
리소스 디렉터리에서는 비즈니스에 중요한 파일이 존재하는지도 확인해야 합니다. 예를 들어 오프라인 템플릿, 첫 화면에 사용되는 글꼴 또는 특정 언어 리소스가 완전한지는 단순히 ‘앱이 실행된다’는 사실만으로 입증할 수 없습니다. 이러한 경로를 저장소 내 텍스트 목록으로 관리하고 각 항목에 test -e를 실행하면, 스크립트 곳곳에 많은 조건을 흩어 놓는 것보다 검토하기 쉽습니다.
CI에 명확한 실패 조건 설정하기
최종 스크립트는 최소한 다음 상황을 차단해야 합니다. 내보낸 IPA가 하나도 없는 경우, 크기 보고서가 없는 경우, 주 애플리케이션에 허용되지 않은 아키텍처가 포함된 경우, 핵심 리소스가 누락된 경우, 프레임워크 집합이 승인 없이 변경된 경우, 그리고 변형 크기가 팀의 상한을 초과한 경우입니다.
다음 증거도 함께 보관합니다.
- Xcode 버전과
DEVELOPER_DIR - 아카이브의 번들 식별자, 버전과 빌드 번호
- 실제로 사용한
ExportOptions.plist - 전체 내보내기 로그
- 각 변형의 아키텍처, 프레임워크 및 파일 목록
- App Thinning 크기 보고서
실패하더라도 내보내기 디렉터리를 즉시 삭제하지 마십시오. 먼저 증거를 패키징해 업로드한 다음 작업 종료 훅에서 작업 공간을 정리해야 합니다. PingMini의 클라우드 Mac에서 여러 작업을 동시에 실행한다면 각 작업이 독립된 디렉터리를 사용하게 하고, 콘솔에서 현재 선택 가능한 구성을 확인해야 합니다. 검증 로직 자체는 특정 고정 노드에 의존해서는 안 됩니다.
App Thinning의 가치는 다운로드 크기를 줄이는 데만 있지 않습니다. 최종 배포 산출물 자체가 달라지기 때문입니다. 기기 변형을 정식 빌드 산출물로 취급해 검사해야 리소스 태그, 아키텍처 또는 의존성이 변경되었을 때 출시 전에 재현 가능하고 원인을 추적할 수 있는 실패 결과를 얻을 수 있습니다.
자주 묻는 질문
App Thinning 검증은 아카이브와 내보낸 패키지 중 어디에서 해야 하나요?
둘 다 검사해야 합니다. 아카이브는 공통 입력을 확인하고, 내보낸 패키지는 기기 변형별 실행 파일 조각과 실제 포함 리소스를 확인하는 데 사용합니다.
기기 변형의 해시가 서로 다르면 빌드 실패인가요?
아닙니다. 실행 파일 조각, 기기별 리소스와 서명 데이터는 달라질 수 있습니다. 파일 목록, 허용 아키텍처, 필수 리소스와 예상 밖 추가 항목을 비교해야 합니다.
CI를 즉시 실패시켜야 하는 항목은 무엇인가요?
크기 보고서 누락, 빈 내보내기 결과, 허용되지 않은 아키텍처, 필수 리소스 누락, 프레임워크 목록 변화와 팀 기준을 넘는 변형 크기는 실패 조건으로 적합합니다.
다음 빌드를 위한 전용 물리 Mac
개발, 지속적 통합 또는 로컬 모델 테스트에 적합한 세 가지 고정 구성 중 하나를 선택하고, 작업 기간에 맞춰 클라우드 Mac을 구성하세요.