이번 편이 끝나면 아래 세 줄로 환경별 Play Store 트랙 업로드가 되는 상태를 향해 간다. iOS 편과 짝을 이루는 Android 쪽 설정이다.
pnpm set-android-dev # dev 환경 빌드 & internal 트랙 업로드
pnpm set-android-beta # beta 환경 빌드 & internal 트랙 업로드
pnpm set-android-pro # production 환경 빌드 & production 트랙 업로드
내부적으로는 각각 아래 명령을 부른다. (iOS와 완전히 같은 --env 패턴이다.)
cd android && bundle exec fastlane dev_mode --env dev
cd android && bundle exec fastlane beta_mode --env beta
cd android && bundle exec fastlane pro_mode --env pro
이걸 위해 이번 편에서 다루는 내용은 다음과 같다.
.aab) 빌드--env 패턴으로 dev / beta / pro 분리iOS는 "Apple이 발급한 인증서를 받아 와서" 빌드를 서명해야 했다. 그래서 match 같은 도구로 인증서를 내려받는 과정이 필요했다. Android는 그럴 필요가 없다. 개발자가 직접 keystore(서명 키 저장소)를 만들면 그게 곧 서명 키다. Apple 같은 발급 기관이 중간에 없다. 그래서 iOS보다 서명 준비가 한결 간단하다.
keystore가 뭔지 짧게: 앱에 서명할 비밀 키를 담아두는 금고 파일이다. iOS의 인증서에 대응하는 개념인데, 발급받는 게 아니라 내가 직접 만든다.
여기서 딱 하나 개념만 잡고 가면 된다. 2017년 8월 이후 Google Play에 새로 올라가는 앱은 Play App Signing을 써야 하는데, 이건 키를 둘로 나누는 시스템이다.
| 키 | 누가 갖고 있나 | 역할 |
|---|---|---|
| Upload Key (업로드 키) | 개발자 | AAB/APK에 서명해서 Play Console에 업로드할 때 사용 |
| App Signing Key (앱 서명 키) | Play Store가 사용자 디바이스로 배포할 때 다시 서명 |
흐름으로 보면 이렇다.
개발자 머신 Play Console 사용자 디바이스
───────── ──────────── ──────────────
AAB 빌드
↓ Upload Key로 서명
─────────────────→ 업로드
↓ Upload Key 확인
↓ App Signing Key로 다시 서명
──────────────────────────────→ 설치
이게 왜 좋은가? Upload Key를 잃어버려도 복구가 가능하기 때문이다. Google에 요청해 새 Upload Key로 재등록할 수 있다. 반면 옛날 방식(앱 서명 키를 개발자가 직접 관리)에서는 키를 잃으면 같은 앱을 영영 업데이트할 수 없었다. (1편에서 "keystore 분실 = 재앙"이라고 했던 게 이 옛날 방식 얘기다.) 지금도 옛날 방식으로 운영 중인 앱이 있는 회사라면 keystore를 더 조심해야 한다.
신규 앱이라면 무조건 Play App Signing을 켜자. 사실상 강제이기도 하다.
Upload Keystore 하나면 된다. 환경별(dev/beta/pro)로 따로 만들 수도 있지만, 하나를 공용으로 써도 문제없다. 보통은 이렇게 간다.
이 글에서는 모든 환경이 같은 Upload Keystore를 공유하는 가장 일반적인 구성으로 간다. (패키지명을 환경별로 나눠도 keystore는 하나여도 된다.)
iOS와 똑같은 컨셉이다.
| 환경 | 용도 | 패키지명 예시 | Play Console 트랙 |
|---|---|---|---|
| dev | 개발자/QA용. 빠른 검증 | com.yourcompany.myapp.dev | Internal testing |
| beta | 사내 베타 테스터, 스테이지 서버 | com.yourcompany.myapp.beta | Internal / Closed testing |
| pro | 운영 배포 | com.yourcompany.myapp | Production (또는 Open testing 거쳐서) |
⚠️ 여기서 iOS와 결정적으로 다른 점 하나. Play Console은 패키지명별로 완전히 별개의 앱으로 취급한다. 즉
com.yourcompany.myapp.dev/.beta/ (접미사 없음) 세 개를 Play Console에 각각 따로 "앱 등록" 해야 한다. (iOS는 App Store Connect에서 앱 하나에 여러 번들 ID를 두는 식이었지만, Android는 아예 앱을 3개 만드는 셈이다.) 대신 한 폰에 dev/beta/pro를 동시에 설치할 수 있는 건 iOS와 같다.
"트랙(Track)"이 뭔지 짚고 가면, Play Console에서 빌드를 누구까지 보게 할지 나누는 배포 단계다. Internal → Closed → Open → Production 순으로 대상이 넓어진다. dev/beta는 좁은 트랙(Internal)에, pro는 Production에 올린다.
keytool은 JDK에 기본 포함된 도구다. 1편에서 JDK 17을 깔았으니 바로 쓸 수 있다.
# 프로젝트 루트가 아니라, Git과 무관한 안전한 디렉터리에서 작업한다
mkdir -p ~/keystores
cd ~/keystores
keytool -genkeypair -v \
-storetype PKCS12 \
-keystore my-app-upload.keystore \
-alias my-app-upload \
-keyalg RSA \
-keysize 2048 \
-validity 10000
각 옵션의 의미는 이렇다.
-storetype PKCS12 : 키 저장소 파일 포맷. 옛날엔 JKS가 기본이었지만 JDK 9부터 PKCS12가 표준·권장이다.-keystore : 만들어질 파일명-alias : keystore 안에 든 키의 별칭. 뒤에서 gradle 설정이 이 이름으로 키를 찾는다.-keyalg RSA -keysize 2048 : RSA 2048비트 키-validity 10000 : 유효 기간(일). 10000일이면 약 27년이다. Play Console은 2033년 이후 만료되는 키를 요구하므로 넉넉히 길게 잡는다.실행하면 비밀번호와 신원 정보를 차례로 물어본다.
Enter keystore password: (강력한 비밀번호 입력)
Re-enter new password: (확인)
What is your first and last name?
[Unknown]: Your Name
What is the name of your organizational unit?
[Unknown]: Mobile Team
What is the name of your organization?
[Unknown]: Your Company
... (이하 생략)
Enter key password for <my-app-upload>
(RETURN if same as keystore password): (엔터 또는 별도 비번)
이 keystore 파일과 비밀번호는 프로젝트에서 가장 중요한 자산이다.
- 비밀번호는 1Password 같은 관리자에 보관
- 파일은 여러 곳에 백업 (회사 보안 정책에 따라 NAS·외장 디스크·1Password 첨부 등)
- 절대 Git에 커밋 금지
- Play App Signing 덕에 잃어도 복구는 가능하지만, Google 지원 티켓 등 번거로운 절차를 거쳐야 하니 안 잃는 게 최선
여기서 초보가 자주 놓치는 중요한 전제가 있다. fastlane(supply)으로 자동 업로드를 하려면, 그 전에 Play Console에 앱이 등록돼 있고 + 최소 한 번은 사람이 직접 빌드를 올려둔 상태여야 한다. 이건 Google 정책이라 우회할 수 없다. (즉 "완전 자동화"라도 최초 1회는 수동이다.)
Google Play Console에서 아래를 환경(dev/beta/pro)별로 반복한다. (앞서 말했듯 각각 별개 앱이다.)
dev/beta는 Internal testing 트랙에만 올려도 충분하다. Production까지 가는 건 pro뿐이다.
첫 빌드를 올릴 때 Play Console이 "Play App Signing을 쓰겠냐"고 묻는다. 반드시 활성화한다. 선택지는 둘이다.
이 시점에 우리가 만든 keystore는 자동으로 Upload Key로 등록된다. (1번에서 설명한 그 구조가 여기서 완성된다.)
fastlane이 Play Console API에 접근하려면 Service Account가 필요하다. iOS의 App Store Connect API Key에 해당하는, 자동화 전용 로봇 계정이다.
왜 필요한가: fastlane은 사람이 아니라 프로그램이라 사람 계정으로 로그인할 수 없다. 그래서 "이 로봇에게 업로드 권한을 준다"는 방식으로 인증한다. 그 로봇의 신분증이 JSON 키 파일이다.
fastlane-supplyxxx.json 파일을 안전한 위치에 보관 (이게 로봇의 신분증이다. 커밋 금지.)다시 Play Console로 돌아온다.
Releases 관련 권한을 준다. (최소: View app information, Manage testing tracks, Release to production)받은 JSON으로 연결이 되는지 바로 테스트해본다.
bundle exec fastlane run validate_play_store_json_key \
json_key:/path/to/service-account.json
The service account credentials are valid가 나오면 성공이다. (여기서 실패하면, 대개 2)의 권한 부여가 아직 반영 안 됐거나 JSON 경로가 틀린 것이다.)
이제 RN 프로젝트의 Android 빌드가 우리 Upload Keystore로 서명되도록 연결한다.
gradle이 뭔지 다시 짚으면, Android 쪽 빌드를 실행하는 도구다. (1편 비유: 안드로이드판 빌드 스크립트 러너 + 패키지 매니저.) 서명 설정도 여기 gradle 스크립트에 넣는다.
keystore를 프로젝트 안에 두지 말고 외부 경로에 두고 환경변수로 참조하는 게 안전하다. (실수로 Git에 올라가는 사고 방지)
mkdir -p ~/keystores
cp ~/Downloads/my-app-upload.keystore ~/keystores/
gradle.properties는 Git에 올라간다여기가 초보가 사고 치기 쉬운 지점이다. android/gradle.properties는 보통 Git에 커밋되는 파일이라, 여기에 비밀번호를 적으면 그대로 유출된다. 그래서 두 가지 패턴이 있다.
패턴 A — 사용자 전역 ~/.gradle/gradle.properties (내 컴퓨터에만 있는 파일이라 커밋될 일이 없음)
# ~/.gradle/gradle.properties
MYAPP_UPLOAD_STORE_FILE=/Users/you/keystores/my-app-upload.keystore
MYAPP_UPLOAD_KEY_ALIAS=my-app-upload
MYAPP_UPLOAD_STORE_PASSWORD=실제-비밀번호
MYAPP_UPLOAD_KEY_PASSWORD=실제-비밀번호
패턴 B — 환경변수에서 읽기 (CI 친화적)
build.gradle이 "환경변수가 있으면 그걸 쓰고, 없으면 gradle property를 폴백으로" 쓰게 만든다. CI에서는 환경변수만 주입하면 끝이라 깔끔하다.
이 글에서는 패턴 B로 간다. 나중에 CI까지 자연스럽게 이어진다.
android/app/build.gradle 수정signingConfigs(서명 설정을 정의하는 블록)를 이렇게 작성한다. System.getenv(...) ?: project.findProperty(...)는 "환경변수 먼저, 없으면 gradle property"라는 폴백 문법이다.
// android/app/build.gradle
android {
// ... 기존 설정
signingConfigs {
release {
def storeFilePath = System.getenv("ANDROID_KEYSTORE_FILE") ?: project.findProperty("MYAPP_UPLOAD_STORE_FILE")
def storePass = System.getenv("ANDROID_KEYSTORE_PASSWORD") ?: project.findProperty("MYAPP_UPLOAD_STORE_PASSWORD")
def keyAliasValue = System.getenv("ANDROID_KEY_ALIAS") ?: project.findProperty("MYAPP_UPLOAD_KEY_ALIAS")
def keyPass = System.getenv("ANDROID_KEY_PASSWORD") ?: project.findProperty("MYAPP_UPLOAD_KEY_PASSWORD")
if (storeFilePath) {
storeFile file(storeFilePath)
storePassword storePass
keyAlias keyAliasValue
keyPassword keyPass
}
}
}
buildTypes {
release {
signingConfig signingConfigs.release
// ... 기존 minify 등
}
}
}
이렇게 두면 fastlane이 환경변수로 keystore 경로·비번을 넘겨주는 방식으로 서명이 동작한다.
dev/beta/pro의 패키지명을 나누려면 productFlavors를 쓴다. flavor는 "같은 앱의 여러 변형"을 정의하는 gradle 개념으로, 여기서는 환경별 변형을 만든다.
// android/app/build.gradle
android {
// ...
flavorDimensions "environment"
productFlavors {
dev {
dimension "environment"
applicationIdSuffix ".dev" // 패키지명 뒤에 .dev 붙임
resValue "string", "app_name", "MyApp Dev" // 앱 표시 이름
}
beta {
dimension "environment"
applicationIdSuffix ".beta"
resValue "string", "app_name", "MyApp Beta"
}
pro {
dimension "environment"
// suffix 없음 — 운영용
resValue "string", "app_name", "MyApp"
}
}
}
AndroidManifest.xml에서는 앱 이름이 위 app_name을 참조하게 한다.
<application
android:label="@string/app_name"
...>
이렇게 하면 빌드 태스크가 flavor × buildType 조합으로 늘어난다.
assembleDevRelease, bundleDevReleaseassembleBetaRelease, bundleBetaReleaseassembleProRelease, bundleProRelease여기서 assemble*은 APK, bundle*은 AAB를 만든다. (스토어 업로드는 AAB이므로 bundle*을 쓴다.) 이 태스크 이름이 곧 fastlane에서 호출할 키가 된다.
💡 React Native의 JS 번들 경로도 flavor별로 자동 분리되므로 추가 설정 없이 동작한다. 다만
react-native-config로.env의 JS 변수를 환경별로 바꾸려면 flavor별.env매핑이 따로 필요하다. (자세한 건 다음 편에서 다룬다.)
iOS 편과 똑같은 패턴이다. android/fastlane/에 .env(공통)와 .env.<name>(환경별)을 둔다. --env dev를 주면 .env → .env.dev 순으로 합쳐진다.
.env모든 환경이 함께 쓰는 인증·keystore 정보를 둔다.
# android/fastlane/.env
# 모든 환경 공통 - Play Console 인증 (5번에서 받은 JSON)
SUPPLY_JSON_KEY=/Users/you/keystores/play-service-account.json
# 모든 환경 공통 - keystore (6번에서 배치한 파일)
ANDROID_KEYSTORE_FILE=/Users/you/keystores/my-app-upload.keystore
ANDROID_KEYSTORE_PASSWORD=실제-비밀번호
ANDROID_KEY_ALIAS=my-app-upload
ANDROID_KEY_PASSWORD=실제-비밀번호
.env.dev / .env.beta / .env.pro환경마다 달라지는 값(패키지명, flavor, 업로드 트랙 등)을 둔다.
# android/fastlane/.env.dev
PACKAGE_NAME=com.yourcompany.myapp.dev
GRADLE_FLAVOR=Dev
GRADLE_TASK=bundle
GRADLE_BUILD_TYPE=Release
SUPPLY_TRACK=internal
CHANGELOG_PREFIX=[DEV]
# android/fastlane/.env.beta
PACKAGE_NAME=com.yourcompany.myapp.beta
GRADLE_FLAVOR=Beta
GRADLE_TASK=bundle
GRADLE_BUILD_TYPE=Release
SUPPLY_TRACK=internal
CHANGELOG_PREFIX=[BETA]
# android/fastlane/.env.pro
PACKAGE_NAME=com.yourcompany.myapp
GRADLE_FLAVOR=Pro
GRADLE_TASK=bundle
GRADLE_BUILD_TYPE=Release
SUPPLY_TRACK=production
CHANGELOG_PREFIX=[PROD]
여기서 이 값들이 6번의 flavor 이름(Dev/Beta/Pro)과 이어져서 bundle + Dev + Release = bundleDevRelease 태스크가 만들어지는 식이다.
💡
SUPPLY_TRACK에 쓸 수 있는 값:internal,alpha(closed testing),beta(open testing),production. 처음에는 운영도internal로 두고 검증한 뒤, Play Console에서 수동으로 promote(승급)하는 걸 강력히 권장한다. 실수로 자동 production 배포가 나가는 사고를 막기 위해서다. 정말 자동으로 production까지 보내려면 별도 lane으로 분리해서 사람이 명시적으로 호출하게 만드는 게 안전하다.
.gitignore비밀번호·JSON 키가 담긴 .env 파일들은 커밋 금지다.
# android/fastlane/.gitignore
.env
.env.dev
.env.beta
.env.pro
*.json # service account JSON 실수 방지
여기까지 완료한 것
✅ Upload Keystore 생성 및 안전 보관
✅ Play App Signing 개념 이해 (Upload Key ↔ App Signing Key 분리)
✅ Play Console에 dev/beta/pro 앱 등록 + 첫 수동 업로드
✅ Service Account JSON 발급 및 권한 부여
✅build.gradle서명 설정 +productFlavors로 환경 분리
✅ 환경별.env구성
iOS와 마찬가지로, 아직 실제 빌드·업로드 lane(gradle → upload_to_play_store)은 비어 있다. 서명과 환경이라는 토대를 깐 단계다. 다음 편에서는 이 토대 위에 Android용 Fastfile lane을 작성해, 맨 위에서 예고한 세 줄을 실제로 완성한다.