한 프로젝트는 여러 환경에서 빌드 혹은 실행될 수 있다. 가장 단순하게는 개발 환경과 운영(production) 환경 두 환경으로 분리할 수 있고, local, dev, stage, test 등을 보다 세분화해서 분리할 수도 있다.

이 글에서는 여러 환경에 따라 React Native 프로젝트를 빌드 및 실행해야 할 때, 어떻게 설정을 나누어 관리할 수 있는지 설명한다.

1. react-native-config 설치

일단 react-native-config를 설치한다. 이 라이브러리는 .env.ENV_NAME파일들을 이용해 각 환경별로 필요한 값을 관리할 수 있다.

1.1. react-native-dotenv vs react-native-config

react-native-config 라이브러리와 비슷한 라이브러리로는 react-native-dotenv가 있다. 두 라이브러리는 단일 환경 사용자 측면에서는 거의 동일하지만, 현재 react-native-dotenv 가 다중 환경 지원이 제대로 되지 않는 이슈가 있어서 여기서 사용하기에는 적합하지 않다. (GitHub issue - APP_ENV doesn't work as expected 참고)

따라서 이 글에서는 react-native-config 를 사용한다.

1.2. .env 파일 작성

프로젝트 root 디렉토리에, 만들려는 환경만큼 파일을 생성한다. (이 파일들은 .gitignore 에 추가되는 게 좋다. 참고)

.env             // 기본, local 환경을 위한 env 파일로 사용
.env.dev         // dev 환경을 위한 env 파일
.env.stage       // stage 환경을 위한 env 파일
.env.production  // production 환경을 위한 env 파일

각 파일에서 원하는 값은 key=value 형식으로 작성하면 된다.

BUILD_ENV=LOCAL
BASE_URL=http://localhost:3000

2. 안드로이드 설정

2.1. android/app/build.gradle

안드로이드 설정에서 가장 중요한 것은 이 android/app/build.gradle 파일이다. 하나씩 설정을 추가해보자.

2.1.1. react-native-config 를 위한 설정

react-native-config 는 단일 환경이 아닌 여러 빌드 환경을 설정할 때는, autolinking 말고도 추가로 해줘야 할 설정이 있다. 문서를 자세히 읽지 않으면 놓치기 쉬우므로 정리하고 넘어간다.

파일 최상단에 (apply plugin: "com.android.application" 바로 아랫줄에) 아래 코드를 추가한다.

apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"

android.defaultConfig 항목에는 아래 코드를 추가한다.

android {
    // ...
    defaultConfig {
        // ...
        // APP_PACKAGE_NAME 대신 자신의 앱 패키지명을 넣으면 된다
        resValue "string", "build_config_package", "APP_PACKAGE_NAME"
    }
    // ...
}

2.1.2. 환경 분리를 위한 설정

android.buildTypes 항목에 환경별 설정을 추가한다.

android {
    // ...
    buildTypes {
        debug {
            signingConfig signingConfigs.debug
            applicationIdSuffix ".dev"
        }
        release {
            // Caution! In production, you need to generate your own keystore file.
            // see https://facebook.github.io/react-native/docs/signed-apk-android.
            signingConfig signingConfigs.debug
            minifyEnabled enableProguardInReleaseBuilds
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        }
        //
        // 아래부터가 추가되는 코드
        //
        devDebug {
            initWith debug
            applicationIdSuffix ".dev"
            matchingFallbacks = ['debug']
        }
        devRelease {
            initWith release
            applicationIdSuffix ".dev"
            matchingFallbacks = ['release']
        }
        stageDebug {
            initWith debug
            applicationIdSuffix ".test"
            matchingFallbacks = ['debug']
        }
        stageRelease {
            initWith release
            applicationIdSuffix ".test"
            matchingFallbacks = ['release']
        }
    }
}

devDebug, devRelease 등이 추가된 환경들이다. 앞에 붙은 dev, stage 가 빌드 환경 이름이고 뒤에 붙은 Debug, Release 가 빌드 유형 정보다. (네이밍 방식이 정해진 것은 아니다. 꼭 이 형식을 따를 필요는 없다.)

  • initWith: 해당 타입을 기반으로 새 타입을 생성하겠다는 의미다.
  • applicationIdSuffix: 앱이 빌드되면 패키지명 뒤에 이 값을 붙여준다. 즉 같은 앱을 환경별로 나눠서 설치할 수 있게 해준다.
  • matchingFallbacks: 앱 빌드 시 환경이름을 buildTypes 이름이 아니라 이 값으로 대체한다. (devDebug로 빌드해도 React Native 코드 상에서 환경 이름에 접근할 때는 debug가 된다.) 기본 node 환경에도 대응되게 하려면 이 값을 설정해주는 게 좋다.

2.1.3. 스토어키 등록

안드로이드 스튜디오에서는 실행 및 빌드 시 원하는 .env.ENVNAME 파일 적용을 할 수 없다. (사실 가능할 수도 있다. 찾아보진 않았다.) 커맨드라인에서는 가능한데, 실행은 아무 추가적 설정 없이 가능한 반면 (릴리즈)빌드는 스토어 키 설정을 해주어야 한다.

android.signingConfigs 에 릴리즈용 키 정보를 등록하면 된다.

signingConfigs {
    debug {
        storeFile file('debug.keystore')
        storePassword 'android'
        keyAlias 'androiddebugkey'
        keyPassword 'android'
    }
    // 이름을 release 로 하긴 했지만 다른 이름도 가능하다.
    // 단, 그 경우 `android.buildTypes.release`의 `signingConfig` 값도 같이 바꿔주어야 한다.
    release {
        if(project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
            storeFile file(MYAPP_UPLOAD_STORE_FILE)
            storePassword MYAPP_UPLOAD_STORE_PASSWORD
            keyAlias MYAPP_UPLOAD_KEY_ALIAS
            keyPassword MYAPP_UPLOAD_KEY_PASSWORD
        }
    }
}

보다 자세한 내용은 React Native 공식 문서 (Publishing to Google Play Store) 를 참고.

2.2. 환경별 파일 분리

./android/app/src/main 디렉토리에 아래와 같은 파일들이 존재할 것이다.

  • AndroidManifest.xml
  • MainActivity.java
  • MainApplication.java
  • 각종 리소스(이미지, res/values/*.xml 등)

해당 파일들을 환경별로 따로 관리하고 싶다면, 환경별로 디렉토리를 별도로 만들어 관리하면 된다.

예를 들어 devDebug 환경의 AndroidManifest.xml 파일을 따로 관리하고 싶다면 android/app/src/devDebug 디렉토리를 만든 뒤, 해당 디렉토리에 AndroidManifest.xml 파일을 새로 작성하면 된다. (일반적으로는 기존의 파일을 복사해와서 수정하고 싶은 부분만 수정하게 될 것이다)

2.3. Firebase 설정 파일 분리

일반적으로 Firebase 설정 파일인 google-services.jsonandroid/app 디렉토리에 위치하게 된다. 하지만 환경별로 분리해주고 싶을 경우, 2.2. 와 동일한 방법으로 환경별로 각각 파일을 만들어주어야 한다.

  • android/app/google-services.json: 환경별 Firebase 설정 파일이 없을 경우 일괄적으로 적용되는 설정 파일
  • android/app/src/ENV_NAME/google-services.json: ENV_NAME 환경에만 적용되는 Firebase 설정 파일

2.4. 실행/빌드 명령어 작성

환경별 실행/빌드 명령어를 package.json 파일에 작성해두자.

{
  // ...
  "scripts": {
    // ...
    "android": "ENVFILE=.env react-native run-android --appIdSuffix=dev",
    // 아래 주석 처리된 내용은 2.4.1. 항목 참고.
    // "android-dev": "ENVFILE=.env.dev react-native run-android --variant=devDebug --appIdSuffix=dev",
    // "android-stage": "ENVFILE=.env.stage react-native run-android --variant=stageDebug --appIdSuffix=test",
    "android-dev": "ENVFILE=.env.dev react-native run-android",
    "android-stage": "ENVFILE=.env.stage react-native run-android",
    "build-android-dev": "cd ./android && ENVFILE=.env.dev ./gradlew assembleDevRelease && cd ../",
    "build-android-stage": "cd ./android && ENVFILE=.env.stage ./gradlew assembleStageRelease && cd ../",
    "build-android-production": "cd ./android && ENVFILE=.env.production ./gradlew assembleRelease && cd ../",
  }
}

이렇게 하면 커맨드라인에서 아래 같은 명령어들로 안드로이드앱을 실행 및 빌드하는 것이 가능해진다.

yarn android                  # 로컬/개발 환경 실행
yarn android-dev              # dev/개발 환경 실행
yarn android-stage            # stage/개발 환경 실행
yarn build-android-dev        # dev 빌드
yarn build-android-stage      # stage 빌드
yarn build-android-production # production 빌드

2.4.1. react-native run-android --variant=VAR_NAME 이슈

현재 react-native run-android --variant=VAR_NAME 명령어를 사용하면 metro 번들러가 정상적으로 동작하지 않아 에러가 발생한다. 의도적인 것인지 버그인지는 불명, 해결 방법도 불명이다.

react-native bundle 명령어를 선행하면 에러는 제거할 수 있지만 hot-reload 가 동작하지 않는다. (이 말은 js/ts 파일이 수정될 때마다 다시 빌드해줘야 함을 의미한다.)

따라서 react-native run-android 명령어 사용 시에는 variant 옵션을 사용하지 않기를 권한다. ENVFILE 옵션만 써줘도 테스트는 충분히 할 수 있다.

3. iOS 설정

3.1. Configuration 추가

PROJECT - [Info] - [Configuration] 에서 [+] 버튼 클릭한다.

[Duplicate "Debug" Configuration] ("Release" 도 크게 상관 없음) 눌러서 필요한 만큼 설정을 추가하자.

3.2. Scheme 추가

최상단 메뉴에서 [Product] - [Scheme] - [Edit Scheme] 을 선택한다. 이후 [Duplicate Scheme] 버튼으로 Scheme 추가한다.

환경 이름 설정 후 [Run], [Test], [Profile], [Analyze], [Archive] 탭의 Build Configuration 항목을 모두 변경하자.

여기서 Build Configuration 은 "3.1. Configuration 추가" 에서 추가했던 항목들을 선택할 수 있다. 되도록 Scheme과 Build Configuration은 1:1로 매칭해주는 것이 좋다. (반드시 그래야 하는 것은 아니고, 편의성을 위해서다)

> Scheme - Build Configuration 설정 예
SchemeBuild Configuration
my-project(기본값 유지)
my-project-devDev
my-project-stageStage
my-project-productionProduction

3.2.1. Scheme 의 Pre-actions, Post-actions 적용

iOS 는 환경별 .env 파일 적용이 자동으로 이루어지지 않기 때문에, 별도의 선행 스크립트를 삽입해주어야 한다.

[Edit Scheme] 의 [Build] 탭에서 [Pre-actions] 선택해 스크립트를 작성하자.

echo ".env.ENVNAME" > /tmp/envfile

[Post-actions] 에는 아래 스크립트를 삽입하자.

rm /tmp/envfile

3.3. Firebase 설정 파일 관련 설정 추가

빌드될 때마다 각 환경에 맞는 firebase 설정 파일을 사용하게 하는 설정도 추가할 것이다.

일단 ./ios/firebaseInfo 디렉토리 생성(디렉토리 이름은 이후 설정에서 일관되게 유지하기만 하면 바꿔도 상관 없다)해 환경별로 GoogleService-Info.plist를 작성하자. 아래처럼 파일이 추가될 것이다.

  • ./ios/firebaseInfo/GoogleService-Info-dev.plist
  • ./ios/firebaseInfo/GoogleService-Info-stage.plist
  • ./ios/firebaseInfo/GoogleService-Info.plist

TARGET - [Build Phases] 에서 [+] 버튼을 눌러서 스크립트 추가하자.

스크린샷에서는 스크립트 이름을 "Firebase GoogleService-Info.plist" 라고 작성했으나, 다른 이름을 써도 무방하다.

순서는 대략 "Link Binary With Libraries" 와 "Copy Bundle Resources" 사이로 했는데, 다른 위치에 있어도 정상 동작할 수도 있다.

스크립트는 아래 형식으로 작성한다.

PATH_TO_GOOGLE_PLISTS="${PROJECT_DIR}/firebaseInfo"
PATH_TO_PROJECT="${PROJECT_DIR}"

echo "CONFIGURATION: $CONFIGURATION"

case "${CONFIGURATION}" in

"Debug" )
                cp -r "$PATH_TO_GOOGLE_PLISTS/GoogleService-Info-dev.plist" "${PATH_TO_PROJECT}/GoogleService-Info.plist" ;;

           "Stage" )
                cp -r "$PATH_TO_GOOGLE_PLISTS/GoogleService-Info-stage.plist" "${PATH_TO_PROJECT}/GoogleService-Info.plist" ;;

           "Dev" )
                cp -r "$PATH_TO_GOOGLE_PLISTS/GoogleService-Info-dev.plist" "${PATH_TO_PROJECT}/GoogleService-Info.plist" ;;

           "Release" )
                cp -r "$PATH_TO_GOOGLE_PLISTS/GoogleService-Info.plist" "${PATH_TO_PROJECT}/GoogleService-Info.plist" ;;

            *)
                ;;
        esac

3.4. Package 이름 분리

TARGET - [Build Settings] - [Packaging] - [Product Bundle Identifier] 항목을 수정하자.

3.5. 앱 이름 분리

먼저 TARGET - [Build Settings] 에서 앱 이름으로 사용될 User-defined 값을 추가하자.

그 추가한 값을 Info.plist - [Bundle display name] 에 적용하면 된다.

3.6. 실행 및 빌드

이제 Xcode 에서 알맞는 Scheme 을 고른 뒤 [Run] 혹은 [Archive]를 하면 앱을 실행 혹은 빌드할 수 있다.

(커맨드라인에서 하는 방법은 찾고 있다.)

4. 이슈

  • 현재 iOS 빌드 시 GoogleService-Info.plist가 제대로 생성되지 않아 에러가 발생하는 현상이 있다. 이 경우 다시 빌드를 하면 문제 없이 된다. 원인 및 해결 방법은 찾는 중이다.
  • .env파일들을 어떻게 공유하는 게 좋을지 방법을 고민 중이다. 수정할 때마다 팀원들에게 넘겨주기에는 번거롭고 실수를 유발하기도 쉽다.