- 187번 PR 중 86.6%가 warm — 일상 SLA는 warm만 보면 됨. cold를 P95에 섞지 말 것
- 의존성 변경, 캐시 삭제, scheme 교체 시 cold가 되고, 벽시계 2–3배 차이는 흔함
- 둘 다 warm이어도 cache miss, 다중 job 디스크 경합, 테스트·서명이 한 job에 있으면 여전히 들쭉날쭉
- 대략적 순서: 큐 → cold/warm 분리 → 캐시 → 동시 실행 제어 → 마지막에 하드웨어 (워터폴 분해 참고)
전체 대조 데이터 → GitHub Actions 최적화 기둥 · Benchmark
1. CI가 추첨 같음: 같은 commit인데 몇 배 차이
GitHub Actions에서 iOS CI 돌리다 보면 이런 장면 익숙할 겁니다. iOS CI가 느린 이유? GitHub Actions xcodebuild 속도 편차를 검색해 왔다면 특히 그렇죠.
- 같은 commit 재실행해도 벽시계가 2–3배 달라짐
- 대시보드 P95는 빨갛게 뜨는데, 체감은 「평소 merge는 그렇게 안 기다림」
pod install이 30초였다가, 다음엔 멈춘 것처럼 느껴짐- 금요일 오후엔 merge 버튼을 꺼려 함 — CI가 또 느린 쪽에 걸릴까 봐
반드시 CPU가 부족한 건 아닙니다. 팀과 14일 Shadow 듀얼런을 해보니 더 흔한 원인은 숫자를 섞어 봤고, 환경도 불안정한 경우였어요 — cache가 안 남고, 디스크가 경합되면 체감이 추첨 같습니다. 칩 교체는 보통 훨씬 뒤 순서입니다.
이 글은 한 가지만 다룹니다: 빌드가 왜 들쭉날쭉한가. job이 runner 큐에서 대기? → 큐 대응. cache 설정, 구매 vs 임대는 cache 전문과 ROI 글에 맡깁니다.
2. cold와 warm: 한 냄비에 넣지 말기
자주 빠지는 함정: 모든 빌드 시간을 하나의 P95에 넣는 것. 가끔 cold가 끼면 꼬리가 길어져요 — 데이터만 보면 「매일 느리다」인데, 실제 merge 체감은 그렇지 않을 수 있습니다.
- warm: 의존성 그대로, cache 유지, scheme 동일 — 대부분 증분 컴파일. 「평소 merge」를 대표
- cold: lock 파일 변경, cache 삭제, target·scheme 교체, runner 첫 실행 — resolve·pod install·대규모 재컴파일
일상 SLA는 warm P50/P95만 보면 됩니다. cold는 별도 라인으로 — merge 체감과 묶지 마세요.
2.1 언제 cold가 되나
macos-latest에서는 cold가 더 흔합니다 — job 끝나면 workspace가 지워지고 cache가 「정착」하기 어렵거든요.
| 트리거 | 전형적 추가 시간 | 로그 키워드 |
|---|---|---|
Podfile.lock 변경 |
+3–8분 | pod install, Downloading dependencies |
| DerivedData miss / 삭제 | +5–15분 | CompileSwift, 전체 .o 재생성 |
| scheme / target 전환 | +2–10분 | 다른 xcodebuild -scheme |
| SPM 의존성 resolve 변경 | +1–5분 | Resolve Package Graph |
| Xcode 소버전 업그레이드 (macos-latest) | 첫 빌드 +10–20분 | 새 SDK / 모듈 cache 재구축 |
의존성을 자주 올리면 cold가 늘어납니다 — 머신이 늙은 게 아니라 이번 주에 한 일이 다르다는 뜻이에요. 「Pod 올린 주」와 「평소 개발 주」를 나눠 보고하면 숫자가 말이 됩니다.
2.2 warm인데도 왜 불안정한가
둘 다 warm이어도 벽시계가 30% 정도 흔들릴 수 있습니다. 흔한 원인:
- cache miss: key에
arm64누락, 브랜치 미바인딩, 여러 job이 같은 슬롯 경합 - 같은 org에서 macOS job이 몰리면 디스크·네트워크가 서로 끌어내림
- 이번엔 main만 빌드, 다음엔 unit+UI test 전체 — 작업량 자체가 다름
- 변경 범위: Pod 소스 vs SwiftUI 프리뷰 한 줄 — compile 양이 크게 다름
3. 시간이 어디로 가나
workflow 각 step에 타임스탬프를 찍으면 벽시계를 대략 다섯 덩어리로 나눌 수 있어요. 아래는 warm일 때 흔한 분포입니다 (프로젝트마다 다름).
① checkout + env setup ~0:30 – 1:30 ② pod install / SPM resolve ~0:30 – 2:00 (cold 시 ↑↑) ③ xcodebuild compile+link ~3:00 – 8:00 (코드 변경 범위에 좌우) ④ tests (simulator / unit) ~1:00 – 6:00 (선택, 흔히 과소평가) ⑤ archive + codesign ~1:00 – 4:00 (릴리스 파이프라인) warm P50 합계 흔한 구간: 6 – 14분
실전 팁: step 시간이나 time으로 pod install, xcodebuild build, xcodebuild test를 각각 봅니다. ②가 5분 넘으면 cold·cache부터; ③이 들쭉날쭉하면 DerivedData·동시 실행; ④가 항상 길면 테스트를 분리하거나 nightly 전량으로.
4. 테스트와 서명: 숨은 발목 잡기
「xcodebuild가 느리다」는 피드백을 쪼개 보면, 테스트나 서명이 같은 build에 포함된 경우가 많습니다.
- Simulator cold start: CI에서 첫 기동에 몇 분 더 — 예열 없으면 job마다 반복
- UI Test는 unit보다 한 단계 느림. compile과 한 job에 넣으면 P95가 보기 힘듦
- 인증서, Keychain, Profile 다운로드 — 호스팅 runner에서 job마다 다시 설정하는 경우
- Archive·IPA는 릴리스 파이프라인용. PR 검증 시간과 섞지 말 것
PR: build + 가벼운 테스트, warm P95만.
TestFlight / 릴리스: 별도 workflow·별도 지표. 한곳에 넣으면 「평소 merge 대기」가 영원히 안 맞습니다.
5. 14일 Shadow 실측
듀얼 트랙 대조: macos-latest와 전용 Mac mini M4에 각각 187회 PR, Xcode 16.2·CocoaPods 1.15.2 맞춤. 전체 방법은 기둥 Benchmark 참고.
| 분류 | 샘플 수 | 비율 | macos-latest P95 | 전용 M4 P95 |
|---|---|---|---|---|
| warm build | 162 | 86.6% | 14:12 | 6:05 |
| cold build | 25 | 13.4% | 19:40 | 11:20 |
| 섞어서 계산 (오판하기 쉬움) | 187 | 100% | ~16:00+ | ~7:30+ |
cold와 warm을 섞어 P95를 내면 일상 체감이 대략 15–25% 과대평가되어 「당장 머신 바꿔야 한다」는 결론이 나기 쉽습니다. 더 안전한 방법: merge 체감은 warm, 의존성 올린 주는 cold를 따로 기대.
전용 M4에서도 warm이 cold보다 확실히 빠릅니다 — DerivedData·Pods에 고정된 「집」이 있다는 뜻. cache 설정은 cache 전문에서 자세히.
6. 순서 정하기: 파악부터 cache까지
큐 문제를 제외한 뒤(또는 이미 크지 않다면) 이 순서로 — 먼저 구매 얘기하지 마세요.
- 빌드에 태그: cold vs warm (
Podfile.lock변경 여부, cache hit) - 일상 SLA는 warm P95만; cold는 주간 리포트나 별도 라인
- DerivedData, Pods, SPM cache 전략 확정 — cache 모범 사례 (준비 중)
- macOS job 과밀 금지: self-hosted는 1–2 동시 실행; 호스팅도 workspace 공유 최소화
- PR 검증과 릴리스/signing 분리 — 테스트가 merge 지표를 오염시키지 않게
- cache까지 했는데도 부족하면 M4 / M4 Pro — 칩과 공수
- name: Classify build type
run: |
if git diff --name-only HEAD~1 | grep -q Podfile.lock; then
echo "BUILD_TYPE=cold" >> $GITHUB_ENV
else
echo "BUILD_TYPE=warm" >> $GITHUB_ENV
fi
- name: Record wall clock
run: |
echo "build_type=${BUILD_TYPE}" >> metrics.csv
echo "wall_sec=$(( $(date +%s) - START ))" >> metrics.csv
7. 자주 묻는 질문 (FAQ)
같은 commit 재실행에 2–3배 차이, 정상인가요?
macos-latest에서는 흔합니다. 한 번은 cache full hit, 다음엔 miss+큐가 겹치면 다른 빌드처럼 보여요. 두 번의 pod install·cache hit 로그부터 보고, 칩 교체는 나중에.
P95는 어떻게 계산해야 하나요?
일상 merge 체감은 warm P95로. 2주에 30샘플 정도면 안정적. cold는 별도 집계나 횟수만 — 섞으면 구매 결정이 치우칩니다.
pod install이 매번 느린데 CocoaPods 탓인가요?
호스팅 runner에서는 Pods가 「살 곳」이 없고 cache key에 브랜치·아키텍처가 빠진 경우가 더 많아요. self-hosted 고정 디스크면 warm 때 30초 이내가 흔합니다.
Mac mini M4로 바꾸면 편차가 사라지나요?
대조에서 warm P95가 크게 줄고(−57%), σ도 수렴(−40%)했지만, cold/warm 미분리·cache 미설계면 전용 Mac에서도 스파이크는 남습니다.
느린 게 큐인가요, build인가요?
로그 맨 앞 Waiting for a runner 대기 시간을 보세요. 큐는 벽시계에 포함되지만 GitHub 분 단위 과금은 아닙니다. 큐가 거의 0인데도 느리면 이 글의 빌드 쪽 → cache로.
더 읽을 거리?
cache YAML·key 설계 → cache 전문; 구매 vs 임대 → ROI 모델과 월 500회 빌드 선택.
8. 마무리
GitHub Actions iOS CI가 「느리다」는 말은 순수 CPU 부족인 경우가 드뭅니다. 더 자주 겹치는 건:
- cold와 warm을 섞어 P95가 올라감
- 호스팅 job 종료 시 디스크 초기화로 cache가 안 남음
- 테스트·서명과 PR build가 한 job에 섞임
warm P95를 먼저 제대로 잰 뒤 cache → 동시 실행 → 하드웨어 순으로. 14일 Shadow에서 지표·cache만 맞춰도 warm P95가 14:12에서 6:05까지 — 머신 구매를 먼저 결정할 필요는 없었습니다.
DerivedData에 고정된 「집」이 필요하신가요?
Vuncloud Cloud Mac M4 Pro는 1TB 데이터 디스크·actions-runner 사전 설치. DerivedData / Pods를 디스크에 계속 두면 호스팅 runner식 「매번 cold」가 줄어듭니다.