이 포스트는 하기 블로그를 토대로 작성 하였음을 알려드립니다.
📌 Fastlane을 통한 앱 자동 배포 - Fastlane을 사용해서 React native로 만든 앱을 자동으로 배포해 보자
📌 Fastlane에 필요한 각 플랫폼 별 .env, API Key, Certificates 등은 이 포스트 에서 확인 가능합니다.
이 전에 RN을 개발하면서 스토어에 배포까지 Xcode 와 Android Studio 에서 빌드하고 번들링하는 작업을 직접 수동으로 진행하고 업로드 하였다.
사실 익숙해지니 크게 불편하진 않았다. 다만, 굳이 쉽게 커맨드 한줄이면 모든 것을 자동으로 해주는 기능이 있음에도 불구하고 사용하지 않는다는 것은 개발자로서 성실해서가 아니라, 오히려 무지해서라는 답변이 가깝다.
금번엔 Fastlane 이라는 자동 배포를 도와주는 tool을 사용하여 구현부터 실 배포까지 진행했던 경험을 작성하고자 한다.
fastlane is the easiest way to automate beta deployments and releases for your iOS and Android apps. It handles all tedious tasks, like generating screenshots, dealing with code signing, and releasing your application.
위 설명과 같이 Fastlane 은 iOS와 안드로이드의 테스트용 배포 또는 릴리스용 배포를 간단하게 자동화해주는 툴이다. 배포뿐만 아니라, 스크린샷 생성, 코드 사이닝, 앱 스토어 등록 정보 등을 생성, 관리할 수 있다.
본인은 React Native 환경에서 두 플랫폼을 구현했지만, RN 뿐만아니라 iOS 및 Android Native에서도 구현이 가능하다는 점 참고 바란다. 이 포스트는 RN을 대상으로 설명한다.
본인은 home brew 를 통해 설치했다.
$ brew install fastlane
fastlane 자체가 Ruby 언어로 이루어져 있어서 Ruby의 패키지 매니저인 gem을 사용하여 설치하기도 한다.
$ sudo gem install fastlane -NV
본인은 초기화 단계부터 설정한 것이 아니기 때문에 초기화 단계는 위에 기재한 블로그 내용을 그대로 서술했음을 알린다.
다음 command로 fastlane 초기화를 실행한다.
$ cd ios
$ fastlane init
그 결과 아래와 같은 화면을 확인할 수 있다.
여기서 배포를 위한 선택지는 2번과 3번이다.
2. Automate eta distribution to TestFlight
3. Automate App Store distribution
2번은 TestFlight 에 배포를 위한 beta 설정이며, 3번은 실제 App Sotre 에 배포 할 release 설정이다.
상황에 맞는 설정을 진행한다.
(무엇을 선택하든 나중에 다시 파일을 수정하면 그에 맞게 설정할 수 있으니 큰 고민은 필요없다.)
다음은 본인의 iOS 프로젝트를 선택하는 화면이다.
tv 앱을 제작하는 것이 아니라면 iOS App 프로젝트인 2번을 선택한다.
다음은 Apple 로그인을 위한 화면이다.
iOS App 배포를 위해 사용되는 Apple store connect 의 이메일 주소를 하기 Apple ID Username:
부분에 기입한다.
이 후 사진은 첨부하진 않았지만, 2FA 또는 휴대폰 인증과 같이 이중 인증을 진행한다.
폴더 구조 및 구성 파일은 다음과 같다.
|- fastlane
||- Appfile
||- Fastfile
||- (Matchfile)
|- Gemfile
|- Gemfile.lock
fastlane
폴더 : fastlane의 설정 및 실행 파일들이 들어 있는 폴더Appfile
: bundle identifier, Apple ID, Team ID 등 필요한 정보들이 들어간다.Fastfile
: 실제 자동 배포를 도와줄 실행 명령어들을 담는 파일이다.Matchfile
: iOS는 배포를 위해 Provisining profile, Distribution 등 필요한 문서들이 필요하다. 이를 프로젝트 로컬 자체에서 관리하면서 다이렉트로 끌어다 사용하는 방식도 있지만, 보통 개발자가 많아지면 각자 로컬에서 따로 관리하기 때문에 싱크를 맞추기 어렵다. 이를 개선하기 위해 원격 저장소의 Private repo.에 필요한 파일들을 담아 당겨쓰는데 이를 관리하기 위한 파일이 바로 Matchfile
이다. 이는 이 포스트에서 자세하게 다루도록 하겠다.Gemfile
, Gemfile.lock
: Ruby 언어로 구성되어 있는 fastlane을 실행시키기 위한 라이브러리 설치 파일이다.📌 참고
앞서 언급했듯이 fastlane은 Ruby 언어로 이뤄져있다. 앞으로 서술할 문법들은 전부 Ruby인데, 우리의 목적은 Ruby 언어를 공부하는 것이 아니라 fastlane을 어떻게 사용하는 것이냐가 중요하기 때문에 따로 문법에 대한 설명은 생략하겠다.
하지만 해당 문법이 구글링 결과 대부분 개발자들이 국룰로 사용한다는 것은 명백하다.
fastlane/Fastfile
에 다음과 같이 작성한다.
default_platform(:ios)
platform :ios do
# 목적에 맞는 배포 version을 기입하는 함수
def updateVersion(options)
if options[:version]
version = options[:version]
else
version = prompt(text: "Enter the version type or specific version\n(major, minor, patch or 1.0.0): ")
end
re = /\d+.\d+.\d+/
versionNum = version[re, 0]
if (versionNum)
increment_version_number(
version_number: versionNum
)
elsif (version == 'major' || version == 'minor' || version == 'patch')
increment_version_number(
bump_type: version
)
else
UI.user_error!("[ERROR] Wrong version!")
end
end
# beta 버전 배포 자동화 구문
desc "Push a new beta build to TestFlight"
lane :beta do |options|
increment_version_number(xcodeproj: 'yourProjectName.xcodeproj')
updateVersion(options)
match(type: "appstore", force_for_new_devices: true)
sync_code_signing(type: "appstore", app_identifier: "your.bundle.identifier")
build_app(
workspace: "yourProjectName.xcworkspace",
scheme: "yourProjectName",
export_options: {
provisioningProfiles: {
"your.bundle.identifier" => "match AppStore your.bundle.identifier"
}
})
upload_to_testflight
# release 버전 배포 자동화 구문
desc "Push a new release build to the App Store"
lane :release do |options|
increment_build_number(xcodeproj: "yourProjectName.xcodeproj")
updateVersion(options)
match(type: "appstore", force_for_new_devices: true)
sync_code_signing(type: "appstore", app_identifier: "your.bundle.identifier")
build_app(workspace: "yourProjectName.xcworkspace", scheme: "yourProjectName")
upload_to_app_store(
force: true,
reject_if_possible: true,
skip_metadata: false,
skip_screenshots: true,
languages: ['en-US', 'ja','ko'],
release_notes: {
"default" => "bug fixed",
"en-US" => "bug fixed",
"ja" => "バグ修正",
"ko" => "버그 수정"
},
submit_for_review: true,
automatic_release: true,
submission_information: {
add_id_info_uses_idfa: true,
add_id_info_serves_ads: true,
add_id_info_tracks_install: true,
add_id_info_tracks_action: false,
add_id_info_limits_tracking: true,
export_compliance_encryption_updated: false,
}
)
end
end
iOS 앱 개발 및 배포를 위해서는 Certificate
와 Provisioning profile
이 필요하다.
Provisioning profile
의 APP ID
는 전부 동일하지만, 인증서 퍼블릭키 와 디바이스 정보 는 팀원마다 각각 다른 값을 가지고 있다.
즉, 모든 개발자가 각자 다른 배포 인증서와 Provisioning profile
을 발급 받아야 하며 이는 관리하는 측면에서 굉장히 까다롭고 동작 또한 제대로 안될 가능성이 크다. 또한, 배포 인증서를 발급받을 수 있는 개수의 한계도 존재하는 등 많은 문제점이 존재한다.
이를 위해 fastlane
은 match
를 지원한다.
match
란, 위 문제점에서 언급했듯이 팀원, 환경에 관계없이 자동으로 인증을 동기화 해주는 툴이다.
자세한 내용은 이 블로그를 참고 바란다.
iOS App store에 접속 가능한 ID/PW 및 APPLE_APPLICATION_SPECIFIC_PASSWORD 가 필요하다.
PROJECT/ios/fastlane
FASTLANE_USER="App Store ID(이메일 주소)"
FASTLANE_PASSWORD="비밀번호"
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD="앱 암호"
앱 암호 관련한 내용은 여기를 참고하면 된다.
본인은 firebase에 distribution을 진행하였기에 관련 플러그인을 설치해야 했다.
PROJECT/ios/fastlane
$ fastlane add_plugin firebase_app_distribution
다음 명령어를 통해 실제 fastlane을 실행시켜 보자.
$ cd ios
$ fastlane beta
엔터를 치면 다음과 같은 화면이 나온다.
Enter the version type or specific version
(major, minor, patch or 1.0.0):
major, minor, patch 중 하나를 택하거나 혹은 version 풀네임을 기입해도 된다.
본인은 2.0.0
을 입력하여 실제 v2.0.0을 배포하도록 하였다.
배포가 정상적으로 완료되면 다음과 같은 화면을 볼 수 있다.
📌 참고
Google Console 에서 Service Account를 생성하고 API Key를 발급받아 프로젝트 안에 삽입하는 과정이 필요하다.
이는 내용이 길기에 여기서 생략하고 대신 이 포스트를 참고하여 먼저 수행한 후 이어서 진행하면 된다.
다음 command로 fastlane 초기화를 실행한다.
$ cd android
$ fastlane init
커맨드를 실행하면 다음 화면을 볼 수 있다.
Package Name
으로 Android 프로젝트의 Package Name을 입력한다.
이 전에 다운 받은 API Key를 담은 JSON 파일을 어디다가 저장할 것인지 묻는다.
가장 먼저 JSON 파일을 원하는 android 폴더 내에 집어 넣고, 그 경로를 Path to the json secret file:
에 적어주면 된다.
다음은 안드로이드를 배포할 때, 등록한 스토어 정보(metadata)를 다운로드할 지 물어보는 과정이다.
그 후로 Continue by pressing Enter
를 입력하는 화면이 나온다.
Enter
키를 눌러 진행하여 설정을 마친다.
iOS 와 동일하므로 이 부분에 대한 설명을 생략한다.
|- fastlane
||- Appfile
||- Fastfile
|- Gemfile
|- Gemfile.lock
fastlane/Fastfile
파일 안에 다음과 같이 작성한다.
default_platform(:android)
platform :android do
# Version Code를 자동으로 +1 올려주는 함수
def increment_version_code()
path = '../app/build.gradle'
re = /versionCode\s+(\d+)/
s = File.read(path)
versionCode = s[re, 1].to_i
s[re, 1] = (versionCode + 1).to_s
f = File.new(path, 'w')
f.write(s)
f.close
end
# 목적에 맞는 배포 Version을 기입하는 함수
def increment_version_number(bump_type: nil, version_number: nil)
path = '../app/build.gradle'
re = /versionName\s+("\d+.\d+.\d+")/
s = File.read(path)
versionName = s[re, 1].gsub!('"','').split('.')
major = versionName[0].to_i
minor = versionName[1].to_i
patch = versionName[2].to_i
if (bump_type == 'major')
major += 1
minor = 0
patch = 0
elsif (bump_type == 'minor')
minor += 1
patch = 0
elsif (bump_type == 'patch')
patch += 1
end
if(version_number)
s[re, 1] = "\"#{version_number}\""
else
s[re, 1] = "\"#{major}.#{minor}.#{patch}\""
end
f = File.new(path, 'w')
f.write(s)
f.close
increment_version_code()
end
# 최종적으로 원하는 Version을 입력 받아 처리해주는 함수
def updateVersion(options)
if options[:version]
version = options[:version]
else
version = prompt(text: "Enter the version type or specific version\n(major, minor, patch or 1.0.0): ")
end
re = /\d+.\d+.\d+/
versionNum = version[re, 0]
if (versionNum)
increment_version_number(
version_number: versionNum
)
elsif (version == 'major' || version == 'minor' || version == 'patch')
increment_version_number(
bump_type: version
)
else
UI.user_error!("[ERROR] Wrong version!!!!!!")
end
end
# beta 버전 배포 자동화 구문
desc "Submit a new Beta Build to Crashlytics Beta"
lane :beta do |options|
updateVersion(options)
gradle(task: "clean bundleRelease")
upload_to_play_store(
skip_upload_metadata: true,
skip_upload_changelogs: true,
skip_upload_screenshots: true,
skip_upload_images: true,
skip_upload_apk: true,
track: 'internal'
)
end
# release 버전 배포 자동화 구문
desc "Deploy a new version to the Google Play"
lane :release do |options|
updateVersion(options)
gradle(task: "clean bundleRelease")
upload_to_play_store(
skip_upload_metadata: true,
skip_upload_changelogs: true,
skip_upload_screenshots: true,
skip_upload_images: true,
skip_upload_apk: true
)
end
end
다음 명령어를 통해 실제 fastlane을 실행시켜 보자.
$ cd android
$ fastlane beta
엔터를 치면 다음과 같은 화면이 나온다.
Enter the version type or specific version
(major, minor, patch or 1.0.0):
major, minor, patch 중 하나를 택하거나 혹은 version 풀네임을 기입해도 된다.
본인은 2.0.0
을 입력하여 실제 v2.0.0을 배포하도록 하였다.
배포가 정상적으로 완료되면 다음과 같은 화면을 볼 수 있다.
스크립트 파일만 실행하여 모든 것을 커맨드 하나로 끝내는 작업을 만들어보자.
deployment-ios.sh
파일을 생성하고 다음 내용을 넣자.
echo "*** Publishing to Play store***"
cd ios
fastlane beta
실행은 터미널에 ./deployment-ios.sh
를 치면 된다.
deployment-android.sh
파일을 생성하고 다음 내용을 넣자.
echo "*** Publishing to Play store***"
cd android
./gradlew clean
cd ../
yarn bundle:android
rm -rf android/app/src/main/res/drawable-*
rm -rf android/app/src/main/res/raw
cd android
fastlane beta
동일하게 실행은 터미널에 ./deployment-android.sh
를 치면 된다.
fastlane 을 통해 배포를 하면 그에 따른 불필요한 파일들이 생성된다. git
에서 관리하지 않기 위해 .gitignore
파일에 아래와 같은 내용을 추가한다.
#fastlane
ios/*.mobileprovision
ios/*.cer
ios/*.dSYM.zip
android/fastlane/README.md
ios/fastlane/README.md
좋은 글 감사합니다. 자주 올게요 :)