iOS 빌드 인프라, 어디서 돌릴까 — 6가지 옵션 비교와 self-hosted runner 구축기
React Native 모노레포의 iOS 빌드를 어디서 돌릴지 고민하며 6가지 인프라 옵션을 비교했고, 최종적으로 사내 Mac에 GitHub Actions self-hosted runner를 붙였습니다. 옵션별 비교, 우리가 self-hosted를 고른 이유, 그리고 self-hosted 특유의 함정(코드 서명·키체인)을 어떻게 넘었는지 공유합니다.
들어가며
저희 팀은 여러 개의 React Native 앱을 하나의 Nx 모노레포에서 운영하고 있습니다. 빌드/배포 파이프라인은 GitHub Actions 위에 올라가 있고, Git 태그를 푸시하면 해당 앱이 Stage/Prod로 빌드되는 구조입니다. Android는 Linux runner에서 잘 돌아갑니다. 문제는 항상 iOS였습니다.
iOS 빌드는 macOS에서만 가능합니다. Xcode가 macOS 전용이기 때문에 Linux runner를 아무리 늘려도 IPA 하나 만들 수 없습니다. 그래서 "iOS 빌드를 어디서 돌릴 것인가"는 단순한 CI 설정 문제가 아니라, macOS 컴퓨팅 자원을 어떻게 확보할 것인가라는 인프라 의사결정이 됩니다.
이 글에서는 ① iOS 빌드 인프라 6가지 옵션을 비교하고, ② 저희가 사내 Mac + self-hosted runner를 선택한 이유, ③ 그리고 self-hosted를 실제로 굴리면서 마주친 코드 서명·키체인 문제를 어떻게 해결했는지를 다룹니다.
iOS 빌드는 왜 인프라 문제가 되는가
GitHub Actions에서 Linux runner는 분당 약 $0.006, Windows는 $0.010 수준입니다. 그런데 macOS runner는 분당 $0.06 안팎으로 한 자릿수 배 비쌉니다. macOS는 Apple 하드웨어에서만 합법적으로 실행할 수 있어서, 클라우드 사업자 입장에서도 "Mac 하드웨어를 사서 빌려주는" 구조라 단가가 높을 수밖에 없습니다.
여기에 두 가지 제약이 더 붙습니다.
- 분당 과금 구조: iOS 빌드는 Pod 설치, 네이티브 컴파일, 코드 서명, 아카이브까지 한 번에 15~25분이 쉽게 걸립니다. 빌드 횟수가 늘수록 비용이 선형으로 증가합니다.
- Apple의 라이선스 제약: 일부 클라우드 Mac은 macOS 라이선스 약관 때문에 24시간 최소 대여 같은 제약이 있습니다. "필요할 때 1시간만" 쓰는 게 불가능한 경우가 있습니다.
즉, iOS 빌드 인프라는 "싸고, 빠르고, 관리 안 해도 되는" 세 가지를 동시에 만족하기 어렵습니다. 그래서 팀의 빌드 빈도와 관리 여력에 따라 정답이 달라집니다. 먼저 선택지를 펼쳐 보겠습니다.
아래 비용은 저희가 리서치하던 시점 기준의 대략적인 수치입니다. 가격 정책은 자주 바뀌므로 정확한 금액은 각 서비스 공식 페이지를 확인하세요.
옵션 비교
1. GitHub Actions macOS Runner
GitHub이 호스팅하는 macOS runner. 기존 워크플로우에서 runs-on만 바꾸면 되는 가장 손쉬운 선택지입니다.
| 항목 | 내용 |
| 가격 | 표준(3~4 core) ~$0.062/분, xlarge(M2 Pro) ~$0.10/분 |
| 설정 난이도 | 낮음 (runs-on: macos-15 한 줄) |
| 관리 부담 | 없음 |
| 빌드 제한 | 분당 과금 (private repo는 무료 tier 미포함) |
- 장점: 인프라 관리가 전혀 필요 없고, 기존 GitHub Actions 워크플로우를 그대로 쓸 수 있습니다. 더 빠른 머신이 필요하면 runs-on만 xlarge로 바꾸면 됩니다.
- 단점: 분당 과금이라 빌드가 많아지면 비용이 가파르게 오릅니다. 빌드 캐시 용량 제한(10GB)이 있고, 머신 환경을 커스텀하기 어렵습니다.
- 월 예상: 20분 빌드 × 30회 = 600분 → 표준 ~$37, xlarge ~$61.
2. Codemagic
React Native·Flutter를 공식 지원하는 모바일 전용 CI/CD. 코드 서명과 스토어 업로드를 자동으로 관리해 줍니다.
| 항목 | 내용 |
| 가격(무료) | M2 머신 500분/월, 동시 빌드 1개 |
| 가격(종량제) | M2 ~$0.095/분 |
| 가격(정액) | ~$333/월(무제한) |
| 설정 난이도 | 중간 (codemagic.yaml) |
- 장점: 무료 500분이면 소규모 팀엔 충분합니다. Fastlane 연동, 코드 서명 자동 관리, TestFlight 자동 업로드 등 모바일에 특화돼 있습니다.
- 단점: GitHub Actions와 별도의 CI를 이중 운영하게 됩니다. 기존 워크플로우를 마이그레이션해야 합니다.
3. EAS Build (Expo)
Expo의 빌드 서비스. bare workflow(순수 RN)도 지원합니다.
| 항목 | 내용 |
| 가격(무료) | iOS 15빌드/월 |
| 가격(유료) | Starter $19/월, Production $199/월 |
| 설정 난이도 | 중간 (eas.json + Expo CLI) |
- 장점: RN에 특화돼 빌드 설정이 단순하고, EAS Update(OTA)와 통합됩니다.
- 단점: bare를 지원하지만 결국 Expo 생태계(eas-cli)에 종속됩니다. 무료 15빌드는 금방 소진되고, 빌드 큐 대기가 발생합니다. 기존 Fastlane 설정과 충돌할 수 있습니다.
4. MacStadium
전용 Mac 하드웨어를 월 단위로 대여하는 호스팅 서비스.
| 항목 | 내용 |
| 가격 | M2 8GB ~$109/월 ~ M4 Pro ~$299/월 |
| 설정 난이도 | 높음 (서버 세팅·CI 연동 직접 구성) |
| 빌드 제한 | 무제한 |
- 장점: 전용 하드웨어라 성능이 안정적이고 빌드가 무제한입니다. self-hosted runner로 붙일 수 있습니다.
- 단점: 월 고정 비용이 들고, OS·Xcode 업데이트 같은 머신 관리는 직접 해야 합니다. 빌드가 적으면 비효율적입니다.
5. AWS EC2 Mac
AWS가 제공하는 클라우드 Mac(Dedicated Host).
| 항목 | 내용 |
| 가격 | mac2.metal ~$6.5/hr |
| 최소 대여 | 24시간 (Dedicated Host 제약) |
| 설정 난이도 | 높음 |
- 장점: AWS 인프라(VPC, IAM)와 통합됩니다.
- 단점: 24시간 최소 과금이 치명적입니다. 1시간만 써도 24시간분(약 $156)이 청구됩니다. 인스턴스를 stop해도 Dedicated Host를 release하기 전까지 과금이 계속됩니다.
- 결론: 간헐적인 CI 빌드 용도로는 부적합합니다.
6. Self-hosted Mac (사내 Mac)
사내에 Mac mini를 두고, GitHub Actions self-hosted runner를 설치해 빌드를 받는 방식.
항목 내용
| 항목 | 내용 |
| 가격 | 하드웨어 구매 비용만 (Mac mini M4 ~80만 원대) |
| 설정 난이도 | 중간 (runner 설치 + 서비스 등록) |
| 관리 부담 | 높음 (물리적 관리, OS/Xcode 업데이트) |
| 빌드 제한 | 무제한 |
- 장점: 한 번 사두면 빌드 횟수·시간 추가 비용이 없습니다(전기료 제외). 스펙을 자유롭게 고를 수 있습니다.
- 단점: 전원·네트워크·OS 업데이트 같은 물리적 관리가 필요하고, 장애 시 빌드가 멈춥니다.
비교 요약
| 옵션 | 월 예상 비용 | 설정 난이도 | 관리 부담 | 빌드 제한 |
| GitHub Actions 표준 | ~$37 | 낮음 | 없음 | 분당 과금 |
| GitHub Actions xlarge | ~$61 | 낮음 | 없음 | 분당 과금 |
| Codemagic 무료 | $0 | 중간 | 없음 | 500분/월 |
| Codemagic 종량제 | ~$57 | 중간 | 없음 | 분당 과금 |
| EAS Build 무료 | $0 | 중간 | 없음 | 15빌드/월 |
| MacStadium | $109~ | 높음 | 중간 | 무제한 |
| Self-hosted Mac | $0 (하드웨어 별도) | 중간 | 높음 | 무제한 |
| AWS EC2 Mac | $4,680~ | 높음 | 높음 | 무제한 |
우리의 선택: Self-hosted runner
저희 상황을 정리하면 이랬습니다.
- 앱이 여러 개라 빌드 빈도가 꾸준히 높다. Stage QA 빌드, Prod 심사 빌드가 앱별로 쌓이면 월 수십 회를 쉽게 넘긴다.
- 이미 GitHub Actions를 메인 파이프라인으로 쓰고 있다. 별도 CI(Codemagic/EAS)를 이중 운영하고 싶지 않았다.
- 기존에 Fastlane 기반 코드 서명·TestFlight 업로드가 구성돼 있어, 이걸 그대로 가져가고 싶었다.
- 사무실에 상시 켜둘 수 있는 Mac이 확보돼 있었다.
분당 과금 옵션은 빌드가 늘수록 비용이 우상향합니다. 반면 self-hosted는 빌드를 아무리 많이 돌려도 추가 비용이 0입니다. 빌드 빈도가 높고 GitHub Actions를 이미 쓰는 팀에게는, 사내 Mac을 self-hosted runner로 붙이는 게 비용·일관성 양쪽에서 가장 합리적이었습니다.
핵심은 CI 파이프라인을 갈아엎지 않는다는 점입니다. 워크플로우에서 iOS 빌드 잡의 runs-on만 self-hosted로 바꾸면, 나머지 로직(태그 파싱, Doppler, Fastlane)은 그대로 재사용됩니다.
jobs:
build:
name: iOS (${{ inputs.app_dir }})
runs-on: self-hosted # macos runner → 사내 Mac
steps:
- uses: actions/checkout@v4
# ... 이하 동일
하이브리드 구성 — macOS가 필요한 잡만 self-hosted로
self-hosted라고 해서 모든 잡을 Mac에서 돌릴 필요는 없습니다. 오히려 그러면 안 됩니다. Lint·타입체크·테스트·태그 파싱처럼 macOS가 필요 없는 작업까지 한 대뿐인 Mac에 몰아넣으면,
그게 곧 병목이 됩니다.
그래서 저희는 잡 단위로 runner를 나눴습니다.

| 잡 | runner | 이유 |
| PR 체크(Lint/Typecheck/Test) | ubuntu-latest | macOS 불필요. GitHub 무료/저가 Linux |
| 태그 파싱 | ubuntu-latest | 단순 셸 스크립트 |
| Android 빌드 | ubuntu-latest | JVM 빌드, Linux로 충분 |
| iOS 빌드 | self-hosted | Xcode 필요 → 사내 Mac만 가능 |
Linux로 충분한 작업은 전부 GitHub의 저렴한 Linux runner에 맡기고, macOS가 반드시 필요한 iOS 빌드 잡 하나만 사내 Mac으로 보냅니다. self-hosted Mac의 부하를 최소화하면서, macOS 자원이 꼭 필요한 곳에만 쓰는 구성입니다.
iOS 빌드 잡은 재사용 워크플로우(workflow_call)로 빼두고, Stage/Prod 워크플로우가 파라미터만 바꿔 호출합니다. self-hosted 전환 후에도 호출부는 그대로입니다.
# stage.yml — 태그를 파싱해 iOS/Android 빌드를 분기 호출
build-ios:
needs: parse-tag
if: needs.parse-tag.outputs.build_ios == 'true'
uses: ./.github/workflows/_build-ios.yml # 재사용 워크플로우
with:
app_dir: ${{ needs.parse-tag.outputs.app_dir }}
lane: beta
version: ${{ needs.parse-tag.outputs.version }}
self-hosted의 진짜 난관 — "환경이 영속적이다"
호스팅 runner와 self-hosted의 본질적 차이는 상태의 영속성입니다.
GitHub 호스팅 macOS runner는 매 빌드마다 깨끗한 새 가상머신에서 시작합니다. Xcode도, 키체인도, Ruby도 매번 초기 상태입니다. 반면 사내 Mac은 항상 같은 머신입니다. 지난 빌드의 흔적이 남아 있고, 로그인 키체인은 잠겨 있고, Ruby는 시스템 버전이 아니라 우리가 깔아둔 버전을 써야 합니다.
이 차이에서 self-hosted 특유의 문제들이 나옵니다. 하나씩 보겠습니다.
1. 코드 서명과 키체인 잠금 — 가장 큰 함정
self-hosted iOS 빌드에서 십중팔구 처음 막히는 지점입니다.
iOS 아카이브에는 배포 인증서로 코드 서명이 필요하고, 그 인증서의 개인 키는 macOS 키체인에 들어 있습니다. 그런데 GitHub Actions runner는 서비스(데몬)로 백그라운드 실행되기 때문에, 로그인 세션의 키체인이 잠긴 상태입니다. 이대로 codesign을 호출하면 인증서 키에 접근하지 못해 빌드가 실패합니다. 호스팅 runner에서는 매번 임시 키체인을 새로 만들어 쓰기 때문에 겪지 않는 문제입니다.
해결은 빌드 직전에 키체인을 잠금 해제하고, 코드 서명 도구가 프롬프트 없이 키에 접근하도록 권한을 여는 것입니다.
- name: Unlock keychain
run: |
KEYCHAIN_PW=$(doppler secrets get KEYCHAIN_PASSWORD --plain ...)
# 로그인 키체인 잠금 해제
security unlock-keychain -p "$KEYCHAIN_PW" \
"$HOME/Library/Keychains/login.keychain-db"
# codesign이 비대화식으로 키에 접근하도록 partition list 설정
security set-key-partition-list -S apple-tool:,apple: -s \
-k "$KEYCHAIN_PW" "$HOME/Library/Keychains/login.keychain-db"
set-key-partition-list가 핵심입니다. 이게 없으면 빌드 중 "키체인 접근을 허용하시겠습니까?" 같은 GUI 프롬프트가 떠서 CI가 무한 대기에 빠집니다. 사람이 없는 빌드 머신에서는 절대 응답되지 않습니다.
Fastlane 레인 안에서도 한 번 더 명시적으로 잠금을 풉니다.
lane :beta do
api_key = app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"], issuer_id: ENV["ASC_ISSUER_ID"],
key_filepath: ENV["ASC_KEY_FILE"]
)
clear_derived_data # ← 영속 환경 정리(아래 참고)
get_provisioning_profile(api_key: api_key)
update_code_signing_settings( # 자동 서명 끄고 수동 서명 고정
use_automatic_signing: false,
code_sign_identity: "iPhone Distribution",
profile_name: lane_context[SharedValues::SIGH_NAME]
)
unlock_keychain( # 빌드 직전 재확인
path: File.expand_path("~/Library/Keychains/login.keychain-db"),
password: ENV["KEYCHAIN_PASSWORD"]
)
build_app(workspace: "...", scheme: "...", export_method: "app-store")
upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: true)
end
인증서·프로비저닝은 App Store Connect API 키로 처리합니다. 키 파일은 GitHub Secrets에 base64로 넣어두고 빌드 시 디코드해 사용하므로, 머신에 자격증명을 영구 보관하지 않습니다.
2. 환경 격리 — 지난 빌드의 잔재 지우기
같은 머신을 재사용하니 이전 빌드의 DerivedData, Pods 캐시가 남아 유령 같은 빌드 실패를 만들 수 있습니다. 그래서 빌드 시작 시 clear_derived_data로 파생 데이터를 비우고, Pods는 명시적 캐시 키로만 관리합니다.
- name: Restore Pods cache
uses: actions/cache@v4
with:
path: apps/${{ inputs.app_dir }}/ios/Pods
key: pods-${{ runner.os }}-${{ hashFiles('.../Podfile.lock') }}
restore-keys: pods-${{ runner.os }}-
Podfile.lock 해시를 키로 쓰기 때문에, 의존성이 바뀌지 않았으면 캐시를 재사용해 pod install을 건너뛰고, 바뀌면 자동으로 새로 받습니다. 영속 머신에서도 캐시가 결정적으로 동작합니다.
3. 런타임 PATH — 시스템 Ruby를 쓰지 않기
Fastlane/CocoaPods는 Ruby 위에서 돕니다. macOS 시스템 Ruby를 직접 쓰면 권한·버전 문제가 잦아, 저희는 rbenv로 깐 Ruby를 씁니다. 그런데 runner를 서비스로 띄우면 로그인 셸의 PATH 설정(.zshrc 등)을 타지 않아, rbenv가 PATH에 없습니다. 그래서 잡 안에서 직접 PATH를 넣어 줍니다.
- name: Setup rbenv PATH
run: |
echo "$HOME/.rbenv/shims" >> $GITHUB_PATH
echo "$HOME/.rbenv/bin" >> $GITHUB_PATH
호스팅 runner였다면 신경 쓸 일이 없지만, 영속 머신에서는 "내 셸에선 되는데 CI에선 안 되는" 전형적인 PATH 함정입니다.
4. 시크릿 관리 — 머신에 값을 박아두지 않기
self-hosted라고 환경변수를 머신 .env에 박아두면, 그게 곧 관리 사각지대가 됩니다. 저희는 Doppler로 시크릿을 중앙 관리하고, 빌드 시점에 필요한 값만 내려받아 .env를 생성합니다. Firebase 설정(GoogleService-Info.plist), Sentry 토큰, 카카오 키 등 민감 파일도 전부 Doppler에서 주입합니다.
- name: Pull env from Doppler
run: |
doppler secrets download --no-file --format=env \
--project ${{ inputs.doppler_project }} \
--config ${{ inputs.doppler_config }} \
--token "$DOPPLER_TOKEN" > .env
이렇게 하면 머신이 바뀌거나 runner를 새로 깔아도, 자격증명은 머신이 아니라 Doppler에 산다는 원칙이 유지됩니다.
runner 등록과 자동 복구
runner 설치 자체는 어렵지 않습니다.
mkdir actions-runner && cd actions-runner
curl -o runner.tar.gz -L https://github.com/actions/runner/releases/download/vX.Y.Z/actions-runner-osx-arm64-X.Y.Z.tar.gz
tar xzf runner.tar.gz
./config.sh --url https://github.com/ORG/REPO --token
# launchd 서비스로 등록 → 재부팅 시 자동 시작
./svc.sh install
./svc.sh start
svc.sh install로 launchd 서비스 등록을 해두는 게 중요합니다. 사내 Mac은 정전·재부팅을 피할 수 없는데, 서비스로 등록해두면 부팅 후 runner가 자동으로 다시 붙습니다. 추가로 macOS 시스템 설정 → 에너지 → "정전 후 자동으로 시작" 을 켜두면, 전원만 복구되면 사람 개입 없이 빌드 머신이 살아납니다. 원격 관리는 SSH와 화면 공유(VNC)로 하고, 고정 IP 또는 VPN을 함께 둡니다.
운영하며 느낀 트레이드오프
self-hosted는 만능이 아닙니다. 비용을 관리 부담과 맞바꾸는 선택입니다.
좋았던 점
- 빌드 횟수가 늘어도 비용이 그대로다. 분당 과금의 심리적 압박이 사라졌다.
- 기존 GitHub Actions·Fastlane 자산을 100% 재사용했다. runs-on만 바꿨다.
- 머신을 우리가 통제하니 Xcode 버전, 캐시, 도구 체인을 원하는 대로 고정할 수 있다.
감수해야 하는 점
- 단일 장애점(SPOF): Mac 한 대가 멈추면 iOS 빌드가 전부 멈춘다. 빌드가 잦은 팀이라면 예비 머신이나 호스팅 runner로의 폴백 전략을 고려해야 한다.
- 물리·OS 관리: Xcode 업데이트, 디스크 정리, 인증서 갱신을 누군가는 챙겨야 한다.
- 동시성: 머신이 한 대면 빌드는 직렬화된다. 여러 앱을 동시에 빌드하려면 머신을 늘리거나 호스팅 runner를 섞어야 한다.
그래서 저희는 하이브리드가 정답이라고 봤습니다. Linux로 되는 일은 GitHub Linux runner에, macOS가 꼭 필요한 iOS 빌드만 사내 Mac에. 필요하면 언제든 runs-on을 macos-15로 되돌려 호스팅 runner로 폴백할 수 있다는 점이, self-hosted를 마음 편히 운영하는 안전판이 됩니다.
마치며
iOS 빌드 인프라에는 "무조건 이게 정답"인 옵션이 없습니다. 빌드 빈도가 낮으면 Codemagic·EAS 무료 tier나 GitHub 호스팅 runner가 관리 부담 없이 가장 편하고, 빌드가 잦고 이미 GitHub Actions를 쓰고 있다면 사내 Mac + self-hosted runner가 비용·일관성에서 유리합니다. 반대로 AWS EC2 Mac처럼 과금 구조가 CI에 맞지 않는 옵션은 후보에서 빠르게 지웠습니다.
self-hosted를 고른다면, 진짜 난이도는 runner 설치가 아니라 "환경이 영속적"이라는 데서 오는 코드 서명·키체인·PATH 문제에 있습니다. 이 글이 그 함정을 미리 피하는 데 도움이 되길 바랍니다.
'Architecture > CI-CD' 카테고리의 다른 글
| 웹 모노레포 CI/CD 개편기 — 라벨 배포에서 태그 배포로 (0) | 2026.04.17 |
|---|---|
| React Native 모노레포에서 앱 빌드 및 배포 전략 설계하기 (0) | 2026.04.17 |
| Github Actions와 Docker+AWS EC2를 활용한 CI/CD (0) | 2022.11.30 |