Swift Package가 로컬 미리보기에서는 정상적으로 작동하지만 클라우드 Mac의 아카이브 작업에 연결하면 아이콘이 비어 있거나 기본 문구가 표시되고 JSON을 읽지 못하는 경우가 있다. 가장 까다로운 점은 대개 컴파일 오류가 아니라 파이프라인이 여전히 성공으로 표시된다는 것이다. SwiftPM은 Bundle.module을 처리하지만, 리소스 선언이 완전한지, 호출할 때 사용하는 이름이 일치하는지, 리소스가 최종 앱 번들에 포함되었는지까지 팀을 대신해 판단해 주지는 않는다. 따라서 테스트 담당자가 화면을 열어 본 뒤 누락을 발견할 때까지 기다리지 말고, 리소스를 빌드 산출물의 일부로 검증해야 한다.
리소스가 누락되는 세 위치부터 파악하기
문제를 조사하기 전에 어느 계층에서 문제가 발생했는지 구분해야 한다. 첫 번째는 소스 디렉터리에 파일이 있지만 Package.swift의 target에 해당 리소스가 선언되지 않은 경우다. 두 번째는 리소스가 .bundle로 생성되었지만 호출 코드가 기본 앱의 bundle을 사용하는 경우다. 세 번째는 파일이 앱 번들에 포함되었지만 파일명 대소문자, 하위 디렉터리 또는 현지화 디렉터리가 런타임 쿼리와 일치하지 않는 경우다.
이미지 한 장, JSON 파일 하나, 현지화 문자열 하나를 읽는 최소 재현 진입점을 먼저 마련하는 것이 좋다. 세 유형이 모두 실패하면 bundle 선택을 우선 확인하고, 특정 파일만 실패하면 리소스 규칙과 경로를 먼저 점검한다.
“컴파일 성공”은 컴파일러가 현재 입력을 받아들였다는 사실만 증명할 뿐, 런타임에 필요한 파일이 배포 산출물에 포함되었다는 사실까지 증명하지는 않는다.
Package.swift에서는 .process 또는 .copy를 명확히 선택한다. 이미지, 리소스 디렉터리, 현지화 콘텐츠에는 일반적으로 .process를 사용하고, 원래 디렉터리 구조나 원본 바이트를 유지해야 하는 파일에만 .copy를 사용한다. 저장소 디렉터리 전체를 리소스 번들로 한꺼번에 복사해서는 안 된다. 그렇게 하면 테스트 샘플, 임시 출력은 물론 배포하면 안 되는 설정까지 함께 포함될 수 있다.
매니페스트로 리소스 계약 고정하기
리소스를 검증할 때 파일 수만 비교해서는 안 된다. 리소스를 새로 추가하면서 기존 리소스를 삭제하거나 이름을 바꾸면 파일 수가 전혀 달라지지 않을 수 있다. 더 안정적인 방법은 저장소에 ci/expected-resources.txt를 유지하고, 각 줄에 리소스 bundle 루트를 기준으로 한 경로를 기록하는 것이다.
Assets.car
Defaults/config.json
en.lproj/Localizable.strings
zh-Hans.lproj/Localizable.strings
매니페스트에는 런타임에 반드시 있어야 하는 파일만 포함해야 한다. 필요할 때 다운로드하는 콘텐츠, 테스트 fixture, 개발용 미리보기 리소스를 섞어서는 안 된다. 리소스 이름을 변경할 때는 매니페스트 변경도 코드 리뷰에 함께 포함해야 한다. 그래야 리뷰어가 배포 계약이 어떻게 바뀌었는지 확인할 수 있다.
리소스 디렉터리 자체도 역방향으로 검사해야 한다. 소스에는 있지만 선언되지 않은 파일에는 경고를 표시하고, 매니페스트에서 요구하지만 빌드 결과에 없는 파일은 즉시 실패 처리해야 한다. 전자는 저장소 정리에 도움이 되고, 후자는 배포 결과를 보호한다.
빌드 후 앱 번들을 직접 검사하기
각 작업은 독립된 DerivedData를 사용해야 한다. 그래야 다른 병렬 작업이 남긴 오래된 bundle 때문에 검사가 잘못 통과하는 일을 막을 수 있다. 다음 스크립트는 시뮬레이터 빌드를 예로 든다. 실제 파이프라인에서는 scheme을 교체하고 bundle 이름 일치 규칙을 프로젝트의 실제 모듈 이름에 맞추면 된다.
set -euo pipefail
DERIVED="${RUNNER_TEMP:-/tmp}/resource-check-${BUILD_ID:-local}"
rm -rf "$DERIVED"
xcodebuild \
-scheme ExampleApp \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$DERIVED" \
build
APP=$(find "$DERIVED/Build/Products" -type d -name 'ExampleApp.app' -print -quit)
test -n "$APP"
COUNT=$(find "$APP" -type d -name '*FeatureKit*.bundle' | wc -l | tr -d ' ')
test "$COUNT" = "1"
BUNDLE=$(find "$APP" -type d -name '*FeatureKit*.bundle' -print -quit)
while IFS= read -r item; do
test -z "$item" && continue
test -e "$BUNDLE/$item" || {
printf 'missing resource: %s\n' "$item" >&2
exit 1
}
done < ci/expected-resources.txt
여기서는 의도적으로 bundle 수가 하나인지 먼저 단언한다. 첫 번째 일치 결과만 바로 선택하면 오래된 산출물, 테스트 bundle 또는 이름이 같은 모듈이 문제를 가릴 수 있다. 검사 대상도 DerivedData의 중간 디렉터리가 아니라 .app 내부의 bundle이어야 한다. 최종 임베드 결과를 나타내는 것은 전자뿐이기 때문이다.
자주 발생하는 실수를 게이트로 전환하기
리소스 유형마다 필요한 검증 방법이 다르므로 모든 검사를 test -e만으로 끝내서는 안 된다.
| 리소스 유형 | 최소 검사 | 일반적인 실패 |
|---|---|---|
| JSON | 파일이 존재하고 파싱 가능함 | 빈 파일이 복사되었거나 형식이 잘못됨 |
| 현지화 문자열 | 대상 언어 디렉터리가 존재하고 키를 읽을 수 있음 | 언어 디렉터리 이름이 잘못되었거나 기본 문구로 대체됨 |
| 이미지와 색상 | 컴파일된 리소스 산출물이 존재하고 스모크 테스트 화면에서 로드 가능함 | 이름의 대소문자가 일치하지 않거나 기본 bundle을 잘못 사용함 |
| 템플릿 파일 | 해시 또는 핵심 필드를 검증함 | 생성 단계가 저장소 버전을 덮어씀 |
macOS에서 흔히 사용하는 작업 볼륨은 대소문자를 구분하지 않으므로 IconDark와 icondark가 불일치해도 개발 머신에서는 문제가 드러나지 않을 수 있다. 파일 시스템이 우연히 오류를 허용하는 데 의존하지 말고, 매니페스트에서 경로를 추출해 실제 경로와 대소문자를 구분하여 문자 단위로 비교해야 한다. JSON에는 파서를 추가로 실행하고, property list에는 plutil -lint를 사용하며, 문자열 디렉터리에서는 최소한 필수 언어와 핵심 키를 검사한다.
Package에서 생성 플러그인을 사용한다면 리소스 검증 전에 생성 단계가 실행되는지 먼저 확인하고, 생성 디렉터리를 해당 작업 전용 임시 경로로 제한해야 한다. 여러 작업이 같은 생성 디렉터리를 공유하면 한 작업이 다른 브랜치의 파일을 읽어 재현하기 가장 어려운 거짓 성공을 만들 수 있다.
증거를 보존하고 작업을 재실행 가능하게 유지하기
실패했을 때 DerivedData 전체를 업로드해서는 안 된다. 용량이 크고 노이즈가 많으며, 장기간 보관하기에 적절하지 않은 경로 정보가 포함될 수도 있다. 더 실용적인 증거 패키지는 리소스 매니페스트, 실제 bundle 파일 트리, 빌드 로그 마지막 부분, 파서 오류, 현재 커밋 식별자로 구성된다. 파일 트리는 다음과 같이 생성할 수 있다.
find "$BUNDLE" -print \
| sed "s#^$BUNDLE/##" \
| LC_ALL=C sort \
> resource-bundle-tree.txt
로그와 파일 트리를 보관하기 전에는 액세스 토큰, 사용자 디렉터리, 임시 자격 증명을 제거해야 한다. 작업이 끝나면 독립된 DerivedData와 임시 생성 디렉터리를 삭제하여 다음 실행이 반드시 빈 디렉터리에서 시작되도록 한다. DPLYMAC의 병렬 실행기에도 같은 원칙을 적용해야 한다. 작업 간에 다운로드 캐시는 재사용할 수 있지만, 식별되지 않은 빌드 출력은 재사용해서는 안 된다.
최종 게이트는 세 가지 질문에 답할 수 있어야 한다. 선언에서 요구하는 것은 무엇인지, 최종 앱 번들에 실제로 무엇이 있는지, 런타임에서 실제 이름으로 읽을 수 있는지다. 이 세 계층에 각각 검사 가능한 결과를 남기면 SwiftPM 리소스 누락은 간헐적인 UI 장애가 아니라 커밋 단계에서 바로 찾아낼 수 있는 일반적인 빌드 오류가 된다.
자주 묻는 질문
SwiftPM 패키지가 빌드에 성공해도 실행 중 리소스를 찾지 못하는 이유는 무엇인가요?
빌드 성공은 런타임 파일명, 대소문자, 최종 배치 위치까지 보장하지 않습니다. 생성된 .app과 내부 .bundle을 직접 검사해야 합니다.
소스 리소스와 최종 빌드 결과 중 무엇을 검사해야 하나요?
둘 다 검사해야 합니다. 소스 검사는 선언에서 빠진 파일을 찾고, 최종 결과 검사는 해당 파일이 실제 배포물에 처리되어 포함됐는지 확인합니다.
검증된 워크플로를 독점 물리 노드에서 실행하세요
작업에 맞춰 M4 또는 M4 Pro, 대상 노드와 결제 주기를 선택한 다음 주문 전에 구성과 추가 옵션을 확인하세요.