프론트 공부/리액트 네이티브

리액트 네이티브로 안드로이드 환경 분리하기 (env 파일 분리 + 앱 패키지 이름 분리 + fcm 설정 + signing key 설정까지!)

홍구리당당 2025. 3. 26. 22:39

공익 차원에서 쓰는 글.
내가 이 설정 하나 하겠다고 정말 2주 넘게 쌩 고생을 했기에..

참고로 저는 expo 안 쓰고 bare react-native-community-cli 를 씁니다!!!!!!

목표

  1. .env.development, .env.production 환경 변수를 나눠서 디버깅이나 앱 빌드, apk, aab 파일 생성할 때 원하는 env 파일 적용되게 하기
  2. 개발 환경 앱 패키지 이름은 com.myService.dev, 실제 앱 패키지 이름은 com.myService 이런 식으로 분리하기 + 파이어베이스(fcm) 도 설정하기
  3. 귀찮게 안드로이드 스튜디오 키지 말고 vscode 내에서 커맨드만으로 디버깅, apk 파일 생성, aab 파일 생성 등 모든 걸 처리하기 (+ signing key 비밀번호, 별칭 등은 안전하게 보관하기)

시작하기에 앞서...

내가 어떤 어플들이 필요한지 먼저 구상해야 한다. 안드로이드 환경을 나눌 때엔 buildType과 flavor 이 두 개를 써서 나눠야 하는데,
여기서 flavor이란 어떤 용도로 앱을 나눌지 내가 직접 커스텀하는 값이다.
buildType이란 앱을 빌드할 때 어떤 타입으로 빌드할지 설정하는 값이다.

예를 들어,
나는 com.myService 라는 어플에 대해
development 환경, production 환경에서 테스트할 수 있어야 했다. -> flavor을 development, production로 나눠야 함.
앱을 빌드할 때엔 실제 플레이스토어에 등록해둔 signging key 를 가지고 빌드할 수 있는 release 모드와, 단순 디버깅용이라 서명이 필요없는 debug 이 2가지 모드가 필요했다. -> buildType을 release, debug로 나눠야 함.

그니까 나는 하나의 어플을

  1. developmentDebug 모드 (개발 서버와 통신하는 어플, 디버깅할 때 씀)
  2. developmentRelease 모드 (개발 서버와 통신하는 어플, apk로 만들 때 씀)
  3. productionDebug 모드 (상용 서버와 통신하는 어플, 디버깅할 때 씀)
  4. productionRelease 모드 (상용 서버와 통신하는 어플, apk로 만들거나 aab로 만들 때 씀)
    이렇게 4개가 필요한 거다.

이 때 flavor 이름은 아무렇게나 지어도 된다! development 말고 staging 이란 명을 써도 되고, 만약 무료 앱과 유료 앱 환경을 분리하고 싶다면 free, priminum 이란 이름을 써도 될테고, 더 많은 환경으로 분리해야 한다면 development, production, staging... 이렇게 더 많이 만들 수도 있다.
다만 buildType은 안 바꾸는 걸 추천한다. 기본적으로 주어지는 release, debug 타입만으로도 충분하고, 어차피 사용자 정의 buildType을 만들어봤자 release, debug 모드로부터 상속받아서 만들어야 한다. matchingFallbacks 도 설정해야 하고, 폴더도 좀 수정해야 하고.. 그치만 불가능한 건 아님!! 자세한 건 buildType 이 문서를 참고하면 된다..

flavor과 buildType은 자동으로 하나로 합쳐져서 이름이 생성된다.
예를 들어 development, production flavor을 만들었고 build type으론 release, debug 가 있다면,
developmentDebug, developmentRelease 이렇게 자동으로 flavor + buildType 카멜케이스로 이름이 생성된다. 나중에 npm run android 할 때 mode=developmentDebug 로 해주면, development flavor의 debug 빌드 타입을 활용해서 앱이 빌드되고 실행된다는 뜻이다.

어쨌든 중요한 건 buildType과 flavor이 각각 뭐하는 놈인지 잘 알아야 한다는 거다.. 난 이 두 개가 헷갈려서 많은 시간을 버렸다.ㅠㅠ


본격적으로 환경 분리하기!!

A. 먼저 env 파일을 분리한다.

1. react-native-config 를 설치한다.
npm install react-native-config 하면 되고, 굳이 타입스크립트용을 따로 설치할 필요는 없다. (아이폰도 세팅하려면 cd ios, pod install 해줘야 함, 안드로이드는 필요 없음.)

2. .env.development, .env.production 파일을 만들고 프로젝트 폴더 최상단에 둔다.

이 때 env 변수 이름은 리액트js 처럼 접두어를 붙일 필요 없이 그냥 APP_ROLE 이라든가, URL 뭐 이렇게 아무렇게나 지으면 된다. 대신 APP_ROLE=development 이런 식으로 = 앞뒤로 띄어쓰기가 없어야 하고, 쌍따옴표나 따옴표를 쓰면 안됨.

  • 또한 env 파일엔 중요한 비밀번호 키나 토큰을 보관해선 안된다. env에는 노출되도 괜찮은 api url 정도 작성해야 함. 왜냐면 우린 이제부터 r 이에 대한 건 https://reactnative.dev/docs/security 이 문서를 참고하면 된다.
    또한 내 포스팅 앞 부분 글을 보면 react-native-config 문서에서도 보안성을 위해 중요한 값은 쓰지 말라고 한다.

3. [project]/android/app/build.gradle 파일을 열고, apply plugin: "..." 아래에 다음 문구를 추가한다.

project.ext.envConfigFiles = [
    productionDebug: ".env.production",
    productionRelease: ".env.production",
    developmentDebug: ".env.development",
    developmentRelease: ".env.development",
]
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"

여기서 productionDebug, productionRelease 등은 앞에서 말했던 flavor + buildType 으로 자동생성된 카멜케이스 이름이다. 즉, 만약 내가 flavor로 development, staging, production 세 개를 설정했다면 envconfigFiles 안엔 productionDebug, stagingDebug, developmentDebug... 이렇게 설정해야 한다.

4. 세팅은 끝났다!!! 확인하기 위해 콘솔문을 추가하고 앱을 실행해보자.

import Config from "react-native-config";

...

console.info("my react native config env: ", Config.APP_ROLE); // <- .env 파일에 APP_ROLE=development 이런 식으로 적어줌.

만약 console 문에 Config... 가 undefined 로 출력된다면 cd android && gradlew clean 하고 다시 실행하자.
만약 Cannot locate tasks that match 'app:installDebug' as task 'installDebug' is ambiguous in project ':app'. Candidates are: 'installDevDebug', 'installDevDebugAndroidTest', 'installPrdDebug', 'installPrdDebugAndroidTest'. 이런 식으로 뭔가 ambiguous 에러가 뜬다면 잘못 설정된 게 아니니 걱정하지 말자.. 아마 flavor 설정을 아직 안 했다면 에러가 안 뜨겠지만, 이미 flavor 설정이 뭔가 되어있는 상태라면 npm run android 할 때 어떤 flavor, buildType으로 안드로이드를 실행하란 건지 모르겠다고 too ambiguous 하다고 에러를 뱉는 것이다. 그니까 일단 자신이 설정한 flavor + buildType을 가지고 이렇게 커맨드를 입력해보자. 다시 말하지만 react-native-community-cli 쓰는 사람 기준이다!! expo는 모름.
npx react-native run-android --mode=developmentDebug ->> 이렇게 --mode= 뒤에 flavor+buildType 카멜케이스체 이름을 붙여주면 해당 모드로 실행하겠단 뜻이다.



B. 앱 패키지 이름 (appId) 분리하기.

기본적으로 앱 패키지 이름은 하나이다! 예를 들어 내가 myService 라는 폴더에서 react-native init 으로 프로젝트를 만들었다면 패키지 이름은 자동으로 com.myService가 된다. 앱이 설치되거나 업데이트되는 기준은 패키지 이름이 같은가 이다!! 즉, 앱 이름이 다르든 내부 로직이 얼마나 다르든 상관없이 패키지 이름이 com.myService로 동일하다면, 그 중 더 높은 버전의 앱으로 덮어씌워진다.

즉 개발용 앱, 실제 상용 앱을 따로따로 설치하고 싶다면 두 앱의 패키지 이름을 분리해야 한다는 것이다.
예를 들어 개발용 앱은 com.myService.dev, 상용 앱은 com.myService로 분리하는 것.
(나는 buildType에 따른 패키지 이름 분리할 필요는 없어서 developmentDebug 모드든 developmentRelease 모드든 둘 다 패키지 이름을 com.myService.dev 가 되게 했다. 다만 buildType에 따라서도 이름을 분리하고 싶다면 https://developer.android.com/build/build-variants?hl=ko 공식문서를 참고하면 된다!)

1. project/android/app/build.gradle 파일을 연다
이제부터 적을 내용들은 모두 이 파일에 적을 것.

2. defaultConfig 라는 게 있을텐데 이 안에 applicationId과 resValue를 적는다.

이미 적혀있으면 패스해도 된다.

    defaultConfig {
        applicationId "com.myService" << 이거!
        resValue "string", "build_config_package", "com.myService" << 이거!
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode 1
        versionName "0.0.1"
    }

3. buildTypes 라는 게 있을텐데 만약 buildType을 커스텀했더라면 이 부분도 수정해야 할테지만, 그냥 debug, release만 쓴다면 그냥 놔두면 된다.

다만 buildTypes에 따라서도 앱 패키지 이름을 바꾸고 싶다면!! 아래처럼 문구를 추가하면 된다. 예를 들어 debug 모드 빌드된 앱들은 모두 .debug 라는 이름을 붙이고 싶다면, 즉 com.myService.debug 라는 패키지 명을 쓰고 싶다면 아래처럼 추가하자.

    buildTypes {
        debug {
            debuggable true
            signingConfig signingConfigs.debug >> 이건 나중에 C번 설명할 때 추가할 내용임.
            applicationIdSuffix ".debug" >> 이렇게!
        }
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
            signingConfig signingConfigs.release >> 이건 나중에 C번 설명할 때 추가할 내용임.
        }
    }

4. buildTypes 아래에 flavor을 추가하자. 아래 코드 복붙하면 된다

buildTypes{...}

flavorDimensions "default"
productFlavors {
    development {
            dimension "default"
            applicationIdSuffix ".dev"  // 개발용 앱에선 com.myService.dev 로 설정하려면 이렇게 적기.
            versionNameSuffix "-DEV" // 버전 뒤에 -DEV 라는 접미사 붙게 설정하려면 이 문구도 추가.
            resValue "string", "app_name", "My Service DEV"
    }
    production {
            dimension "default"
            resValue "string", "app_name", "My Service"
    }
}

먼저 flavorDimensions이란 어떤 타입인지 정하는 변수명이다. default 말고 아무 값이나 원하는 걸 적으면 된다. environment라 이름 지어도 되고... flavor을 추가하고 싶다면, 예를 들어 유아용 어플, 성인용 어플로 flavor을 나누고 싶다면 flavorDimensions "userMode" 을 추가하고, flavor 이름을 kid, adult 로 짓고, 그에 따라 applicationIdSuffix도 .ked, .adult 이런 식으로 지으면 될 거다. 예를 들면 아래와 같다.
참고로 이렇게 flavor을 늘리면 그에 따라 위에서 설정한 project.ext.envConfigFiles 에서도 늘어난 flavor만큼 추가해줘야 한다.
또한 모드 이름도 flavor + buildType 에서 flavor + flavor2 + ... buildType 으로 바뀐다. flavor 은 추가한 순서대로 이름이 붙는데, 예를 들어 아래 코드에선 kidsDevelopmentDebug 이런 식으로 이름이 생성될 것이다. 패키지 이름도 순서대로 붙기 때문에 com.myService.kids.dev 이렇게 된다. (buildType에 따른 패키지 이름도 있다면 com.myService.kids.dev.debug 이렇게 뒤에 buildType 패키지명이 붙음.)

flavorDimensions "userMode", "environment"

productFlavors {
    kids {
        dimension "userMode"
        applicationIdSuffix ".kids"
        resValue "string", "app_name", "My Service Kids"
    }
    adult {
        dimension "userMode"
        applicationIdSuffix ".adult"
        resValue "string", "app_name", "My Service"
    }

    development {
        dimension "environment"
        applicationIdSuffix ".dev"
        resValue "string", "app_suffix", "DEV"
    }
    production {
        dimension "environment"
        resValue "string", "app_suffix", ""
    }
}

5. 이번엔 project/android/app/src 폴더로 간다!!

여기에 보면 debug, main, release 이렇게 3개의 폴더가 있을 것이다. 이 3개는 건드리지 말자!!!

6. project/android/app/src/main 폴더를 flavor 개수만큼 복사하고 project/android/app/src 폴더 안에 붙여넣는다.
예를 들어 flavor이 development, production 2개라면 project/android/app/src 안에 main 폴더 2번 복사해서 붙여넣는 거다.
그럼 src 폴더 안엔 debug, release, main, main copy, main copy 2 이렇게 폴더가 생길 것임. 만약 flavorDimensions이 여러 개라면, flavor 조합만큼 폴더를 복붙해야 한다. 앞의 예시대로 userMode, environment 두 개의 flavor dimesions이 있다고 치면 kidDevelopment, kidProduction, adultDevelopment, adultProduction 4개 조합이 생길테니 main 폴더를 4번 복붙해야 함.

그 후, 복사한 main 폴더들 이름을 각각 flavor 이름으로 바꾼다.
나는 development, production flavor만 있으니까 폴더 이름을 각각 수정하고, project/android/app/src 내 폴더로 main, release, debug, development, production 이렇게 5개를 두었다.

7. 복사한 폴더들 내 java 폴더와 AndroidManifest.xml 를 삭제한다

기존에 있던 main, release, debug 폴더는 그냥 놔두고!!
새로 생성한 flavor 폴더들 안의 것들만 삭제한다!
res 폴더는 그냥 놔둔다. 나중에 앱 아이콘을 flavor 별로 수정하고 싶다면 여기서 바꾸면 된다.

8. 만약 fcm 을 쓰고 있다면... (안 쓰면 8번 패스하고 9번으로 ㄱ)

google-services.json 파일을 아마 project/android/app 폴더 안에 두고 쓰고 있을텐데, 만약 flavor마다 다른 fcm 설정을 해야 한다면 파이어베이스 콘솔에 프로젝트 하나 더 파서 com.myService.dev 앱 등록하고, google-services.json 파일 다운받아서 위에서 만든 flavor 폴더 안에 넣으면 된다.
예를 들면
project/android/app/src/development/google-services.json
project/android/app/src/production/google-services.json
이렇게 각 폴더 안에 google-services.json 을 넣고, project/android/app 폴더에 있던 기존 google-services.json은 삭제하면 된다.

근데 만약!! com.myService 가 받는 fcm 메세지를 com.myService.dev에서도 동일하게 받고 싶다면, 만약 두 어플의 fcm 설정이 동일하다면!! 한 프로젝트 내에 앱을 추가하면 된다!! 하나의 프로젝트 안에 여러 어플이 있다면 패키지 이름이 달라도 fcm 설정을 공유한다. 실제로 프로제트 안에 이미 com.myService가 있는데 여기다가 com.myService.dev 를 추가해서 google-services.json 파일을 다운받아보면, 안에 웬 com.myService 관한 정보들이 들어있다. (아~ 그래서 웹에 푸시메세지 기능 추가할 때 웹이랑 앱은 패키지 이름이 달라도 같은 푸시메세지를 받을 수 있게끔 되어있나보구나..)
그 후 마찬가지로
project/android/app/src/development/google-services.json
project/android/app/src/production/google-services.json
이렇게 각 폴더 안에 google-services.json 을 넣고, project/android/app 폴더에 있던 기존 google-services.json은 삭제하면 된다.
패키지 명이 다르다면 fcm 로직이 동일하더라도 무조건 각 폴더 안에 따로따로 google-services.json을 넣어줘야 에러가 안 난다.
공식문서 를 참고하자.

만약!!! fcm 로직은 동일하지만 특정 주제에 대해선 어떤 어플만 허용, 어떤 어플은 비허용... 하려면 fcm 주제구독 에 대해 서치해보면 될 거 같다. (이 부분은 내 프로젝트에선 필요 없어서 안함)

9. 이제 flavor이 잘 설정됐는지 테스트하기!

누차 말하지만!! react-native-community-cli 기준이다!!

먼저 npx react-native run-android --mode=developmentDebug 를 실행해본다. 제대로 앱이 설치됐다면 성공! 만약 ambiguous.. 어쩌구 에러가 뜬다면 보통 flavor 이름을 잘못 입력했거나 buildType을 붙이지 않은 경우다. --mode 뒤엔 반드시 flavor + buildType 이름을 제대로 작성해주자. 설치된 어플 이름이 My Service DEV 이런 식으로 잘 보인다면 성공이다.

그리고 npx react-native run-android --mode=productionDebug 도 실행해보자. 앞서 applicationIdSuffix 를 설정했기 때문에 development flavor과 production flavor은 서로 다른 패키지명을 가지므로, 앱이 덮어씌워지면 안된다. 두 개 어플이 공존해야 한다. 만약 잘 공존한다면 성공!
역시나 ambiguous 에러가 뜬다면 flavor 이름, buildType 이름을 잘 붙여주고 다시 시도하자.



C. buildTypes에 서명 추가해서 vscode 내 커맨드로 apk, aab 파일 빌드까지 할 수 있게 하기!!!!

마지막이다.. 솔직히 이건 키 문서 이거 따라하기만 하면 끝이다.

우선 나는 build type 중 debug 모드에선 서명 키가 필요 없고, release에서만 서명키가 필요했다.

서명키는 반드시 로컬 컴퓨터에 두고, key alias 나 password 등은 위 공식문서에 나온 대로 ~/.gradle/gradle.properties 파일에 적었다. 프로젝트 폴더 내 .gradle 폴더 아니고 로컬 컴퓨터 전역 위치에 파일 만들어야 한다!!!

문서대로 다 따라하고 project/android/app/build.gradle 파일의 buildTypes 에도 다 설정해줬다면 아래 코드를 적자. 이제 스크립트를 짜야 한다.
package.json 에 아래처럼 코드를 짰다.

  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "lint": "eslint .",
    "start": "react-native start",
    "start:clean": "react-native start --reset-cache",
    "test": "jest",
    "debug:androidDevelopmentDebug": "react-native run-android --mode=developmentDebug --appId com.myService.dev --active-arch-only",
    "debug:androidProductionDebug": "react-native run-android --mode=productionDebug --appId com.myService --active-arch-only",
    "apk:androidDevelopmentRelease-w": "cd android && gradlew clean && gradlew assembleDevelopmentRelease && explorer app\\build\\outputs\\apk\\development\\release",
    "apk:androidProductionRelease-w": "cd android && gradlew clean && gradlew assembleProductionRelease && explorer app\\build\\outputs\\apk\\production\\release",
    "aab:androidProductionRelease-w": "cd android && gradlew clean && gradlew bundleProductionRelease && explorer app\\build\\outputs\\bundle\\production\\release",
    "apk:androidDevelopmentRelease-m": "cd android && ./gradlew clean && ./gradlew assembleDevelopmentRelease && open ./app/build/outputs/apk/development/release",
    "apk:androidProductionRelease-m": "cd android && ./gradlew clean && ./gradlew assembleProductionRelease && open ./app/build/outputs/apk/production/release",
    "aab:androidProductionRelease-m": "cd android && ./gradlew clean && ./gradlew bundleProductionRelease && open ./app/build/outputs/bundle/production/react"
  },

참고로 flavor을 설정한 이상 npm run android 만 하면 ambiguous 에러가 날 거다. npm run android 명령어 쓸 예정이라면 뒤에 --mode 옵션 붙이고 적절히 커스텀하면 된다.

맥과 윈도우 각각 명령어가 조금 달라서 저렇게 나눠줬다.

npm run debug:androidDevelopmentDebug 명령어 맨 뒤에 붙은 --active-arch-only는 그냥 빌드 속도 빠르게 할라고 붙인 옵션이다. 빼고 싶으면 빼도 된다. 이게 뭔지 알고 싶다면 공식 문서 참고. appId도 안 붙여도 된다고는 하는데 이상하게 난 시도하면 앱 이름을 못찾겠단 에러가 나서 그냥 붙였다.

그리고 npm run android --mode 말고 --variant 명령어를 붙어서 사용하라는 경우가 있던데... --help로 react-native-community-cli 명령어 보면 --variant 옵션은 없다. 그냥 --mode 옵션만 쓰면 된다.

&& open~ 혹은 && explorer~ 명령어는 그냥 파일 찾아 열기 귀찮아서 자동으로 빌드 후 파일 열리게 한 거다. 이 또한 없애고 싶으면 없애두 됨.

다음 글은 ios에서의 세팅으로 쪄오겠다.....