React Native 에서 Fastlane으로 배포 파이프라인 설계하기 (5. android 배포 자동화의 환경 구성)

eeennsu·2026년 7월 24일

React Native

목록 보기
74/95

개요

이번 편이 끝나면 아래 세 줄로 환경별 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

이걸 위해 이번 편에서 다루는 내용은 다음과 같다.

  • Android keystore(업로드 키) 생성과 안전한 보관
  • Play App Signing 이해 — iOS의 match보다 오히려 더 단순하다
  • Google Play Console Service Account JSON 발급
  • gradle 액션으로 AAB(.aab) 빌드
  • upload_to_play_store(supply)로 환경별 트랙 업로드
  • iOS와 동일한 --env 패턴으로 dev / beta / pro 분리



1. Android 서명, iOS와 무엇이 다른가

iOS는 "Apple이 발급한 인증서를 받아 와서" 빌드를 서명해야 했다. 그래서 match 같은 도구로 인증서를 내려받는 과정이 필요했다. Android는 그럴 필요가 없다. 개발자가 직접 keystore(서명 키 저장소)를 만들면 그게 곧 서명 키다. Apple 같은 발급 기관이 중간에 없다. 그래서 iOS보다 서명 준비가 한결 간단하다.

keystore가 뭔지 짧게: 앱에 서명할 비밀 키를 담아두는 금고 파일이다. iOS의 인증서에 대응하는 개념인데, 발급받는 게 아니라 내가 직접 만든다.

Android의 두 가지 키 — Upload Key vs App Signing Key

여기서 딱 하나 개념만 잡고 가면 된다. 2017년 8월 이후 Google Play에 새로 올라가는 앱은 Play App Signing을 써야 하는데, 이건 키를 둘로 나누는 시스템이다.

누가 갖고 있나역할
Upload Key (업로드 키)개발자AAB/APK에 서명해서 Play Console에 업로드할 때 사용
App Signing Key (앱 서명 키)GooglePlay 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)로 따로 만들 수도 있지만, 하나를 공용으로 써도 문제없다. 보통은 이렇게 간다.

  • pro(운영) : 전용 Upload Keystore. 절대 잃으면 안 되는 파일
  • dev/beta : pro와 같이 쓰거나, 편의상 디버그용 keystore를 따로 두기도 함

이 글에서는 모든 환경이 같은 Upload Keystore를 공유하는 가장 일반적인 구성으로 간다. (패키지명을 환경별로 나눠도 keystore는 하나여도 된다.)



2. 환경 전략 — dev / beta / pro

iOS와 똑같은 컨셉이다.

환경용도패키지명 예시Play Console 트랙
dev개발자/QA용. 빠른 검증com.yourcompany.myapp.devInternal testing
beta사내 베타 테스터, 스테이지 서버com.yourcompany.myapp.betaInternal / Closed testing
pro운영 배포com.yourcompany.myappProduction (또는 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에 올린다.



3. Upload Keystore 생성

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 지원 티켓 등 번거로운 절차를 거쳐야 하니 안 잃는 게 최선


4. Play Console 등록과 첫 수동 업로드

여기서 초보가 자주 놓치는 중요한 전제가 있다. fastlane(supply)으로 자동 업로드를 하려면, 그 전에 Play Console에 앱이 등록돼 있고 + 최소 한 번은 사람이 직접 빌드를 올려둔 상태여야 한다. 이건 Google 정책이라 우회할 수 없다. (즉 "완전 자동화"라도 최초 1회는 수동이다.)

1) Play Console에서 앱 만들기

Google Play Console에서 아래를 환경(dev/beta/pro)별로 반복한다. (앞서 말했듯 각각 별개 앱이다.)

  1. Create app → 앱 이름, 기본 언어, 무료/유료 선택
  2. 좌측 App content → 개인정보처리방침, 광고, 콘텐츠 등급 등 필수 항목 채우기. (pro는 전부 필요, dev/beta는 internal testing만 할 거면 일부 생략 가능)
  3. Production(또는 Internal testing) 트랙에 첫 빌드를 수동 업로드

dev/beta는 Internal testing 트랙에만 올려도 충분하다. Production까지 가는 건 pro뿐이다.


2) Play App Signing 활성화

첫 빌드를 올릴 때 Play Console이 "Play App Signing을 쓰겠냐"고 묻는다. 반드시 활성화한다. 선택지는 둘이다.

  • (권장) Google이 App Signing Key를 새로 생성 — 가장 안전
  • 내가 만든 키를 App Signing Key로 등록 — 옛날 앱을 옮겨올 때만

이 시점에 우리가 만든 keystore는 자동으로 Upload Key로 등록된다. (1번에서 설명한 그 구조가 여기서 완성된다.)



5. Service Account JSON 발급

fastlane이 Play Console API에 접근하려면 Service Account가 필요하다. iOS의 App Store Connect API Key에 해당하는, 자동화 전용 로봇 계정이다.

왜 필요한가: fastlane은 사람이 아니라 프로그램이라 사람 계정으로 로그인할 수 없다. 그래서 "이 로봇에게 업로드 권한을 준다"는 방식으로 인증한다. 그 로봇의 신분증이 JSON 키 파일이다.

1) Google Cloud에 Service Account 생성

  1. Play Console → 좌측 Setup → API access
  2. 처음이면 약관 동의 후, Google Cloud 프로젝트를 새로 만들거나 기존 것을 선택
  3. Create new service account → Google Cloud Console 새 탭으로 이동
  4. Google Cloud Console에서 CREATE SERVICE ACCOUNT
    • 이름: 예) fastlane-supply
    • CREATE AND CONTINUE 말고 그냥 DONE — 역할(권한)은 Google Cloud가 아니라 Play Console에서 따로 줄 것이다
  5. 만들어진 Service Account 옆 점 세 개 → Manage keys → ADD KEY → Create new key → JSON
  6. 자동으로 받아지는 xxx.json 파일을 안전한 위치에 보관 (이게 로봇의 신분증이다. 커밋 금지.)

2) Play Console에서 권한 부여

다시 Play Console로 돌아온다.

  1. Setup → API access 화면에 방금 만든 Service Account가 보인다
  2. Grant access 클릭
  3. 권한 설정 — Releases 관련 권한을 준다. (최소: View app information, Manage testing tracks, Release to production)
  4. App permissions에서 이 계정이 접근할 앱(dev/beta/pro 전부)을 선택
  5. Apply → Invite user → Send invitation

3) 동작 확인

받은 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 경로가 틀린 것이다.)



6. gradle 설정 — keystore 연결

이제 RN 프로젝트의 Android 빌드가 우리 Upload Keystore로 서명되도록 연결한다.

gradle이 뭔지 다시 짚으면, Android 쪽 빌드를 실행하는 도구다. (1편 비유: 안드로이드판 빌드 스크립트 러너 + 패키지 매니저.) 서명 설정도 여기 gradle 스크립트에 넣는다.

1) keystore 파일 배치

keystore를 프로젝트 안에 두지 말고 외부 경로에 두고 환경변수로 참조하는 게 안전하다. (실수로 Git에 올라가는 사고 방지)

mkdir -p ~/keystores
cp ~/Downloads/my-app-upload.keystore ~/keystores/

2) 비밀번호를 어디에 둘까 — 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까지 자연스럽게 이어진다.


3) 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 경로·비번을 넘겨주는 방식으로 서명이 동작한다.


4) 환경별 패키지명(applicationId) 분리

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, bundleDevRelease
  • assembleBetaRelease, bundleBetaRelease
  • assembleProRelease, bundleProRelease

여기서 assemble*은 APK, bundle*은 AAB를 만든다. (스토어 업로드는 AAB이므로 bundle*을 쓴다.) 이 태스크 이름이 곧 fastlane에서 호출할 키가 된다.

💡 React Native의 JS 번들 경로도 flavor별로 자동 분리되므로 추가 설정 없이 동작한다. 다만 react-native-config.env의 JS 변수를 환경별로 바꾸려면 flavor별 .env 매핑이 따로 필요하다. (자세한 건 다음 편에서 다룬다.)



7. 환경별 .env 파일 구성

iOS 편과 똑같은 패턴이다. android/fastlane/.env(공통)와 .env.<name>(환경별)을 둔다. --env dev를 주면 .env.env.dev 순으로 합쳐진다.

1) 공통 .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=실제-비밀번호

2) 환경별 .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으로 분리해서 사람이 명시적으로 호출하게 만드는 게 안전하다.

3) .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(gradleupload_to_play_store)은 비어 있다. 서명과 환경이라는 토대를 깐 단계다. 다음 편에서는 이 토대 위에 Android용 Fastfile lane을 작성해, 맨 위에서 예고한 세 줄을 실제로 완성한다.

profile
이력서 https://resume.eunsu.pro

0개의 댓글