로컬에서는 문제없이 빌드되던 클라우드 Mac 프로젝트를 무인 작업으로 전환할 때 가장 놓치기 쉬운 부분은 컴파일러 옵션이 아니라 Build Phases의 Run Script다. 스크립트가 저장소 외부의 설정을 읽거나 소스 디렉터리에 파일을 쓸 수도 있고, 출력이 선언되지 않아 빌드할 때마다 실행될 수도 있다. 한 번 실행할 때는 문제가 드러나지 않지만, 작업이 병렬로 수행되면 파일을 서로 덮어쓰고 증분 빌드도 점차 의미를 잃는다. 이런 문제를 해결하려면 재시도 횟수부터 늘릴 것이 아니라, 각 스크립트의 파일 경계를 명확히 선언한 다음 샌드박스를 활성화해 검증해야 한다.
재현 가능한 감사 기준선 만들기
먼저 프로젝트, Scheme, 빌드 구성, Derived Data 디렉터리를 고정한다. 감사 중에는 평소 사용하는 디렉터리를 재사용하지 않아야 한다. 기존 산출물 때문에 누락된 생성 단계가 정상 작동하는 것처럼 보일 수 있기 때문이다.
set -euo pipefail
ROOT="$PWD"
DERIVED="$ROOT/.audit-derived"
rm -rf "$DERIVED"
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
-derivedDataPath "$DERIVED" \
clean build | tee "$ROOT/audit-clean.log"
프로젝트가 Workspace를 사용한다면 -project를 -workspace로 바꾼다. 첫 번째 빌드에서는 전체 빌드 경로가 정상인지 확인한다. 그다음 소스 코드를 변경하지 않은 상태에서 clean 없이 다시 빌드한다. Run Script의 실행 횟수, 총 실행 시간, 출력 파일의 수정 시각을 기록한다. 두 로그는 각각 클린 빌드와 증분 빌드의 기준선이 된다.
감사의 목적은 모든 스크립트를 건너뛰게 만드는 것이 아니다. 각 스크립트가 입력이 변경되었거나 출력이 없거나 명시적인 실행 조건이 요구될 때만 실행되도록 하는 것이 핵심이다.
모든 Run Script 단계 파악하기
먼저 프로젝트 파일에서 스크립트를 찾은 뒤 Xcode로 돌아가 해당 스크립트가 어느 Target에 속하는지, 컴파일 전과 후 중 어디에 위치하는지, 의존성 분석이 활성화되어 있는지 확인한다. 텍스트 검색은 1차 점검에 적합하다.
grep -nE "PBXShellScriptBuildPhase|shellScript =|inputPaths =|outputPaths =" \
App.xcodeproj/project.pbxproj
각 단계별로 표를 만들고 스크립트 이름만 기록해서는 안 된다. 이름이 단순히 “Run Script”인 경우가 많아 문제 해결에 도움이 되지 않는다.
| 점검 항목 | 확인할 내용 |
|---|---|
| 실행 조건 | 매번 실행되는지, 의존성이 변경될 때만 실행되는지 |
| 입력 | 어떤 소스, 설정, 도구, 파일 목록을 읽는지 |
| 출력 | 생성 파일, 보고서, 완료 표식을 어디에 기록하는지 |
| 부작용 | 소스, 전역 설정, 공유 캐시를 수정하는지 |
| 동시성 | 두 작업이 동시에 실행될 때 같은 경로에 쓰는지 |
| 실패 처리 | 하위 명령이 실패하면 즉시 0이 아닌 상태를 반환하는지 |
스크립트 시작 부분에는 set -euo pipefail을 사용하는 것이 좋다. 파이프로 연결된 명령도 함께 확인해야 한다. pipefail이 없으면 앞선 명령의 실패가 마지막에 성공한 tee 때문에 가려질 수 있다.
xcfilelist로 파일 경계 선언하기
경로가 적다면 Input Files와 Output Files에 직접 입력할 수 있다. 파일이 많을 때는 .xcfilelist를 사용해야 검토하기 쉽다. 경로는 사용자 디렉터리를 하드코딩하지 말고 가능하면 $(SRCROOT), $(DERIVED_FILE_DIR) 같은 빌드 변수를 기준으로 작성한다.
예를 들어 YAML 설정에서 요약 값을 생성하는 단계라면 다음과 같은 입력 목록을 사용할 수 있다.
$(SRCROOT)/Config/app.yml
$(SRCROOT)/Scripts/generate-config.sh
출력 목록에는 스크립트가 실제로 생성하는 파일만 선언한다.
$(DERIVED_FILE_DIR)/Generated/config.sha256
해당 스크립트는 먼저 대상 디렉터리에 임시 파일을 쓴 다음 최종 파일을 원자적으로 교체해야 한다. 그래야 병렬 작업이 미완성 파일을 읽는 상황을 막을 수 있다.
set -euo pipefail
SOURCE="$SRCROOT/Config/app.yml"
OUTPUT="$DERIVED_FILE_DIR/Generated/config.sha256"
TEMP="$OUTPUT.tmp.$$"
mkdir -p "$(dirname "$OUTPUT")"
shasum -a 256 "$SOURCE" > "$TEMP"
mv "$TEMP" "$OUTPUT"
저장소 전체를 포괄적으로 입력으로 선언하거나 소스 루트 디렉터리를 출력으로 지정해서는 안 된다. 범위를 지나치게 넓히면 오류는 사라질 수 있지만, 어떤 파일이든 변경될 때마다 스크립트가 실행되고 실제 의존성도 가려진다. 생성물은 우선 Derived Data에 기록한다. 반드시 저장소에 다시 써야 하는 코드 생성 단계는 별도로 실행하고 버전 관리 시스템에서 변경 사항을 검사해야 한다.
샌드박스를 활성화하고 거부 로그 확인하기
1차 선언을 마쳤다면 처음부터 모든 구성을 수정하지 말고, 명령줄에서 빌드 설정을 재정의해 검증한다.
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
-derivedDataPath "$PWD/.audit-derived" \
ENABLE_USER_SCRIPT_SANDBOXING=YES \
build | tee "$PWD/audit-sandbox.log"
sandbox deny가 발생하면 먼저 거부된 경로, 작업 유형, 관련 단계를 찾는다. 설정을 읽었지만 선언하지 않았다면 입력에 추가하고, 선언하지 않은 파일을 생성하거나 수정했다면 출력에 추가한다. 도구가 시스템 런타임 라이브러리를 읽는다고 해서 시스템 디렉터리 전체를 목록에 넣을 필요는 없다. 스크립트가 명시적으로 접근하는 프로젝트 파일, 설정 파일, 자체 제작 도구 경로를 중점적으로 확인한다.
흔한 실수는 샌드박스를 곧바로 비활성화하거나 사용자 홈 디렉터리 전체를 입력 범위에 추가하는 것이다. 전자는 암시적 의존성을 그대로 남기고, 후자는 통제할 수 없는 재빌드를 유발한다. 스크립트가 저장소 외부 파일에 의존한다면 해당 파일을 작업 디렉터리로 복사하고 검증한 다음 명시적인 입력으로 사용해야 한다.
증분 빌드와 동시성 검증을 CI에 포함하기
수정 후에는 최소 네 가지 테스트를 수행한다. 빈 Derived Data에서 시작하는 클린 빌드, 소스 코드를 변경하지 않은 두 번째 빌드, 선언된 입력 하나를 수정한 뒤의 빌드, 서로 다른 두 Derived Data 디렉터리를 사용하는 병렬 빌드다. 병렬 작업은 소스 코드의 읽기 전용 복사본을 공유할 수 있지만 출력 디렉터리를 공유해서는 안 된다.
검증은 다음 순서로 진행한다.
- 클린 빌드가 처음부터 필요한 모든 산출물을 생성할 수 있다.
- 두 번째 빌드에서는 이미 안정적인 출력이 있는 스크립트가 무조건 실행되지 않는다.
- 선언된 입력을 수정하면 해당 단계가 다시 실행된다.
- 관련 없는 파일을 수정해도 해당 단계가 실행되지 않는다.
- 두 병렬 작업이 같은 임시 파일이나 보고서를 덮어쓰지 않는다.
- 스크립트가 실패하면
xcodebuild가 0이 아닌 상태를 반환한다. - 로그에 토큰, 개인 키 내용, 전체 환경 변수가 출력되지 않는다.
마지막으로 샌드박스 설정을 팀에서 실제로 사용하는 빌드 구성에 반영하고, 정기적으로 실행하는 클린 빌드 작업을 하나 유지한다. 증분 빌드는 속도를 담당하고 클린 빌드는 누락된 의존성을 찾는다. 두 방식이 모두 통과해야 Run Script에 재현 가능한 실행 경계가 갖춰졌다고 할 수 있다.
자주 묻는 질문
Run Script가 빌드할 때마다 실행되는 이유는 무엇인가요?
출력 파일이 선언되지 않았거나 의존성 분석 기반 실행 옵션이 꺼져 있을 가능성이 큽니다. 안정적인 입력과 출력 경로를 선언해야 Xcode가 재실행 여부를 판단할 수 있습니다.
스크립트 샌드박스의 deny 로그는 어떻게 해결하나요?
거부된 경로와 읽기·쓰기 방향을 확인한 뒤 필요한 경로를 Input Files나 Output Files 또는 각 File List에 추가합니다. 샌드박스를 끄는 방식으로 해결하지 않는 것이 중요합니다.
증분 빌드가 정상인지 어떻게 확인하나요?
같은 빌드를 두 번 실행해 두 번째 실행에서 스크립트가 생략되는지 확인하고, 선언된 입력 하나를 변경했을 때만 스크립트와 출력이 갱신되는지 검사합니다.
다음 iOS 빌드를 MiniDebug M4에서 실행하세요.
M4, 16GB RAM, 256GB SSD를 기본으로 제공하며 일·주·월·분기 단위로 대여할 수 있습니다. 싱가포르, 일본 도쿄, 한국 서울, 홍콩, 미국 동부 등 5개 노드 중에서 선택하세요. 실제 이용 가능 여부는 콘솔에서 실시간으로 확인할 수 있습니다.