원격 클라우드 Mac에서 Xcode 빌드 중 갑자기 Cycle inside ... 또는 Multiple commands produce ... 오류가 발생했을 때, 가장 흔히 저지르는 실수는 곧바로 DerivedData를 삭제하는 것입니다. 이렇게 하면 일시적으로 오류가 표시되는 순서가 달라질 수 있지만, 원인 분석에 필요한 단서까지 함께 사라집니다. 더 신뢰할 수 있는 방법은 코드, Scheme, 구성 및 Xcode 경로를 고정하고 전체 로그를 저장한 다음, 문제가 Target 의존성 순환인지 여러 Build Phase가 동일한 출력 경로를 두고 충돌하는 것인지 판단하는 것입니다.
먼저 오류를 안정적으로 재현하기
진단을 시작하기 전에 자동 코드 가져오기, 의존성 업그레이드 및 병렬 빌드를 일시 중지하여 두 번의 테스트 사이에 작업 공간이 변경되지 않도록 합니다. 현재 커밋, Xcode 버전, 실제로 선택된 개발자 디렉터리를 기록합니다.
set -o pipefail
git rev-parse HEAD
xcodebuild -version
xcode-select -p
mkdir -p .diagnostics
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-showBuildTimingSummary \
build 2>&1 | tee .diagnostics/build.log
프로젝트에서 Workspace를 사용한다면 -project App.xcodeproj를 -workspace App.xcworkspace로 바꿉니다. 두 옵션을 동시에 전달하면 안 됩니다. 그런 다음 주요 오류 위치를 추출하되, 문맥을 확인할 수 있도록 원본 로그는 그대로 보관합니다.
grep -nE \
'Cycle inside|cycle in dependencies|Multiple commands produce|will be run during every build' \
.diagnostics/build.log
로그의 마지막 명령은 대개 충돌이 감지된 위치일 뿐, 순환이 시작된 지점이라고 단정할 수 없습니다. Xcode가 출력한 의존성 체인을 거슬러 올라가며 처음으로 반복되는 Target, 스크립트 또는 산출물 경로를 찾아야 합니다.
비슷해 보이는 두 오류 구분하기
의존성 순환은 A가 B를 기다려야 하는 동시에 B도 직간접적으로 A를 기다려야 하는 상태를 뜻합니다. 중복 산출물 오류는 두 명령이 동일한 파일을 자신이 생성하거나 처리한다고 선언할 때 발생합니다. 두 문제가 함께 나타날 수도 있지만 해결 방법은 서로 다릅니다.
| 로그 특징 | 우선 확인할 항목 | 일반적인 근본 원인 |
|---|---|---|
Cycle inside |
Target Dependencies, 암시적 의존성 | App과 Framework가 서로 의존 |
Multiple commands produce |
Copy Files, Compile Sources, Run Script | 동일한 파일을 두 번 복사하거나 생성 |
| 스크립트가 빌드할 때마다 실행됨 | Run Script 입력 및 출력 | 의존성을 선언하지 않았거나 의존성 분석을 비활성화함 |
| Archive에서만 실패함 | Embed 단계, 아카이브 스크립트 | Debug와 Release의 단계 구성이 일치하지 않음 |
Xcode 화면의 현재 선택 항목을 보고 추측하지 말고, 명령을 사용해 이번에 실제로 빌드되는 Target과 구성을 확인합니다.
xcodebuild -list -json -project App.xcodeproj \
> .diagnostics/project-list.json
xcodebuild -showBuildSettings \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
> .diagnostics/build-settings.txt
Target과 Build Phase를 따라 의존성 체인 추적하기
Xcode의 Target Dependencies에서 오류 체인의 시작과 끝에 관련된 Target부터 확인합니다. App은 Framework에 의존할 수 있지만, Framework가 App 내부의 타입에 접근하기 위해 다시 App에 의존해서는 안 됩니다. 공유 코드는 별도의 모듈로 분리하거나 프로토콜과 의존성 주입을 사용해 역방향 참조를 제거해야 합니다.
그다음 Build Phases의 각 항목을 차례로 살펴보며 다음 관계를 중점적으로 확인합니다.
중복 소유권 확인하기
동일한 소스 파일이 같은 목적 파일을 생성하는 두 Compile Sources 항목에 동시에 포함되어서는 안 됩니다. 동일한 Framework를 시스템이 생성한 임베드 단계에서 처리하면서 사용자 지정 Copy Files 단계에서 다시 복사해서도 안 됩니다. 코드 생성기를 사용할 때는 생성 디렉터리를 명확히 지정하여, 다른 Target이 이미 컴파일하는 소스 디렉터리에 결과물을 다시 쓰지 않도록 해야 합니다.
암시적 의존성 확인하기
스크립트가 고정 경로를 통해 다른 Target의 빌드 산출물을 읽으면서 의존성을 명시적으로 선언하지 않으면, 병렬 처리 수준에 따라 빌드 순서가 달라질 수 있습니다. 병렬 빌드를 비활성화해 문제를 숨기지 마십시오. 이렇게 하면 오류가 발생하기 어려워질 뿐, 그래프 구조 자체는 수정되지 않습니다.
Run Script의 입력과 출력 명시하기
Run Script는 순환이 자주 발생하는 영역입니다. 스크립트가 구성 파일을 읽거나 Swift 파일을 생성하거나 리소스를 복사한다면 해당 단계의 Input Files와 Output Files를 작성하거나 파일 목록을 사용해야 합니다. 또한 각 출력은 하나의 단계에서만 담당하도록 스크립트를 구성해야 합니다.
예를 들어 버전 파일 생성 스크립트는 먼저 필수 변수를 검증한 뒤, 임시 파일과 원자적 교체 방식을 사용할 수 있습니다.
set -euo pipefail
input="${SRCROOT}/Config/version.txt"
output="${DERIVED_FILE_DIR}/GeneratedVersion.swift"
tmp="${output}.tmp"
test -f "$input"
mkdir -p "$(dirname "$output")"
version="$(tr -d '
' < "$input")"
printf 'enum GeneratedVersion { static let value = "%s" }
' \
"$version" > "$tmp"
if test -f "$output" && cmp -s "$tmp" "$output"; then
rm "$tmp"
else
mv "$tmp" "$output"
fi
해당 단계에서는 $(SRCROOT)/Config/version.txt를 입력으로, $(DERIVED_FILE_DIR)/GeneratedVersion.swift를 출력으로 선언해야 합니다. 생성 파일을 저장소의 파일에 직접 쓰는 대신 파생 디렉터리에 배치하면 소유권을 더 쉽게 판단할 수 있고, 병렬 작업이 서로의 결과물을 덮어쓰는 문제도 줄일 수 있습니다.
최소한의 변경으로 수정 사항 검증하기
한 번에 의존성 하나, 단계 하나 또는 출력 경로 하나만 변경한 뒤 로그를 다시 저장합니다. 여러 참조를 한꺼번에 삭제하면 빌드가 통과할 수는 있지만 실제 근본 원인을 확인하기 어렵고, 아카이브 구성에 존재하는 동일한 유형의 문제를 놓칠 수도 있습니다.
수정 후에는 다음 순서로 검증합니다.
- 클린 빌드를 한 번 실행하여 전체 그래프를 처음부터 구성할 수 있는지 확인합니다.
- 정리하지 않은 상태로 두 번 연속 빌드하여 입력이 변경되지 않았을 때 스크립트가 올바르게 건너뛰어지는지 확인합니다.
- Debug와 Release를 각각 확인하고, 배포용 프로젝트라면 Archive도 한 번 실행합니다.
- 두 빌드의 로그를 비교하여 새로운 중복 출력이나 무조건 실행되는 스크립트 경고가 없는지 확인합니다.
- Target 의존성 변경 사항, 단계별 입력과 출력, 변경 이유를 코드 리뷰 설명에 기록합니다.
DerivedData 정리는 마지막 격리 실험으로 사용할 수 있지만, 이를 수정 결과로 간주해서는 안 됩니다. 안정적인 Xcode 프로젝트라면 병렬 처리 수준이 달라지거나 클린 빌드와 증분 빌드를 수행하더라도 동일한 의존성 순서를 유지해야 합니다. 그래야 클라우드 Mac의 대화형 개발 작업과 무인 작업이 스케줄링 변화로 인해 무작위로 실패하지 않습니다.
자주 묻는 질문
DerivedData를 삭제하면 빌드 순환 문제가 해결되나요?
대부분 해결되지 않습니다. 임시 산출물은 제거되지만 Target 의존성과 중복 출력 경로가 그대로라면 다음 빌드에서 같은 오류가 다시 발생합니다.
Run Script 단계는 왜 빌드 그래프 순환을 만들기 쉬운가요?
스크립트가 입력과 출력을 선언하지 않은 채 다른 단계의 파일을 읽거나 같은 목적지에 쓰면 Xcode가 안정적인 실행 순서를 계산할 수 없습니다.
수정 결과는 어떻게 검증해야 하나요?
클린 빌드 한 번과 정리하지 않은 연속 빌드 두 번을 실행하고, 순환·중복 출력·항상 실행되는 스크립트 경고가 모두 사라졌는지 확인합니다.
다음 원격 Mac 작업을 전용 노드에서 실행하세요
작업 규모에 맞춰 판매 중인 두 가지 구성, 다섯 개 노드와 일·주·월·분기 단위 중에서 선택하세요. 주문 전에 구성과 USD 금액을 모두 확인할 수 있습니다.