개발용 Mac에서는 정상적으로 열리던 Storyboard가 병합 후 원격 아카이브 작업 중간에 실패를 일으킬 수 있습니다. 이때 의존성을 다시 내려받거나 시뮬레이터를 실행하고 DerivedData를 정리해도 소용이 없습니다. 실제 원인은 끊어진 outlet, 잘못 지정된 사용자 정의 클래스 모듈, 또는 현재 배포 대상에서 지원하지 않는 속성을 사용하는 XIB일 수 있기 때문입니다. 클라우드 Mac 기반 지속적 통합 환경에서는 먼저 ibtool로 UI 리소스를 독립적으로 컴파일한 뒤 전체 빌드를 진행할지 결정하는 편이 더 효율적입니다.
UI 리소스 검사를 빌드 앞단으로 이동하기
ibtool은 Xcode에 포함되어 있으며, 스크립트에 도구 경로를 하드코딩하지 말고 xcrun을 통해 호출해야 합니다. Storyboard와 XIB를 읽어 errors, warnings, notices를 출력하고 원본 파일을 storyboardc 또는 nib로 컴파일할 수 있습니다. 이 단계에서는 시뮬레이터를 실행할 필요가 없으므로 의존성 해석 이후, xcodebuild archive 이전에 배치하기에 적합합니다.
먼저 실행 노드에서 실제로 사용하는 개발자 디렉터리를 확인합니다.
set -euo pipefail
xcode-select -p
xcrun --find ibtool
xcodebuild -version
xcrun ibtool --version
이 정보는 작업 로그에 기록해야 합니다. 같은 클라우드 Mac에 여러 버전의 Xcode를 유지하는 경우 파이프라인에서 DEVELOPER_DIR을 명시적으로 설정하고, 작업이 끝난 뒤 해당 환경 변수를 제거해야 합니다. 그렇지 않으면 후속 작업이 잘못된 툴체인을 상속할 수 있습니다.
인터페이스 파일이 그래픽 편집기에서 열린다고 해서 지정된 Xcode, 배포 대상, 모듈 컨텍스트에서 정상적으로 컴파일된다는 뜻은 아닙니다. CI 검사의 목적은 특정 개발자의 데스크톱 상태가 아니라 릴리스 툴체인을 재현하는 것입니다.
Storyboard와 XIB를 검색해 독립적으로 컴파일하기
다음 스크립트는 저장소의 인터페이스 파일을 검색하고 각 입력 파일마다 별도의 출력 디렉터리를 만듭니다. 빌드 및 의존성 디렉터리를 제외해야 생성된 결과물이나 서드파티 리소스를 중복 검사하지 않을 수 있습니다.
#!/usr/bin/env bash
set -euo pipefail
root="${1:-$PWD}"
target="${IPHONEOS_DEPLOYMENT_TARGET:-16.0}"
work="${TMPDIR:-/tmp}/ibtool-preflight"
log="$work/ibtool.log"
rm -rf "$work"
mkdir -p "$work/out"
: > "$log"
find "$root" \
\( -path "*/DerivedData/*" -o -path "*/build/*" -o -path "*/Pods/*" \) -prune \
-o \( -name "*.storyboard" -o -name "*.xib" \) -print0 |
while IFS= read -r -d '' source; do
relative="${source#"$root"/}"
safe_name="$(printf '%s' "$relative" | tr '/ ' '__')"
case "$source" in
*.storyboard)
output="$work/out/${safe_name%.storyboard}.storyboardc"
;;
*.xib)
output="$work/out/${safe_name%.xib}.nib"
;;
esac
printf 'Checking %s
' "$relative" | tee -a "$log"
xcrun ibtool \
--errors \
--warnings \
--notices \
--minimum-deployment-target "$target" \
--compile "$output" "$source" 2>&1 | tee -a "$log"
done
이 스크립트는 임시 디렉터리를 사용하므로 저장소를 오염시키지 않습니다. 실패할 때는 ibtool.log를 보관하고, 성공하면 컴파일 결과를 삭제한 뒤 로그 요약만 업로드해도 됩니다. 임시 출력물을 버전 관리 시스템에 커밋해서는 안 되며, 여러 병렬 작업이 고정 디렉터리를 공유하도록 해서도 안 됩니다.
독립 사전 점검과 프로젝트 빌드의 경계 구분하기
ibtool을 독립적으로 호출하면 XML 손상, 일부 연결 이상, 호환되지 않는 속성, 컴파일 오류를 발견할 수 있지만 전체 target의 모든 빌드 설정까지 파악하지는 못합니다. 사용자 정의 뷰 컨트롤러가 어느 모듈에 속하는지, 특정 리소스가 Copy Bundle Resources에 포함되어 있는지, 조건부 컴파일 후에도 클래스가 존재하는지는 여전히 xcodebuild로 검증해야 합니다.
검사는 다음 두 단계로 나누는 것이 좋습니다.
| 단계 | 입력 | 주요 발견 항목 | 실패 처리 |
|---|---|---|---|
| 빠른 사전 점검 | Storyboard, XIB | 파일 손상, 속성 비호환, 기본 연결 오류 | 즉시 중단 |
| 프로젝트 빌드 | workspace, project, Scheme | 모듈 해석, 리소스 소속, 링크 및 서명 컨텍스트 | 전체 빌드 로그 보관 |
독립 사전 점검은 통과했지만 프로젝트 빌드가 실패했다면 Target Membership, Module 필드, 클래스명 리팩터링 기록, 배포 대상의 상속 관계를 우선 확인해야 합니다. 반대로 사전 점검에서 이미 실패했다면 시간이 더 오래 걸리는 아카이브를 계속 실행할 필요가 없습니다.
현지화 파일을 동일한 품질 게이트에 포함하기
Storyboard 현지화에는 일반적으로 두 가지 방식이 있습니다. 언어별로 별도 리소스를 관리하거나 Base Internationalization과 .strings를 함께 사용하는 방식입니다. 전자는 객체 ID가 어긋나기 쉽고, 후자는 이미 삭제된 키가 남기 쉽습니다. 먼저 plutil로 .strings의 기본 형식을 검사합니다.
find . -name "*.strings" -print0 |
while IFS= read -r -d '' file; do
plutil -lint "$file"
done
Base Storyboard를 사용하는 경우 현재 키 집합을 생성한 뒤 저장소의 현지화 파일과 비교할 수 있습니다.
mkdir -p "${TMPDIR:-/tmp}/ibtool-strings"
xcrun ibtool \
--generate-strings-file "${TMPDIR:-/tmp}/ibtool-strings/Main.strings" \
"App/Base.lproj/Main.storyboard"
plutil -lint "${TMPDIR:-/tmp}/ibtool-strings/Main.strings"
생성된 파일은 비교에만 사용해야 하며 기존 번역문을 직접 덮어써서는 안 됩니다. 더 안전한 방법은 키 이름을 추출해 정렬한 후 차이를 검사하는 것입니다. 새 키는 번역 대기 목록에 추가하고, 삭제된 키는 정리 알림으로 남기며, 기존 키의 텍스트는 계속 현지화 프로세스에서 관리합니다. 이렇게 하면 누락된 항목을 찾으면서도 기존 번역이 Base 텍스트로 잘못 대체되는 일을 방지할 수 있습니다.
경고 기준선과 흔한 오탐 처리하기
모든 출력을 무조건 실패로 처리하면 단기적으로는 엄격해 보이지만, 실제로는 팀이 실패 신호를 무시하는 데 익숙해질 수 있습니다. 더 실용적인 규칙은 errors가 발생하면 즉시 실패시키고, warnings는 저장소에 보관된 검토 완료 기준선과 비교해 새 항목만 차단하며, notices는 빌드 결과물에 저장해 정기적으로 정리하는 것입니다.
일반적인 점검 순서는 다음과 같습니다.
DEVELOPER_DIR가 전체 빌드와 동일한지 확인합니다.IPHONEOS_DEPLOYMENT_TARGET이 프로젝트의 실제 설정에서 가져온 값인지 확인합니다.- 파일 경로의 대소문자를 검사합니다. 특히 이름 변경 시 문자 대소문자만 바뀐 리소스를 주의해야 합니다.
- 끊어진 outlet과 action, 사용자 정의 클래스명을 검색합니다.
- 병렬 컴파일이 출력물을 덮어쓰지 않도록 작업별로 임시 디렉터리가 분리되어 있는지 확인합니다.
- 동일한 Scheme으로 서명 없는 빌드를 한 번 더 실행해 모듈 컨텍스트를 검증합니다.
경고 기준선에는 안정적인 식별자를 저장하고 일시적인 절대 경로는 기록하지 않아야 합니다. Xcode를 업그레이드할 때는 별도 브랜치에서 먼저 사전 점검을 실행하고 새 진단 항목을 검토한 다음 기준선을 업데이트해야 합니다. 업그레이드 커밋에서 새 경고를 모두 곧바로 필터링해서는 안 됩니다.
롤백 가능한 도입 순서 마련하기
처음 도입할 때는 일주일 동안 로그만 수집하고 병합을 차단하지 않아도 됩니다. 오탐의 원인을 확인한 뒤 결정적인 errors를 실패 조건으로 설정하고, 새 warning을 차단하는 품질 게이트를 단계적으로 활성화합니다. 스크립트 자체는 저장소에 고정해 로컬 Mac과 클라우드 Mac이 함께 사용하도록 해야 하며, CI 설정 안에 별도의 구현을 숨겨 두어서는 안 됩니다.
최종 점검 항목은 다음과 같습니다. 툴체인이 고정되어 있어야 하고, 검색 디렉터리에 명확한 제외 항목이 있어야 하며, 각 작업은 독립적인 임시 디렉터리를 사용해야 합니다. 배포 대상은 프로젝트 설정에서 가져오고, 실패 로그는 첨부 파일로 보관하며, 현지화 키는 비교만 하고 덮어쓰지 않아야 합니다. 또한 전체 Scheme 빌드도 계속 실행해야 합니다. 이렇게 하면 인터페이스 리소스 문제가 아카이브 마지막 단계에서 재현하기 어려운 오류로 남는 대신 수십 초 안에 끝나는 사전 점검 단계에서 드러납니다.
자주 묻는 질문
ibtool 사전 점검이 전체 Xcode 빌드를 대신할 수 있나요?
아닙니다. 인터페이스 리소스의 구조와 컴파일 가능성을 빠르게 확인할 뿐이며 실제 Scheme 빌드와 테스트는 계속 필요합니다.
로컬에서 열리는 Storyboard가 CI에서 실패하는 이유는 무엇인가요?
서로 다른 Xcode 선택, 배포 대상 차이, 경로 대소문자 오류 또는 사용자 정의 클래스가 포함된 모듈을 찾지 못하는 경우가 흔합니다.
ibtool의 모든 notice를 실패로 처리해야 하나요?
권장하지 않습니다. 먼저 errors만 차단하고 warnings는 검토된 기준과 비교하며 notices는 진단 로그로 보관합니다.
전용 물리 노드에서 다음 작업 실행
노드와 결제 주기를 선택하고 고정된 M4, 16GB RAM 및 256GB SSD 구성으로 배포를 시작하세요. 실제 사용 가능 여부는 콘솔에 실시간으로 표시되는 결과를 기준으로 합니다.