VPSSpark 블로그
← 개발 일기로

Phone 18 출시 전, Xcode 27·iOS 27·GitHub Actions·Apple Silicon Runner로 다시 돌린 iOS CI/CD 함정 기록

서버 메모 · 2026.09.11 · 약 12분

자주 찾는 키워드: Xcode 27 · iOS 27 · xcode-27 · Apple Silicon · Phone 18 CI

모니터와 노트북 앞에서 디버깅 중인 개발자 작업 공간
마케팅 기기명보다 Runner 이미지와 사용 가능한 시뮬레이터부터 고정한다.

Phone 18 발표회 일주일 전, 회의실에서는 카메라 초점거리 얘기가 없었다——모두가 Actions 목록의 빨간불을 보고 있었다.Xcode 27, iOS 27 SDK, GitHub 호스티드의 xcode-27 라벨, 그리고 막 붙인 Apple Silicon 셀프호스티드 Runner: 네 줄이 같은 주에 겹치면, 하나만 흔들려도 App Store 스크린샷과 TestFlight 베타가 한꺼번에 멈춘다. 우리는 iOS CI/CD 전체를 「초록이면 된다」에서 「출시 주를 버틸 수 있다」까지 다시 돌렸고, 실제로 밟은 함정을 여기에 남긴다.

시간 앵커도 단단하다. 2026년 9월 10일, GitHub는 Changelog에 Xcode 27 runner 이미지가 macOS 27에서 실행된다고 명시했다. 라벨은 여전히 xcode-27 / xcode-27-xlarge이고, arm64만 제공한다. 전날까지 macOS 26 기반에서 「우연히 초록」이던 job이, 이미지가 바뀌는 순간 OS 버전·시뮬레이터 기기명·디스크 잔량 때문에 통째로 무너질 수 있다. Golden Gate 정식판 창과 지원 기종은 먼저 사이트 내 macOS 27 Golden Gate 출시일과 지원 기종을 참고하고, 이 글은 CI 쪽에서 어떻게 밟고 어떻게 수습하는지만 다룬다.

9.10
GitHub가 xcode-27을 macOS 27로 전환 발표
arm64
호스티드 xcode-27은 Apple Silicon만
4 함정
라벨 · 시뮬레이터 · 캐시 · 디스크/대기열

왜 출시 전에 「파이프라인 전체를 다시 돌려야」 하는가——matrix 한 줄 변경으로는 부족하다

많은 팀의 근육 기억은 이렇다: xcode-version을 26에서 27로 바꾸고, destination을 iPhone 18로 교체한다. Phone 18 창 직전에 이렇게 하면 거의 반드시 터진다. 이유는 세 겹이다. 첫째, SDK와 Runner 이미지는 동기 출하가 아니다——로컬에 Xcode 27 GM이 있어도 호스티드 이미지는 아직 public preview이거나, macOS 26 기반에서 27로 막 바뀌었을 수 있다. 둘째, 시뮬레이터 기기 표가 바뀐다: iOS 27 runtime에 하드코딩한 기종명이 곧바로 나온다는 보장이 없고, name=iPhone 18을 쓰면 바로 Unable to find a device matching the provided destination이 난다. 셋째, DerivedData·SwiftPM·CocoaPods의 cache key가 여전히 「Xcode 메이저 이전」 경로에 묶여 있으면, 어제 빌드 쓰레기를 오늘 컴파일러에 먹이는 셈이다.

우리의 방식은 「빌드된다」와 「출시 주를 버틴다」를 나눠 검수한다. 전자는 단일 PR job이 초록인지; 후자는 동시성·디스크·인증서·Simulator 콜드 스타트가 같은 피크에서 서로 발을 밟지 않는지다. 호스티드 쪽은 먼저 GitHub-hosted runners 안내를 읽고, 셀프호스티드 쪽은 라벨로 PR 빠른 피드백과 Archive 무거운 작업을 풀을 나눈다——예전에 쓴 GitHub Actions가 iOS / Xcode CI를 느리게 하는 새 해법과 같은 의사결정 로직이고, 이번 트리거만 Phone 18로 바뀌었다.

Phone 18 전 iOS CI 네 가지 함정: 라벨, 시뮬레이터, 캐시, 디스크와 대기열
네 줄은 이 순서로 잡는다: 먼저 runs-on과 이미지를 고정하고, 다음으로 시뮬레이터를 동적 선택, 그다음 옛 캐시를 무효화한 뒤, 마지막으로 셀프호스티드 풀과 디스크 잔량을 이야기한다.

함정 1: 아직 macos-latest를 쓰거나, xcode-27 라벨이 「아무 Mac이나」라고 착각한다

macos-latest는 2026년 가을에도 「기본으로 돌아갈」 용도로는 괜찮지만, Phone 18 출시 주의 유일한 검수면으로는 부적합하다. 그것은 GitHub의 현재 기본 이미지 정책을 가리킬 뿐, 「iOS 27 SDK + 필요한 Simulator가 갖춰졌다」와 같지 않다. Xcode 27을 고정하려면 워크플로에 명시적으로 runs-on: xcode-27(무거운 작업은 xcode-27-xlarge)를 써야 하며, maxim-lobanov/setup-xcode가 베이스가 맞지 않는 이미지에서 버전을 억지로 바꾸게 맡기면 안 된다——runtime이 빠진 바닥에서는 Xcode.app을 골라도 simctl을 살리지 못한다.

더 은밀한 점: Changelog는 해당 이미지가 arm64만이라고 강조한다. 조직에 Intel 셀프호스티드 노드가 남아 있거나, runs-on: [self-hosted, macOS]를 「모든 Mac이 Xcode 27 job을 받을 수 있다」로 쓰면, matrix를 넓히는 순간 「가끔 초록·가끔 빨강」이 생긴다——초록은 Apple Silicon에 걸린 경우, 빨강은 Intel 노드가 해당 toolchain을 설치·실행하지 못하는 경우다. 라벨은 실제로 준비한 집합만 쓴다. 예: [self-hosted, macOS, ARM64, xcode27]. 미업그레이드 노드는 그 라벨에서 뺀다.

라벨 드리프트가 코드 드리프트보다 위험하다
출시 주에 가장 두려운 것은 일회성 compile error가 아니라, Runner 라벨 의미가 바뀌었는데 문서가 안 바뀐 것이다. 누가 xcode-27을 받고, 누가 macos-26 호환 라인을 받는지 Runbook에 적고, Actions Runner 페이지에서 주간으로 맞춰 본다.

함정 2: destination에 iPhone 18을 박아 넣었는데, 이미지에 그 「전화」가 아직 없다

마케팅 문구의 Phone 18과 Simulator 목록의 기기명은 다른 표다. GitHub actions/runner-images 설명에서 iOS 27.0 runtime에 현재 나열된 것은 iPhone 17 시리즈, iPhone Air, 여러 iPad 등——발표회 당일 iPhone 18이라는 이름의 시뮬레이터가 반드시 나온다는 보장은 없다. YAML을 -destination 'platform=iOS Simulator,name=iPhone 18,OS=27.0'으로 쓰는 것은 출시 전 가장 흔한 자해다.

재현 가능한 방법: job 시작에서 xcrun simctl list devices available -j JSON을 파싱하고, 「iOS 27 runtime + 이름이 iPhone으로 시작」으로 정렬해 한 대를 고른 뒤 UDID를 $GITHUB_OUTPUT에 쓰고, 이후 xcodebuild test는 id=…만 쓴다. 다기종 matrix가 필요하면 「가용 목록 ∩ 팀 화이트리스트」 교집합을 쓰고, 없으면 skip 후 알림을 보내 Release 전체를 빨갛게 만들지 않는다. UI 테스트는 한 가지 현실도 받아들여야 한다: 새 OS 첫 콜드 스타트는 Simulator가 몇 분을 더 먹을 수 있다. timeout을 Xcode 26 시절 20분으로 자르면 우발 실패가 필연이 된다.

작성법출시 주 위험더 안정적인 대안
name=iPhone 18,OS=27.0기기명 미등록 시 즉시 실패UDID로 동적 선택
OS=27.0 마이너까지 고정이미지가 27.0.1일 수 있음runtime 접두사 매칭 또는 최신 가용
macos-latest만 실행SDK/시뮬레이터 불확실명시적 xcode-27 검수면
Intel + Apple Silicon 혼용 라벨동일 job 우발 아키텍처 실패풀 분리, 27 라인은 ARM64만

함정 3: 캐시 key를 Xcode 27에 맞춰 무효화하지 않았다——초록인 것은 「옛 세계」다

메이저 업그레이드에서 「고친 것 같은」 착각을 가장 잘 만드는 것이 캐시다. SwiftPM의 SourcePackages, CocoaPods의 Pods, 자체 DerivedData tarball에서 key에 branch명이나 lockfile hash만 있고 $(xcodebuild -version)/이미지 버전이 없으면, Actions는 기쁘게 Xcode 26 시대 산출물을 restore한다. 증상은 들뜬다: 링크 시 심볼을 못 찾거나, Swift driver가 바로 죽거나, 테스트는 빌드되지만 모듈 캐시 검증이 실패한다.

우리는 cache key에 세 구간을 강제한다: Xcode 짧은 버전, runner image version(호스티드는 환경 또는 문서로 고정), 그리고 Package.resolved / Podfile.lock. 전환 당일 옛 key 접두사를 통째로 바꾸고, 콜드 스타트가 8–12분 늘더라도 출시 주에 「같은 commit이 로컬은 통과·CI는 실패」를 디버깅하지 않는다. 셀프호스티드 Apple Silicon 노드는 더 조심해야 한다: 디스크 DerivedData를 작업 간에 공유하면서 Xcode 버전별 디렉터리를 나누지 않으면, 프리뷰와 GM 중간 파일이 섞인다.

검수 기준
「캐시 적중률 상승」은 업그레이드 성공 지표가 아니다. 「콜드 캐시에서도 안정적으로 초록」이 지표다. 출시 전에 최소 한 번 강제 miss nightly를 남겨 둔다.

함정 4: Apple Silicon Runner는 생겼는데, 디스크와 대기열이 원점으로 되돌린다

Xcode 27 + iOS 27 runtime + 여러 Simulator는 디스크 킬러다. 호스티드 xcode-27 이미지 자체가 이미 무겁다. 셀프호스티드 Mac mini에 Xcode 26, 두 개의 beta, 세 App의 DerivedData가 남아 있으면 Archive 후반에야 No space left on device가 터진다. 출시 주 전 각 노드에 하드 룰을 걸었다: 「현재 검수」 Xcode만 유지, Simulator runtime 화이트리스트화, DerivedData는 job 단위 정리, 매일 밤 cron으로 df -h와 xcrun simctl delete unavailable을 보고.

대기열 문제는 Silicon으로 바꿨다고 사라지지 않는다. 전사적으로 발표회 전에 matrix를 「한 세대 SDK」에서 「26 호환 + 27 검수」로 넓히면, 호스티드 동시성과 셀프호스티드 슬롯이 동시에 찬다. PR 빠른 피드백과 TestFlight Archive는 라벨을 나눠야 한다: 전자는 분 단위, 후자는 대기를 허용하되 전자를 막으면 안 된다. 피크에 슬롯을 가져가는 것은 라벨 설계이지, 머신 포스터의 M 시리즈 모델명이 아니다.

공식 툴체인 동작은 Apple Xcode 문서가 기준이다. 이미지에 어떤 Simulator가 들어 있는지는 그주 runner-images 저장소 Readme가 기준이다——작년 블로그 스크린샷을 설정 소스로 쓰지 말 것.

출시 주 Runbook: 우리가 실제로 전환한 방식

하루째, 프로덕션 PR 라인의 검수 job을 명시적으로 xcode-27로 옮기고, 호환 센티널로 macos-26(또는 아직 지원하는 전 세대) 한 줄을 남긴다. 실패 정책은 「27이 빨강이면 머지 차단, 26이 빨강이면 알림만」. 이틀째, 모든 test destination을 동적 UDID로 바꾸고 YAML에서 Phone 마케팅명을 삭제. 사흘째, 캐시 접두사를 무효화하고 콜드 빌드를 한 번 강제 실행해 wall time을 출시 주 베이스라인으로 기록. 나흘째, 셀프호스티드 풀을 라벨로 분리: ci-pr와 ci-release. Intel 노드가 잘못 27 라벨을 붙이지 않았는지 확인.

온콜 기간 매일 세 가지: Changelog / runner-images가 또 이미지를 밀었는지, Runner 디스크 잔량, queued 대 run 비율이 「노란 바가 초록 바보다 긴」 상태로 돌아갔는지. 숫자가 나빠지면 먼저 matrix 병렬도를 줄인 뒤 머신 추가를 이야기한다——라벨을 나누지 않고 머신만 늘리면, 잘못된 job만 더 빨리 돈다.

클라우드 Mac / 전용 Silicon Runner는 언제 올릴까
호스티드 xcode-27 대기열이 이미 빌드보다 길거나, 특정 GM 이미지를 고정해 공증과 업로드가 필요할 때, 일 단위로 Apple Silicon 클라우드 Mac을 셀프호스티드하는 편이 공유 큐에서 운을 거는 것보다 싸 경우가 많다. 전용 노드는 Release와 인증서 체인에 맞고; 대량 PR 동시성의 첫 관문은 호스티드 프리뷰 이미지에 남겨도 된다.

FAQ

지금 당장 모든 job을 xcode-27로 옮겨야 하나?

아니다. 출시 검수와 스토어 빌드는 27에 고정; 구 OS 호환과 핫픽스는 당분간 전 세대 이미지에 남겨도 된다. 핵심은 라벨과 브랜치 보호 규칙을 명확히 해 「우연히 초록이 된 한 대로 머지」를 막는 것이다.

로컬 Xcode 27은 통과하는데, 왜 CI는 아직 빨강인가?

우선 세 가지: Runner가 정말 arm64 xcode-27 이미지인지, 시뮬레이터 기기명이 존재하는지, 캐시가 옛 DerivedData를 hit했는지. 로컬에 풀 runtime이 있어도 호스티드 이미지에 있다는 보장은 없다.

셀프호스티드와 GitHub 호스티드를 같은 workflow에 섞을 수 있나?

가능하다. 다만 다른 job과 다른 라벨을 쓴다. 같은 runs-on 배열이 호스티드와 임의의 self-hosted에 동시에 매칭되면 스케줄 결과가 재현되지 않는다.

Phone 18 실기기 테스트가 Simulator CI를 대체할 수 있나?

대체할 수 없다. 실기기는 카메라·셀룰러·성능 체감; CI는 매 PR의 빌드·단위 테스트·회귀. 출시 주에는 둘 다 필요하고, 실기기만 머지 게이트의 유일한 조건으로 두면 안 된다.

Phone 18 창 · Xcode 27 온콜

공유 큐에 묶이지 않는 Apple Silicon이 필요하신가요?

Release와 공증을 전용 클라우드 Mac에 고정하고, PR은 호스티드 xcode-27로 남겨 두세요. 일 단위로 이미지와 인증서 체인을 검증하는 편이 발표회 당일 동시성을 다투는 것보다 여유롭습니다.

클라우드 Mac 방안 보기 →

한정

공유 큐에 묶이지 않는 Apple Silicon이 필요하세요?

Xcode 27 · iOS 27 · 전용 클라우드 Mac · 일/월

홈
한정 혜택 요금제 보기