여러 사람이 Xcode 프로젝트를 동시에 수정할 때 가장 위험한 실패는 컴파일 오류가 아니라, project.pbxproj가 손상됐는데도 텍스트 병합은 통과하는 경우다. 파일 참조가 삭제되거나 Scheme이 공유되지 않았거나, 중요한 빌드 설정이 UI 조작으로 조용히 바뀌면 아카이브 단계에 이르러서야 문제가 드러날 수 있다. 더 안전한 방법은 클라우드 Mac에서 프로젝트 파일을 독립적인 빌드 입력으로 취급하는 것이다. 먼저 비용이 적게 드는 일관성 검사를 수행한 뒤 의존성 해석, 테스트, 아카이브를 시작한다.
게이트에서 검사해야 할 항목
실용적인 게이트는 최소 네 계층을 다뤄야 하며, 순서도 바꾸면 안 된다. 먼저 텍스트 잔여물을 확인하고, 파일 형식을 검증한 다음, Xcode가 실제로 프로젝트를 파싱하게 하고, 마지막으로 핵심 설정을 비교한다. 앞 단계에서 실패하면 즉시 종료해 불필요한 빌드 시간을 소비하지 않아야 한다.
| 계층 | 검사 대상 | 실패가 의미하는 것 |
|---|---|---|
| 텍스트 | 충돌 마커, 빈 파일 | 병합이 아직 완료되지 않음 |
| 구조 | project.pbxproj |
plist 구조를 파싱할 수 없음 |
| 프로젝트 | Project, Target, Scheme | Xcode가 프로젝트 모델을 구성할 수 없음 |
| 설정 | SDK, 배포 버전, 서명 방식 | 설정에 의도하지 않은 변화가 발생함 |
git diff에 충돌이 없다고 해서 Xcode 프로젝트가 유효한 것은 아니다. 버전 관리 도구는 텍스트 병합이 완료됐는지만 확인할 뿐, 객체 참조나 Target 또는 Scheme을 Xcode가 여전히 인식할 수 있는지는 알지 못한다.
검사 스크립트는 실행 디렉터리와 Xcode 경로를 고정해야 하며, 대화형 Shell의 현재 상태에 의존해서는 안 된다. 프로젝트에 .xcodeproj와 .xcworkspace가 모두 있다면 일상적인 빌드에서 실제로 사용하는 진입점을 기준으로 검사하되, 하위의 project.pbxproj도 별도로 확인해야 한다.
병합 잔여물과 형식 오류부터 차단하기
아래 스크립트를 ci/check_xcode_project.sh로 저장할 수 있다. 예시는 프로젝트 이름이 App.xcodeproj라고 가정한다. 실제 사용 시에는 환경 변수로 재정의해 프로젝트 이름이 여러 CI 설정에 흩어지지 않도록 한다.
#!/bin/bash
set -euo pipefail
PROJECT_PATH="${PROJECT_PATH:-App.xcodeproj}"
PBXPROJ="${PROJECT_PATH}/project.pbxproj"
test -s "$PBXPROJ" || {
echo "project.pbxproj is missing or empty"
exit 1
}
if grep -nE '^(<<<<<<<|=======|>>>>>>>)' "$PBXPROJ"; then
echo "merge conflict markers found"
exit 1
fi
plutil -lint "$PBXPROJ"
xcodebuild -list -json -project "$PROJECT_PATH" > /tmp/xcode-project-list.json
plutil -lint /tmp/xcode-project-list.json
충돌 마커는 줄 시작 위치에서만 일치시켜 업무용 파일 이름이나 주석에 우연히 등호 문자열이 들어갔을 때 오탐이 발생하지 않게 한다. plutil을 통과한 뒤에는 xcodebuild -list를 호출한다. 실제 Xcode 프로젝트 모델을 구성하는 것은 후자이기 때문이다. 명령이 0이 아닌 종료 코드를 반환하면 표준 오류 출력을 보존해야 하며, 프로젝트 파일을 자동으로 “수정”하거나 다시 생성해서는 안 된다.
Workspace 프로젝트 처리
Workspace를 사용한다면 진입점 검증을 한 번 더 추가한다.
WORKSPACE_PATH="${WORKSPACE_PATH:-App.xcworkspace}"
xcodebuild -list -json -workspace "$WORKSPACE_PATH" \
> /tmp/xcode-workspace-list.json
plutil -lint /tmp/xcode-workspace-list.json
Workspace만 검증하고 하위 프로젝트를 건너뛰어서는 안 된다. Workspace가 인식된다고 해서 그 안의 모든 프로젝트 파일에 병합 잔여물이 없다는 보장은 없다.
공유 Scheme과 대상 집합 검증
CI에 필요한 Scheme은 반드시 버전 관리에 포함해야 한다. 먼저 공유 Scheme 파일을 확인한 다음, xcodebuild -list의 JSON 출력에서 해당 이름이 있는지 검증할 수 있다. 시스템에 기본으로 포함된 Python만으로 파싱할 수 있으므로 추가 의존성을 설치할 필요가 없다.
SCHEME_NAME="${SCHEME_NAME:-App}"
SCHEME_FILE="${PROJECT_PATH}/xcshareddata/xcschemes/${SCHEME_NAME}.xcscheme"
test -s "$SCHEME_FILE" || {
echo "shared scheme is missing: $SCHEME_NAME"
exit 1
}
python3 - "$SCHEME_NAME" /tmp/xcode-project-list.json <<'PY'
import json
import sys
expected = sys.argv[1]
path = sys.argv[2]
with open(path, encoding="utf-8") as handle:
payload = json.load(handle)
schemes = payload.get("project", {}).get("schemes", [])
if expected not in schemes:
raise SystemExit(f"expected scheme not found: {expected}")
PY
저장소에 여러 앱이나 확장 프로그램이 있다면 “Scheme이 하나 이상 존재함”을 허용하지 말고 명시적인 Scheme 목록을 관리해야 한다. 또한 개인 디렉터리의 Scheme을 CI 입력으로 사용해서는 안 된다. 다른 물리 노드로 전환하면 공유되지 않은 파일은 존재하지 않는다.
핵심 빌드 설정 스냅샷 만들기
프로젝트 파싱에 성공하더라도 설정이 실수로 바뀌었을 수 있다. PRODUCT_BUNDLE_IDENTIFIER, IPHONEOS_DEPLOYMENT_TARGET, SWIFT_VERSION, CODE_SIGN_STYLE, SUPPORTED_PLATFORMS처럼 산출물의 의미를 바꾸는 필드만 골라 스냅샷을 만드는 것이 좋다. 경로와 임시 디렉터리가 포함된 전체 -showBuildSettings 출력은 저장하지 않는다. 노드 간 비교에서 불필요한 차이가 대량으로 발생하기 때문이다.
xcodebuild -project "$PROJECT_PATH" \
-scheme "$SCHEME_NAME" \
-configuration Release \
-showBuildSettings |
awk -F ' = ' '
/PRODUCT_BUNDLE_IDENTIFIER =/ ||
/IPHONEOS_DEPLOYMENT_TARGET =/ ||
/SWIFT_VERSION =/ ||
/CODE_SIGN_STYLE =/ ||
/SUPPORTED_PLATFORMS =/ {
gsub(/^[ ]+/, "", $1)
print $1 " = " $2
}
' | LC_ALL=C sort > /tmp/build-settings.current
diff -u ci/build-settings.release /tmp/build-settings.current
ci/build-settings.release를 처음 만든 뒤에는 버전 관리에 포함해야 한다. 배포 버전이나 서명 방식을 의도적으로 조정할 때는 먼저 설정 차이를 검토하고, 같은 변경에서 스냅샷도 업데이트한다. CI가 기준 파일을 자동으로 덮어쓰게 해서는 안 된다. 그렇게 하면 모든 설정 변화가 새로운 기준으로 받아들여진다.
파이프라인 연동과 오탐 처리
일관성 게이트는 의존성 다운로드와 전체 빌드보다 먼저 배치하고, 스크립트의 실패 단계를 알아보기 쉽게 표시한다. 권장 순서는 코드 체크아웃, 고정된 Xcode 선택, 프로젝트 게이트 실행, 의존성 해석, 컴파일, 테스트, 아카이브다. 로컬과 클라우드 Mac에서 스크립트는 다음과 같이 동일한 진입점으로 실행해야 한다.
DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer" \
PROJECT_PATH="App.xcodeproj" \
SCHEME_NAME="App" \
bash ci/check_xcode_project.sh
흔한 오탐은 세 가지 원인에서 발생한다. 첫째, 개발자가 Scheme을 수정했지만 공유하지 않은 경우다. 해결 방법은 노드에서 수동으로 생성하는 것이 아니라 xcshareddata/xcschemes를 커밋하는 것이다. 둘째, 스냅샷에 절대 경로가 포함된 경우다. 이때는 필드 집합을 줄여야 한다. 셋째, 작업마다 서로 다른 Xcode 경로를 사용하는 경우다. 게이트를 시작하기 전에 xcodebuild -version을 출력해 확인하고 DEVELOPER_DIR도 고정해야 한다.
검사가 실패하면 작업 티켓이나 빌드 기록에 최소한 커밋 식별자, Xcode 버전, 실패한 명령, 표준 오류, 프로젝트 진입점을 남겨야 한다. 민감한 값이 포함된 전체 환경 변수는 업로드하지 않는다. 물리 노드를 교체해야 한다면 콘솔에서 현재 선택 가능한 구성을 확인하고, 새 노드가 동일한 저장소에서 게이트를 다시 실행하게 해야 한다. 이전 노드의 임시 프로젝트 상태를 복사해서는 안 된다.
병합 전 체크리스트
게이트를 제출하기 전에 다음 항목을 하나씩 확인한다. 스크립트에 set -euo pipefail이 활성화돼 있는지, 프로젝트 경로와 Scheme을 환경 변수로 재정의할 수 있는지, 충돌 마커 검사가 프로젝트 파일만 스캔하는지, 실제 진입점에 따라 Project와 Workspace를 각각 파싱하는지, 공유 Scheme이 버전 관리에 포함됐는지, 설정 스냅샷에 안정적인 필드만 남겼는지, 모든 기준 파일 업데이트가 사람의 검토를 거쳤는지 확인한다.
이 검사는 컴파일과 테스트를 대체하지는 않지만, 원래 아카이브 단계에서 나타나던 문제를 수초 수준의 단계에서 미리 발견할 수 있게 한다. 프로젝트 파일을 명시적이고 검토 가능하며 반복 실행할 수 있는 입력으로 다루면, 팀은 더 이상 “어떤 개발자의 로컬에서는 아직 프로젝트가 열린다”는 사실로 기본 브랜치의 상태를 판단할 필요가 없다.
자주 묻는 질문
plutil만 실행하면 Xcode 프로젝트가 정상인지 확인할 수 있나요?
아닙니다. plutil은 주로 구조와 구문 오류를 찾습니다. xcodebuild -list도 실행해 Xcode가 프로젝트를 해석하고 필요한 공유 Scheme을 표시하는지 확인해야 합니다.
일관성 검사는 CI의 어느 단계에서 실행해야 하나요?
모든 변경의 전체 빌드 전에 실행하고 공유 브랜치에서도 다시 실행합니다. 이렇게 하면 손상된 프로젝트가 긴 아카이브 시간을 사용하기 전에 중단됩니다.
의도적으로 빌드 설정을 바꾼 경우에는 어떻게 하나요?
차이를 검토한 뒤 버전 관리 중인 기준 스냅샷을 명시적으로 갱신합니다. CI가 기준을 자동 갱신하면 우발적인 변화를 구분할 수 없습니다.
다음 빌드를 독립 물리 노드에서 실행하세요.
칩, 메모리, 저장 공간, 노드와 대여 기간을 선택해 다른 고객과 리소스를 공유하지 않는 클라우드 Mac을 이용하세요.