웹 개발에서 하이브리드 앱 개발로 첫 입사하여 가장 먼저 한 일은 개발/테스트/배포 환경의 정리였다.
입사 당시엔 앱이 출시하기 전이었기에 앱을 각각 testflight와 playconsole에 올려 테스트했으며 환경변수의 설정도, 코드푸시의 반영도 모두 수동으로 진행하고 있었다.
이 부분을 정립하고 자동화하는 것이 첫번째 목표였다.
어플리케이션 서비스의 개발과 출시에 대해 생각해보자. 먼저 우리는 앱을 로컬에서 개발한다. 개발된 앱은 QA를 진행한 후 각각의 앱 퍼블리싱(앱스토어, 플레이스토어)에 배포되어야 할 것이다.
그럼 적어도 우리는 3개의 stage가 개발에 요구된다. 개발을 위한 stage, QA를 위한 stage, 실제 프로덕션 stage.
이와 관련된 시스템 구축은 단순히 환경의 분리에서 그치지 않고 stage를 어떻게 구별할 것이고, 서버는 어떻게 분할할 것이며, git 관리나 버전관리는 어떤 시스템을 구축할 것인가와 같이 많은 부분을 포함하여 엮여있다.
이 과정을 어떻게 나눌것인가는 사실 굉장히 어렵고 복잡한 내용이다. 정답은 없고 현실적인 부분을 고려해야하며 절대 기획한 대로 돌아가지 않는다. 실제로 주 천만뷰가 나오는 서비스의 추천 시스템의 개발이 QA는 커녕 코드리뷰조차 없이 master로 바로 붙는 상황도 본 입장으로써 적절한 상황에 맞춘 형태를 구축하는 것이 가장 중요할 것이다.
아래 환경의 분리는 만약 이러한 시스템을 구축하려 할 때 도움이 될 수 있으면 좋겠다는 마음으로 쓴 내용이다.
일단 이런 환경이 있다고 생각해보자. 개발용/테스트용 데브 서버가 있고 프로덕션에 연결된 라이브 서버가 있다. 개발이나 테스트때는 데브 서버에 연결되게끔 앱을 빌드하고 프로덕션앱은 라이브 서버에에 연결되게끔 빌드한다.
먼저 각각의 스토어에 대해 살펴보자.
AOS의 플레이스토어의 경우 플레이콘솔에 앱이 올라가고 해당 앱을 심사 후 플레이스토어에 배포된다.
IOS의 앱스토어의 경우 테스트 플라이트에 앱이 올라가고 해당 앱을 심사 후 앱스토어에 배포된다.
만약 릴리즈(서비스 출시)전이라면 플레이콘솔과 테스트플라이트를 앱 테스트 용도로도 사용할 수 있다. 플레이콘솔의 경우 alpha테스트 설정을 통해 일부 사용자에게 플레이스토어를 통한 테스트를 실행할 수 있고 테스트플라이트 또한 설정한 테스터에게 테스트플라이트 앱을 통한 테스트를 실행할 수 있다.
실제로 현 회사도 출시 전에 테스트를 위와 같이 진행하였다. 하지만 실제 릴리즈 후에는 생각을 달리 해봐야한다. 플레이콘솔과 테스트플라이트의 앱은 결국 프로덕션(각각의 스토어)으로 릴리즈되어 일반 사용자(유저)가 사용하게된다. 즉 라이브서버와 연결되어 있다는 의미이다.
라이브 서버와 연결된 앱에서 테스트를 하는것은 당연히 안되는 일이다. 가장 먼저 생각해볼만한 옵션은 데브 서버와 연결된 테스트앱을 플레이콘솔/테스트플라이트에 올려서 테스트하고 라이브 서버와 연결된 프로덕션앱을 다시 올려서 심사를 받고 배포할 수 있다.
실제로 해보면 크게 문제가 발생하진 않는다. 다만 몇가지 문제점아닌 문제점이 발생할 수 있다. 가장 먼저 올리는 빌드 넘버에 대한 정의, 설치에 대한 휴먼 에러등을 꼽을 수 있다.
앱은 버전 이외에 빌드넘버라고 하여 현 버전(1.0.0과 같은 버전)에서 개발자가 어떤 빌드인지 구분하기 위한 빌드넘버가 별도로 존재한다. 플레이콘솔과 테스트 플라이트는 빌드 넘버가 같은 앱은 배포할 수 없다. 운이 좋아 하나의 테스트앱, 프로덕션앱만 각각 올라갈 수 도 있지만 QA과정에서 몇번의 테스트앱이 올라갈 수 있고 이 과정에서 개발자들의 혼동은 생각보다 쉽게 일어난다. 테스트앱을 다운로드받는 테스터 입장에선 훨씬 복잡하게 느껴진다. "최신 앱으로 다운로드 받아주세요"라는 말이 그렇게 어려울 수 가 없다.
심사를 올릴 때도 여러앱이 올라가있는것은 긍정적이지 못한다. 실제로 테스트 앱을 심사를 올린 경우가 있었고 다행히도 배포 전 알아채고 재 심사를 받아서 다행이었지 만약 올라갔다면 생각만해도 끔찍한 상황이 발생했을 것이다.
가장 이상적인 것은 프로덕션 앱이 올라갈 곳과 테스트앱이 올라갈 곳이 아예 분리되는 것이다. 두번째 방법은 아예 플레이콘솔과 테스트플라이트에 테스트용 앱을 하나 더 만들어서 분리된 앱 배포환경을 이용할 수 있다. 2개의 앱은 아예 분리되어 있으므로 서로에게 영향을 줄 일이 없다. 테스터와 개발자들이 어떤 앱이 테스트앱인지 프로덕션 앱인지 조금 더 신경쓰면 된다.(보통 앱의 이름을 아예 바꿔놓으므로 크게 헷갈리진 않는다, ex> 우리앱 테스트, 우리앱)
세번째 방법은 firebase와 같은 외부 배포 시스템을 활용할 수 있다. 현 회사와 바로 이전 회사에서 사용했던 방법이고 다운로드를 아예 다른 앱(테스트플라이트 <-> 파이어베이스)에서 받으니 혼동이 아예 없는 수준이다. 다만 firebase SDK 등록이나 firebase앱 등록 및 몇몇 요소를 신경써서 환경을 구축해 두어야 하고 특히 IOS의 경우 code signing 문제나 푸시알림(APNS) 설정 등 꽤나 번거로운 문제들이 많다.(ios앱을 올렸는데 알림이 안오거나 등등)
어떤 방식을 선택하는지는 자유이고 이외에 방법을 선택할 수 도 있다. 핵심은 개발/테스트 환경과 프로덕션환경을 어떻게 구별해서 최대한 문제를 없애고 공수를 줄여 테스트와 프로덕션배포를 할 수 있게끔 시스템을 구축하는 것이 중요하다.
자 이제 직접적으로 환경을 분리해보자. 일단 개발환경 / 테스트 환경 / 프로덕션 환경에서 구별되어야 할것이 무엇인지 고려해보자.
일단 첫번째로 연결된 서버가 달라야 할것이다. 테스트 서버, 개발 서버가 통합되있던 분리되어 있던 개발앱, 테스트앱과 프로덕션 앱은 각각 데브 서버, 라이브 서버와 연결되게끔 빌드해야한다.
두번째로 앱의 이름(id)이다. 디바이스(핸드폰)는 앱 id를 통해 해당 앱이 동일한 앱인지 아닌지를 판단한다. android는 build.gradle
에, ios는 project.pbxproj
에 정의되어 있는데, 테스트앱과 프로덕션앱이 같은 id, 같은 이름을 가지게 되면 혼동이 발생할 것이다.
세번째로 이벤트 트래킹 툴(Event Tracker)가 붙어 있다면 데브환경에선 해당 트래커가 동작하지 않도록 해야할 것이다. 개발앱에서 테스트하며 날아간 이벤트들은 특히 앱 출시 초기에 정성적 평가에 큰 문제를 가져올 것이다.
네번째로 테스트앱에만 포함되어야 하는 기능이 있을 수 있다. 예를 들면 계정의 등급에 따라 다른 화면을 보여줘야할 때 DB에서 일일히 해당 계정의 등급을 바꿔가며 테스트하기 보다는 아예 계정의 등급을 바꿀수 있는 API를 파고 클라이언트(앱)에서 이를 사용할 수 있게끔 하는게 좋을 것이지만 이런 기능은 프로덕션앱에는 절대 올라가선 안되는 기능이다. 이런 부분들을 구별할 수 있어야 한다.
그 외에 remote config를 통해 a/b test등을 실행하는 요소는 개발앱은 받아선 안될테고 캐시 제한을 푼다거나 개발앱과 라이브앱에서 다르게 동작해야하는 다양한 요소가 있을 수 있다.
컴퓨터는 어떤 프로세스를 생성할 때 참고 할 동적인 변수를 환경변수(environment variable)라 칭하고 사용한다. 우리가 앱을 빌드할 때도 이러한 변수들을 참고하는데, 이를 개발 빌드와 프로덕션 빌드 때 다르게 설정할 수 있다면 두 앱의 설정을 바꿀 수 있을 것이다.
이러한 설정을 설정파일(configuration)을 통해 설정할 수 있다. 패키지 설치를 위한 package.json이나 circleci빌드 설정을 위한 config.yml이나 bash의 사용 변수를 설정하는 .bashrc 파일 등 프로세스를 사용할 때 참고하는 파일을 모두 설정파일이라 할 수 있다.
react-native-config는 react-native의 js환경에서 환경변수를 손쉽게 사용할 수 있도록 돕는다.
react-native-config는 프로젝트 루트에 설정된 .env
파일을 참고하여 빌드 타이밍에 환경변수를 설정하고 이를 소스코드 내에서 사용할 수 있게끔 한다. 아래는 .env
와 tsx파일 내에서 react-native-config를 사용하는 예시이다.
App.tsx
최종적으로 우리가 하려는 것은 환경에 따라 앱의 설정(.env
)이 분리되는 것이다. 먼저 안드로이드를 살펴보자.
안드로이드의 buildtool은 Gradle이다. Gradle은 설정된 build.gradle
을 참고하여 앱을 빌드하는데, 이 때의 설정에 따라 변형 구성을 취할 수 있다.
어플리케이션 레벨의 build.gradle
(app/build.gradle
)을 살펴보면 buildType과 productFlavors를 설정하여 변형된 빌드를 구축할 수 있다.
앞서 말했듯이 우리는 빌드를 개발용(dev), 출시용(live)으로 나누고 싶은 상황이다. build.gradle
의 buildTypes에서 이 두가지 설정을 나눠보자.
그리고 각각의 환경을 개발, 테스트, 프로덕션으로 나누어 활용하려 하고 이를 build.gradle
의 ProductFlavors로 설정할 수 있다.
위와 같이 구성하면 차후 빌드 커맨드에서 아래와 같이 나누어 빌드할 수 있다.
꼭 위의 환경을 다 사용할 필요는 없다. 현 회사에선 위 환경 중 devdebug, testrelese, prodrelease의 3개 환경만 사용되고 특수한 경우(라이브 환경에 꼭 연결하여 테스트해야하는 경우엔 proddebug를 사용)에만 다른 환경을 사용한다. 자세한 빌드환경 분리를 적용하고 싶으면 아래의 공식문서를 참고하자.
이제 실제로 위에 언급한 환경에서 분리되어 사용될 설정을 진행해보자.
먼저 앱의 바리언트(variant)에 따라 참조할 환경 변수 파일을 분리하여 빌드 환경따라 다른 변수를 참조하도록 해보자. .env.dev
, .env.test
, .env.prod
로 나누어 생성한다.
build.gradle
의 최 상단에 참조할 환경 변수 파일을 선언하고, react-native-config를 빌드레벨에서 사용할 수 있도록 설정한다.
위 설정대로 빌드하면 각각의 값이 적용되어 화면에 표기괸다.
이번엔 앱의 아이디와 이름을 분리해보자. 앱의 아이디는 build.gradle
의 defaultConfig에 applicationId로 설정되어있다. 지금은 어떤 빌드환경을 사용하더라도 모든앱의 아이디가 동일하므로 앱이 구별되지 못한다. 이를 위해 안드로이드는 앱에 suffixId를 제공한다. productFlavor에 applicationSuffixId를 설정하여 앱을 구별시킬 수 있다.
위와 같이 설정하고 실행하면 각 앱이 com.app.dev, com.app.test, com.app으로 아이디가 설정되어 구별된다. 즉 기기에 세개의 앱을 별도로 설치할 수 있다.
여기까지 진행하면 세개의 다른 환경의 앱이 설치되지만 이름이 모두 동일하여 디바이스 화면에서 구별이 어려우므로 이름을 다르게 설정하도록 해보자. 이름은 AndroidManifest.xml
에 application 레벨에 설정된 android:label의 설정에 따라 변경된다. 이 설정에서 환경 변수를 사용할 수 있지만 이번엔 내부 string.xml
을 사용해서 처리해보자. string.xml
은 안드로이드에서 참고하는 문자열 파일이다. 이 파일에 두개의 변수를 선언해보자.
이번엔 build.gradle
에 productFlavors의 manifestPlaceholders를 설정하자.
마지막으로 AndroidManifest.xml
에서 설정한 appName을 사용할 수 있도록 변경하자.
이제 각 빌드의 이름이 다르게 생성될 것이다.
IOS는 Xcode가 빌드를 담당한다. Xcode는 빌드 타겟(release, debug)과 scheme에 따라 빌드를 구별한다.
먼저 Xcode에서 사용하는 빈 xcode config 파일을 생성한다. 이 파일은 차후 schema에서 설정할 run script에서 우리가 사용할 env를 적용할 코드를 넣어둔다.
[이미지]
생성한 Config.xcconfig 파일하단에 아래 코드를 추가한다.
빌드 타입인 debug / release에서 해당 Config 파일을 사용하도록 설정한다. info -> configuration에서 설정할 수 있다.
[이미지]
우리가 소위 react-native run-ios로 구동하는 빌드가 debug, 실제로 어플리케이션 레벨로 올라가는 빌드가 release라 생각하면 된다.
이후 우리가 설정한 .env를 적용하기 위한 scheme를 설정한다. 앞서 언급한대로 dev / test / prod schema를 생성한다. 상단에서 Product -> Scheme -> New Scheme으로 생성할 수 있다.
생성된 scheme 설정에서 우리가 사용할 .env를 프로젝트 내 env파일경로로 설정하고 react-native-config를 실행하는 코드를 추가한다.
Edit Scheme -> Build -> Pre-action 에 새로운 run script를 생성함으로써 적용할 수 있다. test의 경우 아래 코드를 run script에 작성한다. 반약 dev라면 .env.dev로 변경하면 된다.
TODO: bundle identifier 내용 추가
이제 Xcode에서 앱을 run 함으로써 앱을 구동할 수 있다.
위 내용까지 진행하면 react-native 커맨드를 통한 실행은 문제없이 진행되지만 결국 최종적으로는 앱이 각각의 firebase 및 스토어에 업로드 되어야 한다.
그러기 위해선 release 빌드를 통해 생성되는 abb(혹은 apk)와 ipa 파일이 요구된다. 안드로이드의 경우 gradlew를 통해 빌드하여 apk, abb 수 있고, ios의 경우 xcode에서 sheme의 build config를 release로 변경하여 빌드함으로써 ipa파일을 생성할 수 있다.
물론 양쪽 다 firebase, playconsole, testflight에 업로드하기 위한 사전 작업들이 필요하다. 이를 먼저 알아보고 fastlane을 통해 빌드 라인을 만드는 것까지 진행해보자.
우리는 각 앱의 test 단계 앱을 firebase에 올리기로 하였다. 따라서 firebase에 해당 앱을 등록할 필요가 있다. firebase에 가입 후 새로운 어플리케이션을 생성한다.
어플리케이션에서 새로운 안드로이드/ios 앱을 등록한다. 각각의 앱의 이름은 앱의 identifier와 동일해야 한다. 우리는 test앱에 suffix를 붙이므로 해당까지 포함한 앱의 이름으로 등록한다.
각 앱을 등록하면서 firebase sdk를 양쪽에 심어야하므로 해당 문서를따라 앱을 설정하도록 한다.
[firebase 등록 과정, android, ios]
fastlane은 앱의 빌드, 인증, 출시와 관련된 일련의 과정을 묶어 자동화해주는 프레임워크로 fastlane 파일을 설정해둠으로써 이후 빌드에 소요되는 시간적 비용을 최소화 시킬 수 있다.
먼저 fastlane을 설치해야한다. mac의 경우 아래 과정을 통해 설치를 하고 이외 os의 경우 공식 문서를 참고하자.
각각의 os레벨 디렉토리(./android, ./ios)에서 아래 커맨드로 fastlane을 생성하면 사용 준비가 끝난다.