React Native Fastlane으로 자동 배포하기

김지섭·2023년 9월 12일
0

Fastlane이란?

Fastlane은 모바일 앱 개발의 여러 빌드 및 배포 과정을 자동화하기 위한 도구입니다. iOS와 Android 플랫폼 모두를 지원하며, 여러가지 "lanes"라고 불리는 스크립트를 통해 테스트 자동화, 빌드 생성, 메타데이터 관리, 앱 스토어 배포 등을 손쉽게 처리할 수 있습니다.

Fastlane은 다음과 같은 기능을 제공합니다:

  1. Snapshot: 앱의 스크린샷을 자동으로 생성해줍니다.
  2. Scan: 앱의 테스트를 실행하고 결과를 취합합니다.
  3. Gym: iOS 앱을 빌드합니다.
  4. Match: 코드 서명 자산을 안전하게 저장하고 공유합니다.
  5. Deliver: iOS 앱을 TestFlight나 App Store에 배포합니다.
  6. Supply: Android 앱을 Google Play Store에 배포합니다.

이 외에도 Fastlane은 플러그인 아키텍처를 지원하여 커뮤니티에서 다양한 플러그인을 개발하고 공유하고 있습니다.

이 글에서는 Fastlane의 Gym, Deliver, Supply 를 통해서 React Native 프로젝트를 Android, iOS 각각 빌드하여 Beta 트랙에 배포하는 방법을 설명합니다.

Fastlane 설치

아래 명령어를 통해서 Fastlane을 설치합니다. MacOS 기준으로 작성되었습니다.

# Using RubyGems
sudo gem install fastlane -NV

# Using Homebrew
brew install fastlane

.gitignore

.gitignore

*.mobileprovision
*.cer
*.dSYM.zip
*.p12
*.certSigningRequest
.env
pc-api-*.json

iOS

API 키 생성

  1. https://appstoreconnect.apple.com 에 로그인합니다.
  2. 사용자 및 액세스 를 클릭합니다
  3. 탭을 클릭합니다
  4. Issuer ID 를 기록합니다
  5. API 키를 생성합니다
    이름: Fastlane
    엑세스: 제품 개발
  6. 키 ID 를 기록합니다
  7. 다운로드 버튼을 눌러 API 키 파일를 다운로드합니다.

위 과정을 완료하면 Issuer ID, 키 ID, API 키 파일 을 가져올 수 있습니다.

위 내용을 .env에 기록합니다.

./ios/fastlane/.env

APP_STORE_CONNECT_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
APP_STORE_CONNECT_API_KEY_ID=xxxxxxxxxx
APP_STORE_CONNECT_API_KEY_CONTENT="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"

APP_STORE_CONNECT_API_KEY_CONTENT\n으로 구분하여 작성되어야 합니다.

Fastlane 초기화

cd ./ios

fastlane init
> 2

이어서 로그인을 진행하고, 팀이 여러개 있다면, App Store Connect teams 및 Developer Portal 선택 작업을 진행하게 됩니다.

설치를 마치면 기본적으로 다음과 같은 파일이 생깁니다.

./ios/fastlane/Fastfile

default_platform(:ios)

platform :ios do
  desc "Push a new beta build to TestFlight"
  lane :beta do
    increment_build_number(xcodeproj: "MyApp.xcodeproj")
    build_app(workspace: "MyApp.xcworkspace", scheme: "MyApp")
    upload_to_testflight
  end
end

기본 생성 Fastfile에는 다음과 같은 문제점이 있습니다.
1. 아카이브(archive)는 성공했지만, 프로비저닝 프로파일(provisioning profile) 매핑이 제공되지 않음
2. 사용자의 Apple 계정 로그인 상태에 따라서 실패할 수 있음
3. Marketing 버전을 설정할 수 없음

위 문제들을 해결하기 위해서 다음 단계를 진행합니다.

Fastfile 수정

./ios/fastlane/Fastfile

require 'dotenv'
Dotenv.load

default_platform(:ios)

platform :ios do
  desc "Push a new beta build to TestFlight"

  def set_marketing_version(options)
    return unless options[:version]

    version = options[:version]
    re = /\d+\.\d+\.\d+/
    versionNum = version[re, 0]

    if (versionNum)
      increment_version_number(
        version_number: versionNum
      )
    else
      UI.user_error!("[ERROR] Wrong version!!!!!!")
    end
  end

  lane :beta do |options|
    cert
    sigh(force: true)

    increment_build_number(xcodeproj: "SmartParking.xcodeproj")
    set_marketing_version(options)

    build_app(workspace: "SmartParking.xcworkspace", scheme: "SmartParking")
    api_key = app_store_connect_api_key(
      key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
      issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
      key_content: ENV["APP_STORE_CONNECT_API_KEY_CONTENT"],
      duration: 1200,
      in_house: false
    )
    upload_to_testflight(
      api_key: api_key
    )
  end
end

Fastlane 실행

cd ./ios

fastlane beta version:1.0.0
+------+---------------------------+-------------+
|                fastlane summary                |
+------+---------------------------+-------------+
| Step | Action                    | Time (in s) |
+------+---------------------------+-------------+
| 1    | default_platform          | 0           |
| 2    | cert                      | 2           |
| 3    | sigh                      | 4           |
| 4    | increment_build_number    | 0           |
| 5    | increment_version_number  | 0           |
| 6    | build_app                 | 207         |
| 7    | app_store_connect_api_key | 0           |
| 8    | upload_to_testflight      | 329         |
+------+---------------------------+-------------+

[14:47:13]: fastlane.tools just saved you 9 minutes! 🎉

Android

keystore 설정

./android/local.properties

...
storeFile=/Users/user/keystore/MyApp.KEYSTORE
storePassword=
keyAlias=
keyPassword=

./android/app/build.gradle

...
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

android {
	signingConfigs {
        Properties properties = new Properties()
        properties.load(new FileInputStream("$project.rootDir/local.properties"))
        properties.each { prop ->
            project.ext.set(prop.key, prop.value)
        }
    	debug {
            ...
        }
        release {
            storeFile file(properties['keystore'])
            keyPassword properties['key_password']
            keyAlias properties['key_alias']
            storePassword properties['store_password']
        }
    }
     buildTypes {
     	...
        release {
        	...
            signingConfig signingConfigs.release
        }
    }
}

Api 키 생성

  1. https://play.google.com/apps/publish 로그인
  2. pc-api-xxx.json 파일을 ./android/ 경로에 복사

Fastlane 초기화

cd ./ios

fastlane init

> Path to the json secret file: pc-api-xxx.json
> Download existing metadata and setup metadata management? (y/n) n

설치를 마치면 기본적으로 다음과 같은 파일이 생깁니다

./android/fastlane/Fastfile

default_platform(:android)

platform :android do
  desc "Runs all the tests"
  lane :test do
    gradle(task: "test")
  end

  desc "Submit a new Beta Build to Crashlytics Beta"
  lane :beta do
    gradle(task: "clean assembleRelease")
    crashlytics
  
    # sh "your_script.sh"
    # You can also use other beta testing services here
  end

  desc "Deploy a new version to the Google Play"
  lane :deploy do
    gradle(task: "clean assembleRelease")
    upload_to_play_store
  end
end

Fastfile 수정

위 파일을 다음과 같이 수정합니다.

./android/fastlane/Fastfile

require 'dotenv'
Dotenv.load

default_platform(:android)

platform :android do
  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

  def set_version_name(options)
    return unless options[:version]

    version = options[:version]
    path = '../app/build.gradle'
    re = /versionName\s+("\d+.\d+.\d+")/
    s = File.read(path)

    if version
      s[re, 1] = "\"#{version}\""

      f = File.new(path, 'w')
      f.write(s)
      f.close
    end
  end


  desc "Submit a new Beta Build to Crashlytics Beta"
  lane :beta do |options|
    increment_version_code()
    set_version_name(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
end

Fastlane 실행

cd ./android

fastlane beta version:1.0.0
[19:32:48]: Successfully finished the upload to Google Play

+------+----------------------+-------------+
|             fastlane summary              |
+------+----------------------+-------------+
| Step | Action               | Time (in s) |
+------+----------------------+-------------+
| 1    | default_platform     | 0           |
| 2    | clean bundleRelease  | 117         |
| 3    | upload_to_play_store | 38          |
+------+----------------------+-------------+

[19:32:48]: fastlane.tools finished successfully 🎉

통합

./scripts/fastlane-beta.js

const { execSync } = require('child_process');
const packageJson = require('../package.json');

const version = packageJson.version;

const platform = process.argv[2]; // android or ios

if (platform === 'android' || platform === 'ios') {
  const command = `cd ./${platform} && bundle exec fastlane beta version:${version}`;
  execSync(command, { stdio: 'inherit' });
} else {
  console.error("Invalid platform. Use 'android' or 'ios'");
}

./package.json

{
  "version": "0.0.1",
  ...
  "scripts": {
    "beta:android": "node ./scripts/fastlane-beta.js android",
    "beta:ios": "node ./scripts/fastlane-beta.js ios",
    "beta": "yarn beta:android && yarn beta:ios"
  }
}

실행

package.json 의 version 수정 후

yarn beta

Trouble Shooting

[Android] Unsupported class file major version 64

Exit status of command '/Users/kimjisub/GitHub/user-app/android/gradlew clean bundleRelease -p .' was 1 instead of 0.

FAILURE: Build failed with an exception.

* What went wrong:
Could not open settings generic class cache for settings file '/Users/kimjisub/GitHub/user-app/android/settings.gradle' (/Users/kimjisub/.gradle/caches/8.0.1/scripts/cxlxtcmt173waj7d1c80a10te).
> BUG! exception in phase 'semantic analysis' in source unit '_BuildScript_' Unsupported class file major version 64

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 362ms

위의 경우 Java를 17버전으로 내리고 다시 실행하면 된다.

[Android] Google Api Error: Invalid request - Package not found: com.example.app.

PlayStore에 아직 앱을 올린적이 없어서, 해당 패키지명이 등록되지 않아서 발생하는 오류이다.
./android/app/build/outputs/bundle/release/app-release.aab 파일을 최초 1번 PlayStore에 업로드하면 그 이후부터는 문제없이 업로드가 된다.

참고 문헌

https://dev-yakuza.posstree.com/ko/react-native/fastlane

이 문서는 대부분 위 문헌을 참고하였으며, 실제로 설정을 하면서 필요했던 추가적인 내용을 덧대고, 사용하지 않는 부분을 가감하여 작성하였습니다.

0개의 댓글

관련 채용 정보