개발 Mac에서 Scheme을 통해 App Clip을 실행할 수 있다고 해서 배포 산출물이 올바른 것은 아닙니다. 가장 흔히 놓치는 문제는 빌드 실패가 아니라 Clip이 호스트 아카이브에 포함되지 않거나, 상위 앱과 하위 앱의 식별자가 일치하지 않거나, 호출 URL 테스트가 정상 경로만 다루는 경우입니다. 이러한 검사를 클라우드 Mac의 지속적 통합 작업에 포함해야 모든 커밋에 동일한 Xcode, 시뮬레이터, 검증 스크립트를 적용할 수 있습니다.
검증 가능한 배포 결과 정의하기
“Build Succeeded”를 유일한 성공 기준으로 삼아서는 안 됩니다. 전체 검증 과정에서는 최소한 호스트 .xcarchive, App Clip의 독립 빌드 로그, 파싱된 entitlement 파일, 호출 시나리오 기록이 생성되어야 합니다. 테스트 URL은 Scheme의 개인 설정에 흩어 두지 말고 저장소 설정에서 관리하는 것이 좋습니다.
| 검사 계층 | 입력 | 통과 조건 |
|---|---|---|
| 컴파일 | App Clip Scheme | 시뮬레이터 대상으로 빌드 가능 |
| 포함 | 호스트 아카이브 | AppClips 아래에 Clip 존재 |
| 연결 | 두 앱의 식별자와 entitlement | 상위·하위 관계가 일치 |
| 호출 | 기본, 매개변수 포함, 잘못된 URL | 라우팅과 폴백이 예상대로 동작 |
Clip을 단독으로 실행하면 비즈니스 진입점을 검증할 수 있습니다. 호스트를 아카이브한 뒤 포함 관계를 검사해야 실제로 배포 가능한 구조인지 검증할 수 있습니다. 두 검사는 서로 대체할 수 없습니다.
빌드 디렉터리 고정하기
클라우드 작업에서는 이전 실행의 산출물이 복사 단계 오류를 가리지 않도록 독립된 DerivedData를 사용해야 합니다. 먼저 App Clip의 시뮬레이터 버전을 빌드한 다음 호스트를 아카이브합니다. Scheme 이름을 환경 변수로 전달하면 여러 브랜치에서 동일한 스크립트를 재사용할 수 있습니다.
set -euo pipefail
ROOT="$PWD"
OUT="$ROOT/.ci-artifacts"
DERIVED="$OUT/DerivedData"
ARCHIVE="$OUT/HostApp.xcarchive"
rm -rf "$OUT"
mkdir -p "$OUT"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$CLIP_SCHEME" \
-sdk iphonesimulator \
-destination "$SIMULATOR_DESTINATION" \
-derivedDataPath "$DERIVED" \
clean build | tee "$OUT/app-clip-build.log"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$HOST_SCHEME" \
-destination "generic/platform=iOS" \
-archivePath "$ARCHIVE" \
archive | tee "$OUT/host-archive.log"
SIMULATOR_DESTINATION은 파이프라인에 설치된 기기 유형과 시스템 버전으로 고정할 수 있습니다. Xcode를 업그레이드한 뒤에는 먼저 기준 작업을 갱신해야 합니다. 일반 기능 커밋이 테스트 런타임까지 함께 변경하지 않도록 하십시오.
호스트와 Clip의 번들 관계 검사하기
포함된 산출물 찾기
아카이브가 완료되면 Products/Applications에서 호스트 .app을 찾고, 해당 앱의 AppClips 디렉터리에 예상한 대상이 정확히 존재하는지 확인합니다. 앱 파일명을 고정값으로 가정하지 말고 확장자로 검색한 뒤 결과 개수를 검사해야 합니다.
HOST_APP="$(find "$ARCHIVE/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)"
CLIP_APP="$(find "$HOST_APP/AppClips" -maxdepth 1 -name '*.app' -print -quit)"
test -n "$HOST_APP"
test -n "$CLIP_APP"
HOST_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$HOST_APP/Info.plist")"
CLIP_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$CLIP_APP/Info.plist")"
printf '%s
' "$HOST_ID" > "$OUT/host-bundle-id.txt"
printf '%s
' "$CLIP_ID" > "$OUT/clip-bundle-id.txt"
AppClips 디렉터리가 없다면 먼저 호스트 Target의 포함 단계, Clip Target이 현재 아카이브 Scheme에 속하는지 여부, Release 구성이 Debug에서 유효한 설정을 덮어쓰고 있는지 확인하십시오.
최종 entitlement 비교하기
프로젝트 파일에 선언된 내용이 서명된 산출물의 최종 결과와 항상 같지는 않습니다. codesign -d --entitlements :-을 사용해 호스트와 Clip의 entitlement를 각각 내보낸 다음 plutil로 안정적인 형식으로 변환합니다. 게이트에서는 Clip의 상위 앱 연결 값이 현재 호스트를 가리키는지 확인하고, 상속되어서는 안 되는 기능도 제한해야 합니다.
codesign -d --entitlements :- "$HOST_APP" > "$OUT/host-entitlements.plist"
codesign -d --entitlements :- "$CLIP_APP" > "$OUT/clip-entitlements.plist"
plutil -lint "$OUT/host-entitlements.plist"
plutil -lint "$OUT/clip-entitlements.plist"
Xcode 버전에 따라 키 순서가 달라질 수 있으므로 전체 파일을 텍스트로만 비교해서는 안 됩니다. 안정적으로 유지되어야 하는 키를 추출해 항목별로 비교하고, 실제 값을 빌드 산출물에 기록하여 실패 후 다시 확인할 수 있게 해야 합니다.
세 가지 URL로 라우팅 검증하기
호출 테스트는 최소한 기본 진입점, 유효한 매개변수, 비정상 입력을 포함해야 합니다. 예를 들어 기본 진입점은 기본 경량 화면을 열어야 하고, 리소스 번호가 포함된 URL은 지정된 콘텐츠로 이동해야 합니다. 매개변수가 없거나 잘못된 경우에는 빈 화면에 머무르지 않고 안전한 페이지로 돌아가야 합니다.
URL을 테스트 입력으로 사용하기
App Clip Scheme에서 _XCAppClipURL을 사용하는 방식은 수동 디버깅에 적합하지만, 지속적 통합이 개발자의 로컬 Scheme 상태에 의존해서는 안 됩니다. 더 안정적인 방법은 라우팅 파서가 URL을 입력받도록 만들고 단위 테스트에서 입력을 직접 검증하는 것입니다. 애플리케이션 생명주기는 별도의 시뮬레이터 스모크 작업 하나로 확인합니다.
func testInvocationRoutes() throws {
let base = try XCTUnwrap(URL(string: invocationBaseURL))
XCTAssertEqual(router.route(for: base), .home)
let item = try XCTUnwrap(URL(string: invocationItemURL))
XCTAssertEqual(router.route(for: item), .item(id: "42"))
let malformed = try XCTUnwrap(URL(string: invocationMalformedURL))
XCTAssertEqual(router.route(for: malformed), .fallback)
}
이렇게 하면 도메인 이름 해석, 경로 매칭, 쿼리 매개변수 검증, 화면 시작을 서로 분리할 수 있습니다. 라우팅 단위 테스트는 모든 분기를 담당하고, 시뮬레이터 작업은 실제 프로세스가 시작되어 첫 화면을 표시할 수 있는지만 확인하므로 실패 원인을 더 직접적으로 찾을 수 있습니다.
게이트를 강화하고 실패 상태 보존하기
게이트가 실패하면 최소한 xcodebuild 로그, 두 Info.plist, 내보낸 entitlement 파일, 호스트와 Clip 식별자, 실패한 테스트 사례 이름을 보관해야 합니다. 로그에 액세스 토큰, 임시 자격 증명 또는 로컬 경로의 민감한 부분이 포함되어 있다면 업로드하기 전에 마스킹해야 합니다.
검사 순서는 독립 빌드 디렉터리 정리, Clip 빌드, 호스트 아카이브, 포함 상태 검사, 핵심 entitlement 비교, 라우팅 테스트 실행, 시뮬레이터 스모크 실행 순으로 고정하는 것이 좋습니다. 앞단의 구조 검사에 실패하면 즉시 중단하여 잘못된 아카이브에 시뮬레이터 시간을 계속 소비하지 않도록 하십시오.
GPUMini의 작업에서도 xcodebuild -version, 선택한 SDK, 시뮬레이터 런타임을 명시적으로 기록해야 합니다. 장기적으로 유지해야 하는 것은 특정 성공 화면의 스크린샷이 아니라, 입력이 명확하고 산출물을 추적할 수 있으며 실패 후 빠르게 재현할 수 있는 검사 항목입니다.
자주 묻는 질문
App Clip 타깃만 빌드하면 아카이브도 검증된 것인가요?
아닙니다. 단독 빌드는 컴파일 오류만 주로 찾으므로 호스트 앱을 아카이브한 뒤 포함 경로, 식별자 관계와 최종 권한도 확인해야 합니다.
호출 URL은 하나만 테스트해도 되나요?
아닙니다. 기본 경로, 매개변수 경로, 잘못된 입력을 각각 실행해 라우팅과 파싱, 안전한 대체 동작을 확인해야 합니다.
로컬 실행이 성공해도 번들 검사가 필요한 이유는 무엇인가요?
로컬 Scheme은 호스트 포함 단계를 거치지 않고 App Clip을 직접 실행할 수 있어 복사 단계나 권한 설정 오류를 놓칠 수 있습니다.
개발 및 빌드 작업에 사용할 클라우드 Mac 선택
두 가지 M4 구성, 네 가지 대여 기간 및 판매 중인 네 개 노드를 비교한 뒤 작업에 맞게 배포하세요.