React Native) 개발용 Dev 앱 분리하기 (iOS)

2ast·2024년 3월 30일
1

개발용 앱을 분리하는 이유

앱 개발을 하다보면 실제 디바이스에 앱을 설치해서 테스트해 볼 일이 생각보다 많다는 걸 알 수 있다. 그럴 때마다 기존에 설치되어 있는 프로덕션 버전의 앱을 삭제하고 테스트용 앱을 다시 깔아야하는 게 여간 불편한게 아니다. 실제로 회사 동료들이 다같이 QA를 해야할 일이 생기면, 기존앱을 지우고 다시 깔아야하는 수고로움은 둘째치더라도 프로덕션 앱 위에 테스트 앱을 덮어 쓰면서 토큰이 꼬인다거나, 테스트 앱을 다운로드 받은걸 잊고 지내다가 이후 앱이 이상하다며 제보가 오기도 한다. 이런 이유로, 보통 실제 프로덕션 빌드와 개발용 빌드의 환경을 분리해서 별개의 앱으로 인식하게끔 하는 작업을 하고는 하는데, 이 방법을 정리해보고자 한다.

개발용 앱을 분리해보자

Staging configuration 추가하기

이제부터 실제 배포용 환경을 production, 개발용 환경을 staging이라고 부르겠다. 앱 분리를 위해 가장 먼저 할 일은, xcode 상에서 configuration을 추가해주는 일이다.
xcode의 project를 선택하고 info>configutation로 가보면, 기본적으로 Debug와 Release 환경이 있는걸 확인할 수 있다.

이제 여기서 하단 '+' 버튼을 누른 뒤, Duplicate "Debug" Configutation과 Duplicate "Relase" Configuration을 한번 씩 눌러준다. 그 후, Debug configutation은 각각 DebugStaging과 DebugProduction으로, Release configuration은 각각 ReleaseStaging과 ReleaseProduction으로 이름을 변경해준다.(이름은 예시일 뿐, Staging-Debug, Staging.Debug, Debug_Staging 등 마음에 드는 이름을 사용해도 좋다.)
여기서 주의할 점은 debug를 복사한 configuration은 반드시 DebugStaging 또는, DebugProduction으로 명명해야한다는 점이다. Debug를 복사해놓고 ReleaseProduction이라고 이름지어 봐야, 내부적으로 build settings는 모두 debug 셋팅이 적용되기 때문이다.

Bundle Identifier 분기하기

애플은 앱을 구분할 때 Bundle Identifier라는 식별자를 사용한다. 즉, Bundle Id가 다르면 완전 별개의 앱으로 인식하게 된다. 우리가 가장 먼저할 일은, 각 빌드 환경에 따라 Bundle Identifier를 동적으로 설정해주는 작업이다.
먼저 Build Settings 탭의 '+' 버튼을 눌러 User-Defined Setting을 추가해준다.

User-Defined Setting은 개발자가 자유롭게 xcode 내부에서 사용할 수 있는 변수 필드를 정의할 수 있게 해준다. 나는 그렇게 만든 필드의 이름을 BUNDLE_IDENTIFIER라고 명명한 뒤, 그 값을 각 환경에 맞춰 할당해 주었다. 만약 본래 앱의 bundle id가 com.sample이라면 뒤에 .staging suffix를 붙이는 식으로 정의해줬다.

이제 이 값을 Build Settings > Packaging 경로에 있는 Product Bundle Identifier에 바인딩해주기만 하면 끝이다.

Display Name 분기하기

이번에는 스마트폰에서 앱을 구별하기 쉽도록 앱 이름을 다르게 설정해보려고 한다. 전체적인 절차는 Bundle Identifier를 설정해주는 과정과 동일하다. 우선 User-Defined Setting을 추가하고, DISPLAY_NAME이라고 명명해주었다.

이번에는 이 값을 Info.plist 파일의 Bundle display name에 바인딩 해준다.

실제로 Display Name과 Bundle Identifier를 확인해보면 아래와 같이 각 configuration에 맞게 잘 설정 된 것을 확인할 수 있다.

Podfile 설정하기

cocoapod은 내부적으로 debug와 release 등 여러 모드를 가지고 있다. 만약 Configuration이 Debug나 Release같은 이름을 가지고 있다면 적절한 모드를 찾아 자동으로 셋팅을 해주지만, 우리는 이름을 변경했기 때문에, Podfile에 다음과 같은 형태로 코드를 추가하여 수동으로 매칭을 시켜줘야 한다.

project '[Project Name]', '[Configuration]' => :[Mode]

샘플 프로젝트의 케이스를 적용하면 결과물은 아래와 같다.

project 'SampleProject', 'DebugStaging' => :debug, 'DebugProduction' => :debug, 'ReleaseStaging' => :release, 'ReleaseProduction' => :release

Podfile을 변경했기 때문에 pod install 실행해주는 것을 잊지 말자.(만약 pod install에 실패한다면 flipper를 disabled 처리하고 시도해보면 될 수도 있다.)

Scheme 설정하기

마지막으로 Configuration에 맞춰 Build Scheme을 설정해주려고 한다. Build Scheme을 정의해두면 xcode 내부에서 configuration을 전환하는 작업 자체도 편리하지만, 외부에서 커맨드라인을 이용해 빌드할 때도 손쉽게 configuration에 접근할 수 있다.
Xcode 상단 Scheme 영역을 누르고 "Manage Schemes..." 메뉴를 연다음 project scheme을 복사해줄 것이다.

복사하는 방법은 scheme을 선택한 뒤, 하단 더보기 버튼을 눌러 Duplicate 해주면 된다.

반복 duplicate를 통해 총 4개의 동일한 Scheme이 만들어졌으면 이름을 각 Configuration대로 수정해주고, edit scheme 메뉴로 접근하여 build configuration을 지정해주면 된다.

이제 scheme을 전환하는 것만으로, 빌드하고 싶은 configuration을 선택할 수 있게 되었다.
관련해서 작은 팁을 주자면, 본래 react native에서 ios 빌드를 수행하는 명령어인 "react-native run-ios"는 프로젝트 이름과 동일한 scheme을 찾아서 실행하도록 구성되어 있다. 하지만, 이번에 scheme를 커스텀했기 때문에 이 명령어는 더이상 동작하지 않게 된다. 이럴 때는 '--scheme' 옵션을 통해 실행할 scheme를 지정해 주면 해결된다.

react-native run-ios --scheme DebugProduction

이제 staging과 production configuration으로 빌드를 실행해보면 다음과 같이 독립된 앱으로서 설치되는 것을 확인할 수 있다.

Advenced Step

개발용 앱 분리를 위한 기본 설정은 모두 끝이 났다. 하지만 본질적으로 별개의 앱을 추가로 만든 것이니만큼, firebase나 fastlane 등 서드파티 툴을 도입했다면 추가적인 설정을 해주어야한다. 이제부터는 여기에 대응하는 작업을 해보려고 한다.(만약 필요 없다면 이쯤에서 뒤로가기를 눌러도 된다.)

Firebase GoogleService-Info.plist 분기하기

firebase를 프로젝트에 도입하기 위해서는 firebase console에 앱을 추가하고, GoogleService-Info.plist라는 파일을 프로젝트에 추가해야한다. 문제는 개발용 앱의 경우, 기존 앱과는 전혀 다른 앱으로 취급되고 있기 때문에 GoogleServie-Info.plist 파일을 공유할 수 없다는 점이다. 즉, staging과 production용 plist 파일을 빌드타임에 적절하게 선택해서 정해진 경로에 배치해주는 작업이 필요하다.
우선 firebase console에 개발용 ios 앱을 추가해주고 GoogleService-Info.plist 파일을 다운로드 해준 뒤, 두개의 GoogleService-Info-staging.plist라고 파일명을 바꿔준다. 마찬가지로 기존 production용 plist 파일도 GoogleService-Info-prod.plist로 이름을 바꿔주었다.
두 개의 파일을 project/ios 경로에 Configuration이라는 폴더를 만들어 안쪽에 배치해주었고, xcode 내부에서 파일을 직접 추가해주어 프로젝트에서 plist 파일을 인식할 수 있게 해주었다.

준비가 끝났으면, Build Phase 탭으로 이동해 "+" 버튼을 누르고 새로운 script phase를 만들어 준다. 이 phase의 이름은 "Copy GoogleService-Info.plist"로 지었고, Copy Bundle Resources phase 위에 배치해주었다.

그런다음, 안쪽에 아래 스크립트를 붙여넣기 해주면 끝이다.

case "${CONFIGURATION}" in
    "DebugStaging"|"ReleaseStaging")
        cp "${SRCROOT}/Configuration/GoogleService-Info-staging.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
        ;;
    "DebugProduction"|"ReleaseProduction")
        cp "${SRCROOT}/Configuration/GoogleService-Info-prod.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
        ;;
esac

스크립트를 해석해보면, Configuration이 DebugStaging 또는 ReleaseStaging일 경우, Configuration > GoogleService-Info-staging.plist 파일을 복사해서 프로젝트 폴더 안쪽에 GoogleService-Info.plist라는 이름으로 붙여넣고, Configuration이 DebugProduction 또는, ReleaseProduction일 경우, Configuration > GoogleService-Info-prod.plist 파일을 복사해서 동일한 작업을 수행하라는 뜻이다. 이로써 빌드타임에 configuration에 맞는 plist 파일을 찾아 적절한 위치에 배치해주는 작업을 수행할 수 있게 되었다.

Fastlane Match Signing 대응하기

만약 Fastlane을 도입중이라면, 높은 확률로 match signing을 적용하고 있을 것이다. 하지만 여기서도 문제가 발생한다. 배포용 앱과 개발용 앱은 본질적으로 전혀 다른 앱이기 때문에 기존 match signing key로는 개발용 앱을 업로드할 수 없기 때문이다.
가장 먼저 해야할 일은 개발용 앱을 아카이브하여 앱스토어에 업로드하는 작업이다. bundle identifier를 분기하여 개발용 앱을 만들었다고해도 어디까지나 로컬에서의 이야기이고, 앱스토어는 이 개발용 앱의 존재를 모른다. 따라서 인증서와 profile 또한 발급할 수 없다. 물론 이 작업을 apple developer에서 수동으로 할 수도 있겠지만, 그냥 한번 빌드해서 app store에 업로드하는 것만으로 애플이 자동으로 등록해주므로 이 방법을 추천한다.
여기서 주의할 점은, Signing & Capabilities 탭에서 Automatically manage signing을 체크해줘야한다는 점이다. 보통 match signing을 적용할 경우 이 옵션을 체크 해제하고, match key로 수동 설정해주고는 하는데, 이 사실을 까먹고 그대로 빌드하면 singing 에러가 발생할 수 있다.

이렇게 한차례 업로드가 완료됐다면, 이제 앱스토어에 개발용 앱 등록이 됐을 것이다. 이제 match repo에 인증서와 profile을 추가하면 된다. appstore 인증서 기준, "fastlane match appstore -a dev_app_bundle_identifier" 명령어를 통해 개발용 앱의 profile을 match repo에 업로드해줄 수 있다.

fastlane match appstore -a com.sample.staging

이제 빌드 시점에 각 환경에 맞는 profile을 참조할 수 있도록 정의한 lane안쪽의 build_app 명령어를 수정해주면 된다.

 build_app(
 		...
        export_options:{
            provisioningProfiles: {
                 "com.sample.staging" => "match AppStore com.sample.staging",
            }
        }
     )
     
 ...
 
 build_app(
 		...
        export_options:{
            provisioningProfiles: {
                 "com.sample" => "match AppStore com.sample",
            }
        }
     )

Signing & Capabilities를 다시 수정해주는 것도 잊지 말자.

Android 개발용 앱 분리하기

2편에서 계속

profile
React-Native 개발블로그

2개의 댓글

comment-user-thumbnail
2024년 4월 2일

필요한 레퍼런스였는데 감사합니다~

1개의 답글