Xcode 빌드 시스템이 내부적으로 보유한 구조화된 이벤트 데이터를 활용해, 빌드 실패 진단과 성능 분석을 AI 에이전트 친화적으로 만드는 방법을 탐구한다.
URL: https://tuist.dev/ko/blog/2025/11/27/teaching-ai-to-read-xcode-builds
Title: Teaching AI to Read Xcode Builds
Xcode 빌드가 실패하면 개발자는 본능적으로 로그를 찾습니다. 우리는 텍스트의 벽을 스크롤하며, 소음 속에 묻힌 난해한 링커 에러나 정체불명의 크래시를 찾습니다. 빌드 시스템은 실제로 무슨 일이 일어났는지(모든 컴파일, 모든 의존성 해석, 모든 타이밍 지표)를 정확히 알고 있지만, 그 언어는 사람을 위한 것이 아니라 기계를 위해 최적화되어 있습니다. 그리고 AI 에이전트를 위한 것도 더더욱 아닙니다.
만약 이를 바꿀 수 있다면 어떨까요? AI 에이전트가 빌드의 텍스트 출력만 파싱하는 것이 아니라, 실제로 빌드를 “이해”할 수 있다면요?
이것은 가능한 미래를 탐구하는 글입니다. 여기서 논의되는 일부 개념은 이상(aspirational)이며, 완전히 실현하려면 Apple의 지원이 필요합니다.
이런 상황을 상상해 보세요. 큰 iOS 앱을 빌드하다가 링커 에러를 만났습니다. 출력은 대략 이렇게 보입니다:
ld: warning: ignoring duplicate libraries: '-lz'
ld: Undefined symbols:
_OBJC_CLASS_$_SomeFramework, referenced from:
objc-class-ref in MyTarget.o
clang: error: linker command failed with exit code 1 (use -v to see invocation)
그 다음은 어떻게 하시나요? 수백 줄짜리 컴파일 출력 위로 스크롤을 올리고, error를 grep 하고, Swift Package 의존성이 제대로 해결됐는지 확인하고, 빌드 폴더를 클린한 다음 다시 시도합니다. 그래도 안 되면 DerivedData 폴더를 통째로 지우고 처음부터 다시 빌드하기도 합니다. 어떤 때는 그게 해결책이 되지만, 어떤 때는 아무 소용이 없고 “대체 뭐가 문제였지?”라는 의문만 남습니다.
짜증나는 부분은, 빌드 시스템은 정확히 무슨 일이 일어났는지 알고 있다는 점입니다. 어떤 타깃이 실패했는지, 어떤 의존성이 빠졌는지, 전체 의존성 그래프가 어떻게 생겼는지, 각 단계가 얼마나 걸렸는지까지 알고 있습니다. 하지만 이런 풍부한 구조화 데이터를 노출하는 대신, 1980년대 터미널을 위해 설계된 것 같은 텍스트 스트림으로만 이야기합니다.
우리 모두 겪어봤습니다. 몇 분이면 진단할 수 있었던 빌드 실패에 몇 시간을 날린 적이요.
커맨드라인에서 xcodebuild를 실행하면, 진행 표시, 컴파일러 호출, 경고, 에러가 하나의 구분되지 않는 스트림으로 뒤섞인 출력이 나옵니다:
CompileSwift normal arm64 /path/to/File1.swift
CompileSwift normal arm64 /path/to/File2.swift
CompileSwift normal arm64 /path/to/File3.swift
...hundreds more lines...
/path/to/File47.swift:23:15: error: cannot find 'SomeType' in scope
이 출력은 디버깅을 위해 설계된 것이 아닙니다. 로깅을 위해 설계됐습니다. 구조도, 계층도 없고, 텍스트 자체를 파싱하지 않고서는 컴파일 단계와 링커 호출을 프로그램적으로 구분할 방법도 없습니다.
사람에게는 xcbeautify 같은 도구가 컬러를 입히고 노이즈를 줄여줘서, 텍스트의 벽을 더 쉽게 훑게 해줍니다. 하지만 이런 도구도 결국 비정형 텍스트를 “더 예쁜 비정형 텍스트”로 만드는 것뿐입니다. 터미널을 사람이 보기 좋게 만드는 데 최적화되어 있지, 에이전트가 이해하기 쉬운 형태로 만드는 데 최적화되어 있지는 않습니다.
이제 AI 에이전트가 빌드 실패 디버깅을 도와주려고 할 때를 생각해 봅시다. 에이전트가 받는 출력은 비정형 텍스트입니다. 자유형 문자열을 파싱하고, 줄들 사이의 관계를 추측하고, 에러 메시지에 유용한 맥락이 충분히 들어 있기를 바라야 합니다.
설령 완벽하게 파싱한다 하더라도(형식이 공식적으로 명세되어 있지 않고 Xcode 버전마다 바뀔 수 있어 어렵습니다), 로그 자체에는 애초에 들어 있지 않은 핵심 정보가 있습니다. 이런 정보는 xcodebuild 출력에는 절대 나타나지 않습니다. 빌드 시스템 내부에는 존재하지만, 터미널로는 나오지 않습니다.
xcsift라는 흥미로운 프로젝트가 있습니다. 이 도구는 사람이 읽기 좋게 출력 포맷을 꾸미는 대신, xcodebuild 출력을 AI가 소비하기 좋은 구조화된 JSON으로 변환합니다:
bash
xcodebuild build 2>&1 | xcsift --format json
이렇게 하면 오류, 경고, 테스트 실패를 추출해 적절한 데이터 구조로 정리한 머신 리더블 출력이 생성됩니다. 또한 JSON보다 토큰 사용량을 30~60% 줄이는 “TOON”이라는 커스텀 포맷도 제공하는데, AI API 호출에서 토큰당 과금할 때 중요한 요소입니다.
정말 영리한 해결책입니다. xcodebuild가 노출하는 제약 안에서 최선을 끌어냈습니다.
하지만 근본적인 한계가 있습니다. xcodebuild 출력은 실제로 일어나는 일을 “평탄화(flattened)”한 표현입니다. 빌드 시스템은 내부적으로 풍부한 의존성 그래프(타깃 A는 타깃 B에 의존, 파일 X는 모듈 Y를 import, 이 컴파일은 저 링크 단계가 끝날 때까지 대기)를 유지합니다. 그런데 stdout으로 쓸 때는 그 구조가 선형 텍스트 스트림으로 직렬화됩니다. 그래프가 리스트가 되고, 관계가 사라집니다.
xcsift는 그 리스트를 아름답게 파싱할 수 있지만, 그래프를 복원할 수는 없습니다. 타깃 A가 실패했다는 건 알려줄 수 있어도, 타깃 B, C, D가 A를 기다리다 실행조차 못 했다는 건 알 수 없습니다. 어떤 파일이 컴파일됐다는 건 알 수 있어도, 왜 재컴파일이 필요했는지나 그 결과 어떤 다운스트림 작업이 트리거됐는지는 알 수 없습니다.
빌드 시스템은 이 정보를 가지고 있습니다. 다만 공유하지 않을 뿐입니다.
빌드 로그를 파싱하는 것과, 빌드 시스템이 실제로 무엇을 하고 있는지 “관측(observe)”하는 것 사이에는 의미 있는 차이가 있습니다. 회의록을 읽는 것과 실제 회의실에 있는 것의 차이와 비슷합니다. 회의록은 발언을 담지만, 타이밍, 맥락, 반응, 옆 대화는 잃습니다.
xcodebuild 출력을 파싱할 때는 빌드 시스템이 “출력하기로 선택한 것”만 다루게 됩니다. 반면 빌드 시스템의 내부 메시지를 관측하면, 시작되고 끝난 모든 작업, 해결된 모든 의존성, 모든 타이밍 측정, 스케줄러의 모든 결정까지 전부 볼 수 있습니다.
그래서 우리는 이런 질문을 탐구하고자 했습니다. xcodebuild 출력 파싱 대신, 빌드 시스템이 내부적으로 작업을 조율할 때 쓰는 구조화 메시지에 직접 탭(tap)할 수 있다면 어떨까요? 그러면 빌드를 이해하고 디버깅하려는 AI 에이전트에게 무엇이 가능해질까요?
몇 달 전, 우리는 호기심이 생겼습니다. Xcode와 빌드 서비스 사이의 통신을 가로채기(intercept) 위해 XCBLoggingBuildService라는 작은 도구를 만들었습니다. 내부에서 실제로 무슨 일이 일어나는지 보고 싶었습니다.
그 결과는 빌드 관측 가능성(observability)에 대한 우리의 생각을 바꿨습니다.
Xcode에서 “Build”를 누르거나 xcodebuild를 실행할 때, 사실 여러분은 컴파일러와 링커를 직접 호출하는 게 아닙니다. 요청은 SWBBuildService라는 별도 프로세스로 전달됩니다. 이 프로세스가 실제 빌드 엔진입니다. Xcode는 요청을 보내고 응답을 받아 UI로 번역하는 프론트엔드에 가깝습니다. xcodebuild 역시 응답을 텍스트로 번역하는 역할을 합니다.
프로세스 간 통신은 텍스트가 아닙니다. JSON도 아닙니다. stdin/stdout 파이프를 통해 흐르는 MessagePack이라는 바이너리 직렬화 포맷입니다. 그리고 중요한 점: 모든 빌드 이벤트는 구조화 데이터를 가진, 개별적이고 타입이 있는 메시지(discrete, typed message)입니다.
Apple은 최근 이 빌드 시스템을 swift-build로 오픈 소스화했습니다. 이제 메시지 정의를 실제로 읽을 수 있습니다. 컴포넌트 사이에 어떤 데이터가 오가는지 정확히 볼 수 있습니다. 한때 블랙박스였던 것이 이제는 누구나 연구할 수 있는 소스 코드가 됐습니다.
구체적인 예를 봅시다. Swift 파일을 컴파일할 때 xcodebuild는 이렇게 출력합니다:
CompileSwift normal arm64 /path/to/MyFile.swift
딱 한 줄입니다.
하지만 내부적으로 빌드 서비스는 다음과 같은 BuildOperationTaskStarted 메시지를 보냅니다:
BuildOperationTaskStarted:
id: unique task identifier
targetID: which target this belongs to
parentID: the parent task (if nested)
info:
taskName: "CompileSwift"
executionDescription: "Compiling MyFile.swift"
interestingPath: "/path/to/MyFile.swift"
signature: content-addressable hash for caching
그리고 컴파일이 끝나면 BuildOperationTaskEnded 메시지가 옵니다:
BuildOperationTaskEnded:
id: same task identifier
status: succeeded | failed | cancelled
metrics:
utime: 1234567 # CPU time in user mode (microseconds)
stime: 234567 # CPU time in kernel mode (microseconds)
maxRSS: 104857600 # Peak memory usage (bytes)
wcDuration: 2500000 # Wall clock duration (microseconds)
차이가 보이시죠? 이 프로토콜은 컴파일이 얼마나 걸렸는지, 메모리를 얼마나 썼는지, 어떤 타깃에 속하는지, 더 큰 작업의 일부인지 여부까지 압니다. 또한 고유 식별자 덕분에 이 작업을 어떤 에러가 나왔는지, 어떤 출력이 생성됐는지, 의존성 그래프에서 어디에 위치하는지와 연관 지을 수 있습니다.
이 모든 정보는 터미널에는 도달하지 않습니다.
SWBProtocol 모듈을 들여다보면서 우리는 xcodebuild 출력에 절대 도달하지 않는 데이터를 계속 발견했습니다:
BuildOperationTaskUpToDate 메시지가 있습니다. 추측 대신 실제 캐시 히트율을 계산할 수 있습니다.BuildOperationDiagnosticEmitted 메시지로 옵니다.빌드 시스템은 이 모든 것을 알고 있습니다. 다만 우리에게 말해주지 않습니다.
그렇다면 빌드 시스템 안에서 흐르는 이 풍부한 구조화 메시지 스트림을 제대로 캡처하고 노출한다면, 실제로 무엇을 할 수 있을까요?
오늘날 빌드가 실패하면, AI 에이전트는 에러 메시지를 보고 학습 데이터에 대한 패턴 매칭으로 도와주려 합니다. 때로는 통하지만, 특정 프로젝트 맥락이 부족해 자주 실패합니다.
구조화된 빌드 데이터가 있다면, 에이전트는 다음과 같은 질문에 답할 수 있습니다:
NetworkKit이 빌드에 실패했기 때문에 발생했습니다. NetworkKit은 CoreUtilities에 의존하는데, CoreUtilities는 성공했으니 문제는 NetworkKit 자체에 국한됩니다."APIClient.swift와 Models/User.swift를 수정했습니다."더 이상 추측이 아닙니다. 실제 빌드 히스토리와 의존성 정보로 작업합니다.
빌드 성능은 보통 숫자 하나로 측정됩니다: 얼마나 걸렸나? 하지만 그 숫자는 복잡성을 숨깁니다. 60초 빌드가 60초인 이유는, 병렬화할 수 없는 거대한 타깃 하나 때문일 수도 있고, 긴 크리티컬 패스를 가진 작은 타깃 30개 때문일 수도 있고, 인크리멘털 빌드가 실제로는 인크리멘털이 아니기 때문일 수도 있습니다.
작업 단위 타이밍과 의존성 데이터가 있으면, 정말 중요한 질문에 답할 수 있습니다:
FeatureA → SharedUI → MainApp을 통해 지나갑니다. SharedUI에서 5초를 줄이면 전체 빌드도 5초 빨라집니다."Constants.swift 변경이며, 이 파일은 거의 모든 곳에서 import 됩니다."데이터에는 각 작업의 시작/종료 시간과 작업이 속한 타깃 정보가 포함됩니다. 이를 통해 시간에 따른 동시성(concurrency), 순차 체인(의존성일 가능성이 큼), 크리티컬 패스, 하나의 타깃이 여러 타깃을 막는 경합 지점(contension point)을 계산할 수 있습니다.
예를 들어 타깃 A의 작업들이 항상 끝난 직후 타깃 B, C, D의 작업이 시작된다면, A는 세 타깃의 공통 의존성일 가능성이 큽니다. A가 20초 걸리고 모두를 블록한다면, 그게 병목입니다. 프로토콜에는 타깃 의존성을 명시적 인접 리스트로 제공하는 DependencyGraphResponse 메시지도 있어, 타이밍으로 관계를 추론하는 대신 실제 의존성 그래프를 가져올 수도 있습니다.
이런 분석은 대기업의 빌드 엔지니어들이 빌드 트레이스와 의존성 그래프를 보며 수작업으로 하는 일입니다. 같은 데이터에 접근할 수 있다면, AI 에이전트가 자동으로 못 할 이유가 없습니다.
빌드 히스토리 데이터가 쌓이면 패턴이 드러납니다. 몇 주~몇 달의 빌드 기록에 접근할 수 있는 에이전트는 다음 같은 사실을 알아챌 수 있습니다:
Analytics 모듈에 12개의 새 Swift 파일이 추가된 시점과 상관관계가 있습니다."단일 빌드의 텍스트 출력만으로는 불가능합니다. 시간이 지나며 저장·질의·비교 가능한 구조화 데이터가 필요합니다.
가장 즉각적으로 유용한 응용은, 빌드에 대해 평범한 영어(혹은 자연어)로 질문할 수 있는 능력입니다:
이 질문들은 답이 있습니다. 빌드 시스템은 답을 알고 있습니다. 다만 물어볼 좋은 방법이 없을 뿐입니다.
AI 에이전트는 점점 진짜 개발 도구가 되고 있습니다. 사람들은 코드를 쓰고, 이슈를 디버깅하고, 리팩터링하고, 워크플로를 자동화하는 데 사용합니다. 하지만 빌드에 관해서는 에이전트가 눈이 가려진 상태입니다. 코드는 볼 수 있지만 코드가 어떻게 빌드되는지는 볼 수 없습니다.
프로젝트가 커지고 빌드 시스템이 복잡해질수록 이 격차는 더 아픕니다. 올바른 데이터만 있다면 에이전트가 몇 초 만에 진단할 일을, 개발자는 몇 시간씩 디버깅할 수도 있습니다.
구조화 메시지는 이미 존재합니다. 빌드 시스템이 이미 생성합니다. 문제는 이것을 개발자가 점점 더 의존하는 도구들이 접근할 수 있는 형태로 캡처할 수 있느냐입니다.
그렇다면 이 빌드 서비스는 어디에 있을까요? Xcode 설치 디렉터리를 파보면 여기서 찾을 수 있습니다:
/Applications/Xcode.app/Contents/SharedFrameworks/XCBuild.framework/
Versions/A/PlugIns/XCBBuildService.bundle
이 바이너리가 모든 일을 수행합니다. 무언가를 빌드할 때 Xcode는 이를 별도 프로세스로 실행하고 stdin/stdout 파이프로 통신합니다. swift-build의 BuildServiceEntryPoint.swift를 보면 인프로세스(in-process) 실행도 지원하는데, 아마 Xcode가 더 긴밀한 통합을 위해 그렇게 사용할 가능성이 있습니다.
빌드를 시작하기 전에 빌드 서비스는 프로젝트 구조를 이해해야 합니다. 이는 PIF(Project Interchange Format)라는 것을 통해 이뤄집니다.
.xcodeproj를 프로그램적으로 파싱해 본 적이 있다면, 악몽이라는 걸 아실 겁니다. .pbxproj 포맷은 난해하고 문서화가 빈약하며, 서로를 참조하는 UUID로 가득해 따라가기가 어렵습니다. PIF는 빌드 시스템이 내부적으로 실제 사용하는 포맷이며 훨씬 깔끔합니다.
swift-build의 ProjectModel 디렉터리를 보면 계층이 드러납니다. 워크스페이스는 프로젝트를 담고, 프로젝트는 타깃을 담고, 타깃은 빌드 페이즈를 담고, 빌드 페이즈는 파일을 담습니다. Workspace.swift, Project.swift, Target.swift 같은 단순한 Swift struct로 정의되어 있습니다.
흥미로운 점은 PIF가 인크리멘털 업데이트를 지원한다는 것입니다. 빌드 서비스는 프로젝트 구조를 캐시해두고, 다음 빌드에서는 바뀐 부분만 다시 전송합니다. 이것이 Xcode에서 인크리멘털 빌드가 매번 xcodebuild를 처음부터 돌리는 것보다 빠르게 느껴지는 이유 중 하나입니다.
빌드를 시작하면 Xcode와 빌드 서비스 사이에 대화가 오갑니다. 먼저 Xcode가 세션을 수립합니다. 그 다음 프로젝트 구조(혹은 업데이트)를 전송합니다. 그 다음 구성(configuration), 스킴, 타깃을 포함한 빌드 요청을 보냅니다.
실행 중에는 앞에서 말한 메시지가 흐르기 시작합니다. 타깃 시작/종료, 작업 시작/종료, 진행 업데이트, 진단 정보 등입니다. 모든 것이 끝나면 전체 상태와 집계 메트릭을 담은 최종 메시지가 옵니다.
흥미로운 점은 세션이 재사용될 수 있다는 것입니다. Xcode는 각 빌드 후 연결을 끊지 않습니다. 세션을 살아 있게 유지해 이후 빌드에서 설정 단계를 건너뛰고 캐시된 상태의 이점을 얻습니다.
관측 가능성 측면에서 흥미로운 것은, Xcode가 커스텀 빌드 서비스 사용을 명시적으로 지원한다는 점입니다.
XCBBUILDSERVICE_PATH라는 환경 변수가 있습니다. 이를 설정하면 Xcode는 번들된 바이너리 대신 지정한 바이너리를 사용합니다. swift-build 저장소에는 swift-build를 소스에서 빌드하고 커스텀 서비스를 사용해 Xcode를 실행하는 launch-xcode 플러그인도 포함되어 있습니다:
bash
swift package --disable-sandbox launch-xcode
즉, 빌드 서비스를 수정해 원하는 메시지를 로깅하도록 만든 뒤, 다시 빌드하고 Xcode가 그 버전을 쓰게 할 수 있습니다. 또는 Xcode와 실제 빌드 서비스 사이에 프록시를 두어 메시지를 캡처할 수도 있습니다.
바로 그것이 XCBLoggingBuildService가 하는 일입니다. 메시지를 수정하지 않고 그대로 통과시키며 로그만 남기는 패스스루 프록시입니다:
bash
XCBBUILDSERVICE_PATH=/path/to/XCBLoggingBuildService xcodebuild build
프로토콜은 숨겨져 있지 않습니다. 오픈 소스이고, 코드로 문서화되어 있으며, Xcode가 구현체 교체를 명시적으로 지원합니다. 빌드 과정에 더 나은 관측성을 구축하려는 누구에게나 필요한 조각은 모두 있습니다.
빌드 메시지 캡처는 문제의 절반일 뿐입니다. 나머지 절반은 AI 에이전트에게 유용하게 만드는 것입니다.
원시 프로토콜 메시지를 에이전트 컨텍스트에 그냥 덤프하면 금방 한계에 부딪힙니다. 한 번의 빌드만으로도 수천 개의 메시지가 생성됩니다. 어느 컨텍스트 윈도우에도 너무 큽니다. 게다가 대부분은 지금 질문과 무관합니다.
다르게 생각해야 합니다. 에이전트에게 원시 데이터를 주는 대신, 적절한 세부 수준에서 “필요한 데이터”를 “필요한 시점”에 줘야 합니다.
바로 부딪힌 실용적 문제는 이것입니다: 빌드 실행과 그 데이터는 어떻게 연결할 것인가?
에이전트가 xcodebuild를 실행했다면, 나중에 “그 빌드에서 무슨 일이 있었지?”를 물을 방법이 필요합니다. 빌드 서비스는 출력에 자동으로 식별자를 붙이지 않습니다. 여러 빌드를 동시에 돌리거나 과거 데이터를 질의하고 싶다면, 서로를 연결할 수단이 필요합니다.
한 방법은 환경 변수로 빌드 ID를 전달하는 것입니다. 빌드 실행 전에 에이전트가 고유 ID를 생성합니다:
bash
BUILD_ID=$(uuidgen)
XCBBUILDSERVICE_PATH=/path/to/logging-service \
BUILD_TRACE_ID=$BUILD_ID \
xcodebuild build -scheme MyApp
로깅 서비스는 환경에서 BUILD_TRACE_ID를 읽어 캡처한 모든 메시지에 태그를 붙입니다. 나중에 에이전트가 같은 ID로 질의할 수 있습니다.
단순해 보이지만, 데모와 실제 사용 가능한 도구의 차이를 만드는 접착제(glue)입니다. 상관관계가 없으면 “마지막 빌드 보여줘” 수준에 머물고, 동시에 다른 빌드가 섞였을 가능성에 기대야 합니다.
이 많은 메시지를 어디에 넣을까요? SQLite가 자연스럽습니다.
과해 보일 수 있지만, SQLite는 이 용도에 좋은 성질이 있습니다. 단일 파일이라 설정이 필요 없고, 질의 가능해서 모든 데이터를 메모리에 올리지 않고도 필요한 질문만 할 수 있으며, 포터블해서 머신 간 복사나 팀 공유도 쉽습니다.
핵심은 원시 메시지를 그대로 저장해 직접 질의하는 게 아니라, 에이전트가 자주 물을 만한 것들을 미리 계산해둬야 한다는 점입니다. 레이어를 생각해 보죠:
요약 레이어(Summary layer). 빌드당 한 행. 총 소요 시간, 타깃 수, 작업 수, 에러/경고 수, 캐시 히트율, 성공/실패 여부 같은 사전 계산 메트릭을 담습니다. 대략 50 토큰 정도입니다. 에이전트는 거의 비용 없이 빌드의 큰 그림을 얻습니다.
Top-N 레이어. 가장 느린 타깃, 가장 느린 작업, 최근 에러 등. 미리 정렬하고 제한합니다. “왜 느렸어?”라는 질문에 전체 작업 리스트를 덤프하는 대신 몇백 토큰으로 답할 수 있습니다.
디테일 레이어(Details layer). 개별 진단, 작업별 타이밍 분해, 파일 위치가 포함된 전체 에러 메시지. 특정 항목을 파고들 때만 가져옵니다.
원시 레이어(Raw layer). 원본 메시지를 JSON으로 저장. 거의 접근하지 않지만, 이상한 상황 디버깅이나 다른 레이어로 답이 안 나오는 질문에 대비해 남겨둡니다.
스키마는 대략 이렇게 생길 수 있습니다:
sql
-- Summary layer: one row per build with pre-computed metrics
CREATE TABLE builds (
id TEXT PRIMARY KEY,
build_id INTEGER,
started_at TEXT NOT NULL,
ended_at TEXT,
status TEXT,
duration_seconds REAL,
target_count INTEGER DEFAULT 0,
task_count INTEGER DEFAULT 0,
error_count INTEGER DEFAULT 0,
warning_count INTEGER DEFAULT 0,
cache_hit_count INTEGER DEFAULT 0,
cache_miss_count INTEGER DEFAULT 0
);
-- Details layer: individual targets with timing data
CREATE TABLE build_targets (
id INTEGER PRIMARY KEY,
build_id TEXT NOT NULL,
target_id INTEGER NOT NULL,
guid TEXT NOT NULL,
name TEXT NOT NULL,
project_name TEXT,
configuration_name TEXT,
started_at TEXT NOT NULL,
ended_at TEXT,
duration_seconds REAL,
task_count INTEGER DEFAULT 0,
status TEXT
);
-- Details layer: individual tasks with timing and resource metrics
CREATE TABLE build_tasks (
id INTEGER PRIMARY KEY,
build_id TEXT NOT NULL,
task_id INTEGER NOT NULL,
target_id INTEGER,
parent_id INTEGER,
task_name TEXT,
rule_info TEXT,
execution_description TEXT,
interesting_path TEXT,
started_at TEXT NOT NULL,
ended_at TEXT,
status TEXT,
duration_seconds REAL,
utime_usec INTEGER,
stime_usec INTEGER,
max_rss_bytes INTEGER,
was_cache_hit INTEGER DEFAULT 0
);
-- Details layer: structured diagnostics with file locations
CREATE TABLE build_diagnostics (
id INTEGER PRIMARY KEY,
build_id TEXT NOT NULL,
kind TEXT NOT NULL,
message TEXT NOT NULL,
file_path TEXT,
line INTEGER,
column_number INTEGER,
target_id INTEGER,
task_id INTEGER,
timestamp TEXT NOT NULL
);
-- Dependency graph: target dependencies for bottleneck analysis
CREATE TABLE target_dependencies (
id INTEGER PRIMARY KEY,
build_id TEXT NOT NULL,
target_guid TEXT NOT NULL,
depends_on_guid TEXT NOT NULL
);
-- Top-N layer: pre-sorted views for common queries
CREATE VIEW slowest_targets AS
SELECT build_id, name, project_name, duration_seconds, task_count, status
FROM build_targets
WHERE duration_seconds IS NOT NULL
ORDER BY build_id, duration_seconds DESC;
CREATE VIEW slowest_tasks AS
SELECT build_id, task_name, execution_description, interesting_path,
duration_seconds, utime_usec, stime_usec, max_rss_bytes
FROM build_tasks
WHERE duration_seconds IS NOT NULL AND was_cache_hit = 0
ORDER BY build_id, duration_seconds DESC;
화려할 필요는 없습니다. 목표는 흔한 질의를 빠르고 저렴하게 만드는 것입니다.
데이터를 저장했다면, 에이전트는 어떻게 접근할까요?
가장 단순한 접근은 CLI입니다. 에이전트는 이미 셸 명령을 실행할 수 있으므로 별도 서버나 프로토콜이 필요 없습니다. 데이터를 캡처하는 동일 실행 파일에 CLI를 포함해 다음 같은 커맨드를 제공할 수 있습니다:
bash
# High-level summary of a build
SWBBuildService trace summary --build latest
# What went wrong?
SWBBuildService trace errors --build latest
# Why was it slow?
SWBBuildService trace slowest-targets --build latest --limit 5
SWBBuildService trace slowest-tasks --build latest --limit 10
# Find parallelization bottlenecks
SWBBuildService trace bottlenecks --build latest
# Show the critical path
SWBBuildService trace critical-path --build latest
# Compare two builds
SWBBuildService trace diff --builds abc123 def456
# Find historical patterns
SWBBuildService trace search-errors --pattern "linker"
에이전트는 커맨드를 호출해 구조화된 출력을 받으면 됩니다. 실행할 서버도, 구현할 프로토콜도 없습니다. CLI가 DB 질의를 처리하고, 필요에 따라 JSON 혹은 사람이 읽기 좋은 형태로 반환합니다.
또한 편리한 별칭도 지원할 수 있습니다. 정확한 빌드 ID를 요구하는 대신, 가장 최근 빌드 latest, 특정 스킴의 최근 빌드 latest:MyScheme, 최근 실패 failed 같은 표현을 허용하는 방식입니다.
이 접근을 검증하기 위해, 19개 타깃과 수천 개 작업을 가진 대형 오픈 소스 프로젝트인 Wikipedia iOS 앱의 실제 빌드를 계측(instrumentation)했습니다. 빌드 서비스가 캡처한 내용은 다음과 같습니다:
클린 빌드는 457초에 완료되었고, 19개 타깃과 3,303개의 개별 작업에 걸쳐 진행되었습니다. 처음부터 시작했기 때문에 캐시 히트는 0이었습니다. 빌드는 성공했지만, 조사해 볼 만한 경고가 86개 있었습니다.
여기서 의존성 그래프가 매우 유용해집니다. 어떤 타깃이 다른 타깃을 막는지, 그리고 얼마나 오래 걸리는지 분석하면 “병목 점수”(duration × dependent count)를 계산할 수 있습니다.
WMF 프레임워크가 핵심 병목입니다. 246초가 걸리며, ContinueReadingWidget, Wikipedia, WidgetsExtension, NotificationServiceExtension 4개 타깃이 시작하는 것을 막습니다. 앱 익스텐션들이 모두 여기에 의존합니다. 빌드 시간을 개선하고 싶다면 여기부터 집중해야 합니다. WMF를 더 작고 독립적인 모듈로 쪼개거나, 내부에서 작업을 병렬화할 방법을 찾는 것이죠.
또 다른 주목할 병목으로는 CocoaLumberjack(34초, 3개 타깃 블로킹)과 개별 앱 익스텐션들(각각 약 252초, 메인 Wikipedia 타깃을 블로킹)이 있습니다.
의존성 그래프를 이용하면, 최소 가능한 빌드 시간을 결정하는 가장 긴 의존성 체인(=크리티컬 패스)을 계산할 수 있습니다: WMF(246s) ->ContinueReadingWidget(253s) ->Wikipedia(456s). 합계 955초의 직렬 작업입니다.
이는 중요한 사실을 말해줍니다. 만약 이 타깃들이 순차적으로만 실행된다면 빌드는 955초가 걸립니다. 실제 빌드는 457초였으니, 병렬화로 대략 2.1배 속도 향상을 달성한 것입니다. 동시에 한계도 보입니다. 크리티컬 패스 위에 있는 것보다 더 빠르게 만드는 병렬화는 존재하지 않습니다.
가장 느린 5개 타깃은 흥미로운 이야기를 들려줍니다: Wikipedia(456s, 926 tasks), ContinueReadingWidget(253s, 61 tasks), WidgetsExtension(252s, 95 tasks), NotificationServiceExtension(252s, 60 tasks), WMF(246s, 902 tasks).
ContinueReadingWidget은 작업이 61개뿐인데 253초가 걸리고, WMF는 작업이 902개인데도 시간이 약간 더 적습니다. 이는 위젯 컴파일이 몇 개의 비용 큰 파일에서 CPU 바운드일 가능성이 있는 반면, WMF는 내부 병렬화가 더 잘 된다는 것을 시사합니다.
개별 작업으로 내려가면 최적화 기회가 드러납니다. 에셋 카탈로그 컴파일이 단일 작업 중 가장 느렸고, 131초가 걸렸으며 메모리 3.1MB를 사용했습니다. 크리티컬 패스 타깃 시간의 거의 1/3에 해당하며, 병렬화할 수 없는 단일 작업으로 실행됩니다.
그 다음으로 느린 것은 배치 Swift 컴파일(83초, 373.7MB)로, ArticleURLListViewController.swift와 DonateFunnel.swift를 포함한 25개 파일을 커버했습니다. 개별 Swift 파일 컴파일도 함께 배치로 컴파일되기 때문에 비슷하게 81초 수준의 시간이 나타났습니다.
이 데이터를 갖춘 AI 에이전트는 실행 가능한 권고를 제공할 수 있습니다:
"Wikipedia iOS 빌드는 457초 걸렸습니다. 주요 병목은
WMF프레임워크(246s)이며, 이는 4개 타깃을 블록합니다. 다음을 고려해 보세요:
WMF 분리: 프레임워크에 902개의 빌드 작업이 있습니다. 2~3개의 더 작은 프레임워크로 나누면 병렬화가 개선될 수 있습니다.
에셋 카탈로그 최적화: 에셋 카탈로그 컴파일이 131초 걸리며 직렬로 실행됩니다. 기능별로 더 작은 카탈로그로 분리해 병렬 컴파일을 고려해 보세요.
빌드 순서 인사이트: 크리티컬 패스는 WMF -> ContinueReadingWidget -> Wikipedia입니다. 무한 병렬화가 가능하더라도 최소 직렬 시간은 955초이며, 현재 2.1배 병렬화를 달성하고 있습니다.
86개 경고 수정: 빌드 시간에는 영향이 없지만, 노이즈를 줄이고 잠재적 미래 문제를 예방합니다."
이는 로그 출력에 대한 추측이 아닙니다. 빌드 시스템이 내부적으로 추적하는 실제 의존 관계, 작업 타이밍, 리소스 메트릭에 기반한 분석입니다.
이번 탐구는 가능성을 보여주지만, 견고한 도구로 만들려면 몇 가지 과제를 인정해야 합니다.
swift-build 프로토콜이 오픈 소스가 된 것은 큰 진전입니다. 하지만 정말 유용해지려면 Apple이 이를 공식화할 필요가 있습니다. 예를 들어 xcodebuild에 빌드 트레이스 데이터를 저장할 위치(SQLite DB 경로)를 지정하는 플래그를 제공하고, 그 데이터를 질의하는 CLI 인터페이스를 제공하는 것이죠. 우리는 탐구 과정에서 이미 이를 만들어 봤고, Apple도 제대로 지원한다면 같은 것을 제공할 수 있습니다. 그러려면 데이터 스키마를 표준화하고, Xcode 버전이 바뀌며 스키마가 진화할 때 마이그레이션도 처리해야 합니다.
프로토콜 레벨 접근이 열어줄 가능성을 논하기 전에, 오늘날 이미 가능한 것들도 인정할 필요가 있습니다. Xcode는 .xcactivitylog 파일(gzip 압축된 구조화 빌드 로그)과 .xcresultbundle 디렉터리(테스트 결과, 코드 커버리지, 빌드 메타데이터 포함)를 생성합니다. 이 아티팩트들은 빌드 완료 후에도 도구가 파싱할 수 있는 방대한 정보를 담고 있습니다.
xclogparser 같은 도구는 수년간 이 파일들을 파싱해 타이밍 데이터, 경고, 에러를 질의 가능한 포맷으로 추출해 왔습니다. 이런 사후(post-hoc) 접근은 많은 유스케이스에서 잘 동작합니다. 빌드 성능을 분석하고, 시간에 따른 경고 추이를 추적하고, 느린 컴파일 유닛을 식별할 수 있습니다.
Tuist에서도 바로 이것을 만들었습니다. Build Insights 기능은 .xcactivitylog와 .xcresultbundle을 파싱해 팀에 빌드 시간, 캐시 효율, 히스토리 트렌드를 보여주는 대시보드를 제공합니다. 데이터는 개발자, CI 파이프라인, 시간을 가로질러 확장되며, 개별 빌드만으로는 절대 알아차리지 못할 패턴을 보여줍니다. 또한 테스트로도 확장하고 있습니다. Test Insights (공개 대시보드에 이미 제공)도 시간과 공간을 가로지르는 동일한 분석을 테스트 스위트에 제공합니다. 도입은 Xcode 스킴에 post-action을 추가하는 것만큼 간단합니다.
이는 “비전” 섹션에서 설명한 팀 단위 빌드 인텔리전스나 빌드 고고학 같은 것의 상당 부분이 빌드 후 아티팩트만으로도 가능하다는 의미입니다. 구조화 빌드 데이터의 가치를 얻기 위해 Apple의 새로운 확장 포인트 승인을 기다릴 필요는 없습니다.
그렇다면 .xcactivitylog를 파싱할 수 있는데, 왜 굳이 빌드 프로토콜을 다뤄야 할까요?
핵심 차이는 타이밍과 데이터 가용성입니다.
실시간 개입(Real-time intervention). 사후 아티팩트를 파싱할 때는 빌드가 이미 끝났습니다. 프로토콜 스트림을 보는 AI 에이전트는 빌드 도중 문제를 감지하고 행동할 수 있습니다. 빌드를 일시정지하거나, 개발자에게 알리거나, 수정 제안을 할 수 있습니다. 사후 파싱은 항상 “이미 발생한 일”에 반응하는 것입니다.
리빌드 인과성(Rebuild causality). 프로토콜에는 각 작업이 왜 재빌드됐는지 설명하는 BuildOperationBacktraceFrameEmitted 메시지가 있습니다. 룰이 이전에 실행되지 않았기 때문인지, 시그니처가 바뀌었기 때문인지, 입력이 재빌드됐기 때문인지 같은 인과 체인이 포함됩니다. 이는 인크리멘털 빌드 문제를 디버깅하는 데 매우 유용하지만, 일시적(ephemeral)입니다. 플래닝과 실행 중 프로토콜을 통해 흐르다가 사라집니다. 결과 번들은 작업이 실행됐다는 사실은 기록하지만, 실행에 이른 의사결정 트리는 기록하지 않습니다.
라이브 의존성 그래프 계산(Live dependency graph computation). 프로토콜은 플래닝 중 의존성이 어떻게 해석되는지 보여주는 ComputeDependencyGraphRequest와 ComputeDependencyGraphResponse 메시지를 노출합니다. 빌드 시스템이 계산하는 타깃 간 의존성의 실제 인접 리스트를 볼 수 있습니다. 이는 프로토콜에는 존재하지만, 결과 번들은 최종 출력만 담고 플래닝 결정은 담지 않습니다.
실시간 진행 상황(Real-time progress). BuildOperationProgressUpdated 메시지가 타깃 이름, 상태 메시지, 완료 퍼센트를 포함해 라이브 상태를 스트리밍합니다. 작업이 나타나고 완료되는 것을 실시간으로 보여주는 웹 대시보드를 만들 수 있습니다. 어디서든 CI 빌드를 라이브로 보는 경험 같은 것이 가능해집니다. 사후 파싱으로는 불가능합니다.
작업별 리소스 귀속(Per-task resource attribution). 결과 번들에는 집계 타이밍 데이터가 있지만, 프로토콜은 작업이 완료될 때마다 작업별 메트릭(유저 모드 CPU 시간, 시스템 CPU 시간, 피크 메모리, 벽시계 시간)을 스트리밍합니다. 이 정도의 세밀함은 어떤 타깃이 느린지뿐 아니라, 그 타깃 안에서 어떤 특정 컴파일 유닛이 리소스를 먹는지 식별하게 해줍니다.
Apple이 누구나 빌드 이벤트 스트림에 후킹할 수 있는 확장 가능한 아키텍처를 설계한다면, 커뮤니티는 그 위에 강력한 도구를 만들 수 있습니다. 빌드 이벤트에 대한 안정적인 계약(contract)은 CI 대시보드의 실시간 모니터링, 빌드 중 개입 가능한 AI 에이전트, 아직 상상하지 못한 커스텀 워크플로를 가능하게 할 것입니다.
swift-build에는 이미 필요한 조각이 있습니다. 부족한 것은 Apple이 이것을 “언제든 바뀔 수 있는 구현 디테일”이 아니라 공식 확장 포인트로 승인(bless)해주는 것입니다.
Wikipedia iOS 분석은 단일 빌드로도 무엇이 가능한지 보여줍니다. 하지만 진짜 잠재력은 시간에 따른 빌드를 생각할 때 나타납니다.
이후 내용의 상당 부분은 빌드가 끝난 후 .xcactivitylog와 .xcresultbundle을 파싱하는 것만으로도 오늘날 가능합니다. Tuist에서는 Build Insights로 바로 그것을 만들고 있습니다. 프로토콜 레벨 접근은 실시간 기능과 더 풍부한 인과성 데이터를 더해주지만, 구조화된 빌드 관측성의 가치를 얻기 위해 Apple을 기다릴 필요는 없습니다.
모든 PR에서 구조화 빌드 데이터를 캡처하는 CI 시스템을 상상해 보세요. 몇 주, 몇 달이 지나면 패턴이 나타납니다:
이건 이론이 아니라, 대기업이 커스텀 도구로 수작업 분석하는 종류의 일입니다. 구조화 빌드 데이터는 이를 모두에게 접근 가능하게 합니다. 그리고 이는 이미 가능합니다. 빌드 후 아티팩트에는 이런 대시보드를 만들기 위한 타이밍 데이터, 타깃 정보, 경고 카운트가 다 들어 있습니다.
히스토리 데이터와 의존성 정보가 있으면, 에이전트는 빌드 시작 전에도 가이드를 줄 수 있습니다:
"
Constants.swift를 수정하려고 하네요. 빌드 히스토리에 따르면 이 파일은 847개 다른 파일에서 import 하고 있어, 거의 클린 빌드 수준의 리빌드를 유발합니다. 더 타깃팅된 접근을 제안해 드릴까요?"
또는 코드 리뷰 중에:
"이 PR은
SharedFramework.h에 새 import를 추가합니다. 빌드 데이터에 따르면 이 헤더는 12개 타깃에서 포함됩니다. 이 변경은 해당 타깃들의 인크리멘털 빌드에 약 45초를 추가할 것으로 보입니다."
이 인사이트는 시간에 따른 빌드 데이터와 코드 변경을 상관 분석해 얻습니다. 빌드 아티팩트에는 타이밍 데이터가 있고, git 히스토리에는 무엇이 바뀌었는지가 있습니다. 둘에 모두 접근할 수 있는 AI 에이전트는 이 연결을 만들 수 있습니다.
여기서 프로토콜 레벨 접근이 필수입니다. 빌드 서비스는 실시간으로 이벤트를 생성하고, 에이전트는 이를 발생하는 즉시 관찰할 수 있습니다:
"빌드 진행 중…
Analytics타깃이 평소보다 오래 걸리고 있습니다(45초 vs. 보통 30초). 이는 어제 PR에서 3개의 새 트래킹 이벤트를 추가한 이후 시작되었습니다. 추가 컴파일 시간은EventBuilder.swift의 타입 추론에서 발생하고 있습니다."
사후 파싱은 느렸다는 사실을 나중에 알려줍니다. 프로토콜 레벨 접근은 “지금” 알려주며, 더 많은 CI 시간을 낭비하기 전에 빌드를 중단하거나 조사하거나 수정 조치를 취할 선택지를 제공합니다.
몇 주 후 문제가 발생했을 때, 구조화 데이터는 조사할 수 있게 해줍니다:
"이 링커 에러는 3일 전부터 나타나기 시작했습니다. 빌드 히스토리를 보면 마지막 성공 빌드는 커밋
abc123입니다. 그 이후 첫 실패까지 7개의 커밋이 있었고, 실패는NewFeature.framework를 추가하면서 라이브러리 검색 경로를 업데이트하지 않은 변경과 상관관계가 있습니다."
이런 분석은 빌드 후 아티팩트만으로도 충분히 가능합니다. 과거 빌드의 에러, 타이밍, 관련 커밋을 저장한 데이터베이스가 필요할 뿐입니다. 그 다음은 질의와 상관 분석이며, 이는 AI 에이전트가 특히 잘하는 일입니다.
빌드 시스템은 이 질문에 답하는 데 필요한 모든 것을 이미 알고 있습니다. 데이터는 빌드 후 아티팩트에도 있습니다. 우리는 단지 유용한 방식으로 캡처하고 노출해야 합니다.
이번 탐구를 통해 여러 가지를 배웠습니다:
빌드 시스템은 겉보기보다 훨씬 정교합니다. “파일 몇 개 컴파일”처럼 보이는 것은 실제로 캐싱, 병렬화, 상세 계측을 갖춘 복잡한 그래프 스케줄러입니다. 데이터는 존재하지만 노출되지 않습니다.
구조화 데이터는 AI가 할 수 있는 일을 근본적으로 바꿉니다. “CompileSwift normal arm64…”를 파싱하는 에이전트는 거의 아무 것도 할 수 없습니다. 반면 타이밍, 의존성, 메트릭이 포함된 구조화 이벤트를 받으면 정말 유용한 분석을 제공할 수 있습니다.
컨텍스트 윈도우 효율이 중요합니다. 3000개의 빌드 작업을 프롬프트에 덤프할 수는 없습니다. (요약 → top-N → 디테일 → 원시) 계층 접근은 “사용 불가”와 “실용적”의 차이를 만듭니다.
의존성 그래프가 빌드 최적화의 핵심입니다. WMF가 4개 타깃을 막는다는 사실은, WMF가 246초 걸린다는 사실보다 훨씬 실행 가능성이 높습니다. 그래프는 무엇을 고쳐야 하는지 알려주고, 타이밍만으로는 “느리다”는 것만 말해줍니다.
빌드 관측성은 투자 부족입니다. 개발자는 더 나은 도구가 있다면 몇 분이면 진단할 문제로 몇 시간을 잃습니다. “빌드 시스템이 아는 것”과 “개발자가 볼 수 있는 것”의 격차는 엄청납니다.
앞으로의 길은 두 갈래이며, 우리는 둘 다 적극적으로 진행 중입니다.
Tuist에서는 빌드 후 아티팩트를 활용해 Build Insights와 Test Insights를 만들고 있습니다. 이는 지금 당장 동작하고, 실험적 도구가 필요 없으며, 개발자·CI 파이프라인·시간 전반에 걸친 빌드 성능 가시성을 팀에 제공합니다. 도입은 Xcode 스킴에 post-action을 추가해 .xcactivitylog와 .xcresultbundle을 업로드하는 것만큼 간단합니다.
이것이 실용적인 경로입니다. Apple의 승인도, 빌드 서비스 교체도 필요 없고, 즉시 가치를 얻을 수 있습니다. 팀 전체 빌드 인텔리전스, 과거 트렌드, 경고 추적, 빌드 고고학 모두 사후 파싱으로 가능합니다.
실시간 기능과 더 풍부한 인과성 데이터를 위해, 우리는 AI 에이전트를 위한 에이전틱 인터페이스를 제공하는 swift-build 포크인 Argus를 오픈 소스했습니다. 이는 매우 실험적입니다. 빌드 이벤트 스트림에 직접 탭했을 때 가능한 것을 보여주며, 사후 파싱으로는 제공할 수 없는 기능을 포함합니다:
mise로 Argus를 전역 설치할 수 있습니다:
bash
mise use -g github:tuist/argus
그 다음 빌드 관측성을 활성화하려면, 에이전트의 메모리나 시스템 프롬프트에 다음을 추가합니다:
When running Xcode builds, use Argus to capture and analyze build data:
1. Run builds with Argus and a session ID for correlation:
BUILD_TRACE_ID=$(uuidgen)
XCBBUILDSERVICE_PATH=$(which argus) BUILD_TRACE_ID=$BUILD_TRACE_ID xcodebuild build -scheme MyScheme
2. Query build results using the session ID:
argus trace summary --build $BUILD_TRACE_ID
argus trace errors --build $BUILD_TRACE_ID
argus trace slowest-targets --build $BUILD_TRACE_ID --limit 5
argus trace bottlenecks --build $BUILD_TRACE_ID
Or use "latest" to query the most recent build:
argus trace summary --build latest
3. Use --json flag for programmatic access:
argus trace summary --build $BUILD_TRACE_ID --json
4. Run `argus trace --help` to discover all available commands.
개선 아이디어가 있다면 Argus 저장소에 PR이나 이슈를 열어 주세요. 무엇을 만들었는지 듣고 싶습니다. Mastodon, Bluesky, LinkedIn에서 학습을 공유하고 우리를 태그해 주세요.
프로토콜 정의를 보려면 swift-build 저장소를 확인하고, 빌드 최적화에 대해 논의하려면 Tuist 커뮤니티에 참여하세요. 개발 워크플로 확장에 도움이 필요하다면 상담 예약도 가능합니다.