React Native CLI 프로젝트를 개발하다 보면 bundle install, bundle exec pod install, bundle exec fastlane ios beta 같은 명령을 매일 마주친다. JS 개발자에게는 낯선 Ruby 생태계지만, 실은 JS의 pnpm/npm 세계와 1:1로 대응되는 구조다. 매핑이 머리에 들어오면 어렵지 않다.
이 문서는 같은 RN CLI 프로젝트를 실무에서 빌드·배포하는 데 꼭 알아야 하는 만큼만 정리한다.
| Ruby (Bundler) | JS (pnpm/npm) | 역할 |
|---|---|---|
| gem | npm 패키지 | 재사용 라이브러리 단위 |
| Gemfile | package.json | 의존성 선언 파일 |
| Gemfile.lock | pnpm-lock.yaml | 정확한 버전 잠금 파일 |
| Bundler | pnpm / npm | 의존성 관리 도구 |
bundle install | pnpm install | 의존성 설치 |
bundle exec <cmd> | pnpm exec <cmd> / npx | 프로젝트가 명시한 버전으로 실행 |
| RubyGems | npm registry | 패키지 저장소 (https://rubygems.org) |
| rbenv / asdf | nvm / volta / fnm | 언어 런타임 버전 관리자 |
.ruby-version | .nvmrc | 프로젝트의 런타임 버전 핀 |
이 표 하나가 사실상 전부다. "Ruby 진영의 의존성 관리 도구가 Bundler이고, 그 설정 파일이 Gemfile이다" — 끝.
답은 단순하다. RN의 빌드·배포 체인이 Ruby로 만들어진 두 도구 위에 서 있기 때문이다.
RN 프로젝트
├── package.json → JS 의존성 (pnpm이 관리)
├── ios/
│ ├── Podfile → iOS 네이티브 의존성 (CocoaPods가 관리) ← Ruby
│ └── fastlane/
│ └── Fastfile → iOS 배포 자동화 (Fastlane이 관리) ← Ruby
├── android/
│ └── fastlane/
│ └── Fastfile → Android 배포 자동화 (Fastlane이 관리) ← Ruby
└── Gemfile → 위 Ruby 도구들의 버전 (Bundler가 관리)
RN 개발자가 실무에서 만나는 Ruby 도구는 두 가지다.
.framework, RN 네이티브 모듈 등)를 설치/링크하는 도구gem 'cocoapods'가 들어 있다iOS/Android의 빌드, 코드 서명, 스토어 업로드를 스크립트화하는 CLI 이며 Ruby로 작성된다.
즉 CocoaPods도 Fastlane도 Ruby로 만들어진 도구라서, RN 개발자도 Ruby 생태계를 어쩔 수 없이 한 번은 마주치게 된다. 다만 Ruby로 코드를 직접 짤 일은 거의 없다 — Fastfile을 손볼 때만 약간 필요할 뿐, 평소엔 명령어만 알면 된다.
do ... end)이 자연어처럼 읽힌다. 그래서 Fastfile, Podfile처럼 "설정 파일처럼 보이는 스크립트"를 만들기 좋다# Ruby — Fastfile에서 흔히 보는 모양
lane :beta do
match(type: "appstore")
build_app(scheme: "my_app")
end
들여쓰기와 do ... end, 문자열 보간 "#{var}"만 익숙해지면 JS 개발자도 큰 어려움 없이 읽는다.
Bundler·CocoaPods·Fastlane 모두 Ruby 위에서 동작하므로 Ruby가 먼저 깔려 있어야 한다. 그런데 macOS에는 Ruby가 들어오는 경로가 최소 세 가지라, 어느 게 잡히는지 의식하지 않으면 사람마다 결과가 달라진다.
| 경로 | 위치 | 특징 |
|---|---|---|
| macOS 시스템 Ruby | /usr/bin/ruby | 보통 2.6.10. 권한 문제(gem install 시 sudo), Apple도 점차 분리 중. 쓰지 말 것 |
| Homebrew Ruby | /opt/homebrew/opt/ruby/bin/ruby (Apple Silicon) | brew install ruby로 설치. 항상 최신 한 버전만. 빠르고 간단하지만 버전 고정 불가 |
| rbenv / asdf | ~/.rbenv/versions/X.Y.Z/... | 프로젝트별 버전 핀 가능. 팀 표준화하기 좋음 |
which ruby 한 번 쳐보면 자기 머신이 어느 경로를 쓰는지 즉시 알 수 있다.
.ruby-version 파일프로젝트 루트의 .ruby-version은 이 프로젝트에 어떤 Ruby 버전을 써야 하는지를 명시한다. rbenv·asdf가 이 파일을 자동으로 읽어 해당 버전을 활성화한다. JS의 .nvmrc와 동일한 역할이다.
# .ruby-version 예시
3.1.4
bundle execgem = Ruby의 패키지. fastlane, cocoapods, activesupport 모두 gem이다. npm 패키지와 정확히 같은 개념이다.
Gemfile은 Bundler가 읽는 의존성 선언 파일. 루트 Gemfile 일부:
source 'https://rubygems.org' # 어디서 gem을 받을지
ruby ">= 2.6.10" # 호환 Ruby 버전
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem 'xcodeproj', '< 1.26.0'
# Ruby 3.4부터 표준 라이브러리에서 빠진 것들 명시
gem 'bigdecimal'
gem 'logger'
gem 'benchmark'
gem 'mutex_m'
JS로 옮기면 이런 느낌이다.
{
"dependencies": {
"cocoapods": ">=1.13, !=1.15.1"
}
}
다른 점은 두 가지.
gem 'cocoapods', '>= 1.13'은 그냥 Ruby 함수 호출이다.!= 1.15.0 같은 식으로 특정 버전 제외를 표현할 수 있다 — 빌드 실패를 일으키는 알려진 나쁜 버전을 피하는 장치다.bundle install이 자동 생성하는 잠금 파일. pnpm-lock.yaml과 같은 역할이다.
GEM
remote: https://rubygems.org/
specs:
cocoapods (1.15.2)
activesupport (7.2.3)
RUBY VERSION
ruby 3.1.4p223
BUNDLED WITH
2.6.7
pnpm-lock.yaml을 커밋해야 하는 것과 똑같이, 이게 있어야 팀원 전원과 CI가 같은 버전을 쓴다BUNDLED WITH는 lock을 만든 Bundler 자신의 버전. RUBY VERSION은 해석된 Ruby 버전Bundler는 위 두 파일을 읽어서 의존성을 해결하고 설치하는 도구. Ruby 1.9 시절부터 사실상 표준이며 최신 Ruby에 기본 포함되어 있다(없다면 gem install bundler).
자주 쓰는 명령:
bundle install # Gemfile.lock 기준으로 설치 (pnpm install)
bundle update # 범위 내에서 최신으로 lock 갱신 (pnpm update)
bundle update cocoapods # 특정 gem만 갱신
bundle outdated # 더 새 버전이 있는지 확인
bundle exec <cmd> # Gemfile 환경으로 명령 실행 (핵심)
bundle exec가 왜 필수인가이게 RN 개발자가 가장 헷갈리는 부분이다. 결론부터:
bundle exec <cmd>는 이 프로젝트의 Gemfile에 박힌 정확한 버전의<cmd>를 실행하라는 뜻이다.
문제 상황:
# 시스템에 cocoapods 1.16.0이 전역 설치돼 있다고 하자
$ gem install cocoapods # → 1.16.0 깔림
# 그런데 프로젝트 Gemfile.lock에는 cocoapods 1.15.2가 잠겨 있다
$ pod install # 어느 버전이 실행될까?
# → 시스템 PATH상의 1.16.0이 실행됨. 잠긴 버전과 다름!
# → 같은 명령이 사람마다 다른 결과를 낼 위험
bundle exec는 이 충돌을 막는다.
$ bundle exec pod install # → Gemfile.lock의 1.15.2가 실행됨
내부적으로 Bundler가 LOAD_PATH(npm의 node_modules처럼 라이브러리를 찾는 경로)를 Gemfile에 적힌 gem들로 한정해서 명령을 실행한다. pnpm exec/npx가 로컬 node_modules/.bin을 우선 쓰는 것과 정확히 같은 원리다.
규칙: RN 프로젝트의 pod install, fastlane ...은 항상 bundle exec를 앞에 붙여 실행한다.
Fastlane은 iOS/Android 앱의 빌드, 코드 서명, 스크린샷, 스토어 업로드, 베타 배포를 자동화하는 오픈소스 CLI다. 2014년 Felix Krause가 개인 프로젝트로 시작해 Fabric을 거쳐 Google에 인수되었고, 현재는 커뮤니티 주도로 유지된다.
수동 배포에는 보통 이런 단계가 들어간다.
iOS
1. Xcode 열기 → scheme · configuration 선택
2. provisioning profile · 인증서 동기화
3. Info.plist의 버전·빌드 번호·display name 갱신
4. Archive → IPA export
5. App Store Connect 업로드 → TestFlight 처리
Android
1. versionName / versionCode 수정
2. 키스토어로 서명
3. ./gradlew bundleRelease로 AAB 생성
4. Play Console 업로드 → 트랙 지정
매번 사람이 하면 (a) 실수가 잦고 (b) 누가 했는지 추적이 어려우며 (c) CI에서 재현이 안 된다. Fastlane은 이걸 "코드 한 파일" 로 만든다.
역사적인 이유다.
Podfile, Vagrantfile이 모두 Ruby DSL인 것과 같은 이유다지금도 Ruby인 이유는 이미 사용자/플러그인/CI 환경이 굳어 있어 바꿀 이유가 없기 때문이다.
my_app/
├── Gemfile # 루트: cocoapods 중심 (iOS pod install용)
├── Gemfile.lock # 루트 잠금
├── ios/
│ └── fastlane/
│ ├── Fastfile # iOS lane 정의 (dev/beta/pro/...)
│ ├── Appfile # bundle id, Apple ID 등 메타
│ └── Matchfile # match(인증서 동기화) 설정
└── android/
├── fastlane/
│ ├── Fastfile # Android lane 정의
│ ├── Appfile # 패키지명, json key 경로
│ └── Pluginfile # 플러그인: versioning_android
├── Gemfile # Android 전용: fastlane + plugin
└── Gemfile.lock # Android 전용 잠금
pod install을 안정적으로 돌리기 위한 것android/Gemfile은 Fastlane 중심. 안드로이드 빌드 자동화에 필요한 gem만 가짐cd ios && bundle exec ... 또는 cd android && bundle exec ... 식으로 디렉터리를 맞춰 실행하는 것이 안전하다# 1) Ruby 버전 확인 (Gemfile.lock의 RUBY VERSION 기준)
ruby -v
# 2) Bundler 설치 (없다면)
gem install bundler
# 3) gem 설치 — 루트와 android 양쪽 필요
bundle install
(cd android && bundle install)
# 4) JS 의존성
pnpm install
# 5) iOS 네이티브 의존성
(cd ios && bundle exec pod install)
# iOS 네이티브 라이브러리 추가/업데이트 후
cd ios && bundle exec pod install
# iOS 베타 배포
cd ios && bundle exec fastlane beta
# Android 베타 배포
cd android && bundle exec fastlane beta
bundle install # Gemfile.lock 갱신
git add Gemfile Gemfile.lock
| 증상 | 원인 | 해결 |
|---|---|---|
Could not find gem '...' in locked gems | Gemfile.lock과 설치된 gem 불일치 | bundle install |
Your Ruby version is X, but your Gemfile specified Y | 시스템 Ruby가 Gemfile 요구와 다름 | rbenv/asdf로 해당 버전 설치 |
pod install이 이상한 에러 | 시스템 pod과 Gemfile의 pod 버전이 다름 | bundle exec pod install로 실행 |
match 단계에서 인증서 못 찾음 | 인증서 repo 권한·MATCH_PASSWORD 부재 | repo 권한, 환경변수, Apple Developer 계정 점검 |
upload_to_play_store에서 401/403 | PLAY_STORE_JSON_PATH 부재/잘못 | .env 로드, 서비스 계정 권한 확인 |
bundle: command not found | Bundler 미설치 | gem install bundler |
| Ruby 3.4 이후 빌드 깨짐 | 표준 라이브러리에서 일부 모듈 빠짐 | 루트 Gemfile처럼 bigdecimal, logger, benchmark, mutex_m 명시 (이미 적용됨) |
bundle exec fastlane ios beta
└──┬───┘ └──┬──┘ └─┬┘ └─┬─┘
│ │ │ └── Fastfile의 lane 이름 (실행할 작업)
│ │ └─────── platform 지정 (default_platform 다르면 필요)
│ └────────────── 실행할 CLI (fastlane gem이 제공)
└────────────────────── Gemfile에 잠긴 버전으로 실행하라는 wrapper
세 줄로 요약하면:
bundle install로 설치하고 bundle exec로 실행