[Flutter] Fastlane을 사용하여 배포 자동화하기

2023-03-18 hit count image

Fastlane을 사용하여 Flutter로 개발한 앱을 배포하는 방법에 대해서 알아보도록 하겠습니다.

개요

Flutter로 앱을 개발하였다면, 이렇게 개발한 앱을 사용자가 사용할 수 있게 배포해야 합니다. Flutter의 공식 문서에는 Flutter로 개발한 앱을 배포하는 방법에 대해서 설명하고 있습니다. 이를 참고하면 Flutter로 개발한 앱을 배포할 수 있습니다.

Fastlane은 네이티브로 개발한 앱뿐만아니라 하이브리드 앱(Flutter, React Native 등)도 쉽게 배포할 수 있게 도와주는 툴입니다.

이번 블로그 포스트에서는 Fastlane을 사용하여 Flutter로 개발한 앱을 배포하는 방법에 대해서 알아보겠습니다.

Fastlane

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와 안드로이드의 테스트용 배포 또는 릴리스용 배포를 간단하게 자동화해주는 툴입니다. 배포뿐만 아니라, 스크린샷 생성, 코드 사이닝, 앱 스토어 등록 정보 등을 생성, 관리할 수 있습니다.

이번 블로그 포스트에서는 스크린샷, 앱 스토어 등록 정보 등은 등록이 되어있다고 가정하고, 테스트용/릴리스용 배포를 자동화하는 부분만을 다룰 예정입니다.

스크린샷, 앱 스토어 등록 정보 생성 등, 다른 기능도 사용하고 싶은 분들은 공식 사이트를 참고하시기 바랍니다.

Flutter 공식 홈페이지에서도 CI/CD를 위해 Fastlane을 사용하는 방법에 대해서 안내하고 있으므로, 공식 문서도 참고해 보시기 바랍니다.

Fastlane 설치

Fastlane을 사용하여 Flutter로 개발한 앱을 배포하기 위해서는 Fastlane을 설치할 필요가 있습니다. 다음 명령어를 사용해서 Fastlane을 설치합니다.

# Using RubyGems
sudo gem install fastlane -NV

# Alternatively using Homebrew
brew cask install fastlane

공식 사이트에서는 Homebrew를 사용해서 설치하는 방법과 RubyGems을 사용해서 설치하는 방법을 안내하고 있습니다.

저는 처음에 Homebrew를 사용해서 설치하고, 테스트해봤지만 제대로 동작하지 않는 부분이 있었습니다. 따라서 RubyGems을 사용해서 설치하는 것을 권장합니다. 혹시 Homebrew로 제대로 동작하지 않는다면 RubyGems으로 다시 설치하신 후 시도해 보시기 바랍니다.

iOS

Flutter로 개발한 iOS 앱을 Fastlane을 사용해서 배포하는 방법에 대해서 알아봅시다.

iOS를 위한 Fastlane 초기화

아래에 명령어를 사용하여 iOS를 위한 Fastlane을 초기화 합니다.

cd ios
fastlane init

위에 명령어를 실행하면 아래와 같은 화면을 확인할 수 있습니다.

Fastlane을 사용한 Flutter 앱 자동 배포 - iOS 초기화

이번 블로그 포스트에서는 Testflight용과 앱 스토어에 배포를 위한 설정에 대해서 알아볼 예정입니다. 따라서 2번이나 3번을 선택하여 진행하면 됩니다.

2. Automate beta distribution to TestFlight
3. Automate App Store distribution

여기에서는 Testflight용 배포를 위한 설정인 2번을 선택하여 진행합니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - iOS 프로젝트 선택

2번을 선택하고 진행하면 위와 같이 iOS의 프로젝트를 선택하는 화면을 확인할 수 있습니다. Flutter로 개발한 앱을 배포할 예정이므로 2번을 선택하여 진행합니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - Apple 로그인

2번을 선택하여 진행하였다면, 위와 같이 Apple 로그인을 위한 화면을 확인할 수 있습니다. iOS의 앱 배포를 위해 사용하는 Apple store connect의 로그인 아이디를 입력합니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - iOS 설정 진행

저는 이미 Fastlane을 사용하여 로그인한 적이 있기 때문에 위와 같은 화면을 확인할 수 있었습니다. 처음 Fastlane을 설정하시는 분들은 이중 인증을 위한 절차를 진행하셔야 됩니다.

위와 같은 화면 이외에도 여러차례 Continue by pressing Enter를 입력하는 화면이 나옵니다. Enter 키를 눌러 진행하여 설정을 완료합니다.

iOS용 Fastlane 폴더 및 파일

iOS의 설정을 완료하면 Flutter의 ios 폴더 밑에 아래에 폴더 및 파일이 생성되는 것을 확인할 수 있습니다.

|- fastlane
|  |- Appfile
|  |- Fastfile
|- Gemfile
|- Gemfile.lock

각 폴더 및 파일을 자세히 살펴보도록 하겠습니다.

  • fastlane 폴더: Fastlane의 설정 및 실행 파일들이 들어 있는 폴더입니다.
  • Gemfile, Gemfile.lock: Fastlane은 Ruby로 개발되었습니다. 이 파일들은 Ruby에서 라이브러리 관리하기 위한 파일들입니다.

Fastlane을 실행하기 위한 설정 내용이 들어 있는 fastlane/Appfile 파일에서 주석을 제거하면 아래와 같습니다.

app_identifier("io.github.dev-yakuza.kumoncho")
apple_id("[email protected]")

itc_team_id("119423059")
team_id("WFDJCJXQZ6")

Appfile 파일은 iOS의 자동 배포를 위한 Fastlane 설정 파일입니다. 간단한 파일이므로 자세한 설명은 생략하도록 하겠습니다.

그 다음은 실제로 앱을 배포하기 위한 실행 파일인 fastlane/Fastfile 파일에서 주석을 제거하면 아래와 같습니다.

default_platform(:ios)

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

여기에 표시된 Fastlane을 실행하기 위해서는 다음과 같은 명령어를 사용할 수 있습니다.

# cd ios
fastlane beta

이와 같이 Fastlane을 실행하면 iOS의 build number를 올리고(increment_build_number), 앱을 빌드한 다음(build_app), Testflight용으로 업로드 합니다.(upload_to_testflight)

iOS용 실행 파일 수정

기본적으로 제공하는 fastlane 파일로는 완벽하게 배포 자동화를 할 수 없습니다. 따라서 iOS용 배포를 자동화하기 위해 fastlane/Fastfile 파일을 아래와 같이 수정합니다.

# frozen_string_literal: true

default_platform(:ios)

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

  def certificate(options)
    if options[:type] == 'github'
      create_keychain(
        name: 'ios_app_keychain',
        password: '****************',
        timeout: 1800,
        default_keychain: true,
        unlock: true,
        lock_when_sleeps: false
      )
      import_certificate(
        certificate_path: 'distribution.p12',
        certificate_password: '****************',
        keychain_name: 'ios_app_keychain',
        keychain_password: '****************'
      )
    end
    install_provisioning_profile(path: 'distribution.mobileprovision')
    update_project_provisioning(
      xcodeproj: 'Runner.xcodeproj',
      target_filter: 'github',
      profile: 'distribution.mobileprovision',
      build_configuration: 'Release'
    )
    api_key = app_store_connect_api_key(
      key_id: '**************',
      issuer_id: '***********************',
      key_filepath: 'distribution.p8'
    )
    api_key
  end

  desc 'Update version'
  lane :version do |options|
    updateVersion(options)
    increment_build_number(xcodeproj: 'Runner.xcodeproj')
  end

  desc 'Submit review only'
  lane :submit_review do |_options|
    upload_to_app_store(
      submit_for_review: true,
      automatic_release: true,
      force: true,
      skip_metadata: true,
      skip_screenshots: true,
      skip_binary_upload: true
    )
  end

  desc 'Push a new beta build to TestFlight'
  lane :beta do |options|
    api_key = certificate(options)
    build_app(
      workspace: 'Runner.xcworkspace',
      scheme: 'Runner',
    )
    upload_to_testflight(api_key: api_key)
  end

  desc 'Push a new release build to the App Store'
  lane :release do |options|
    api_key = certificate(options)
    build_app(
      workspace: 'Runner.xcworkspace',
      scheme: 'Runner',
    )
    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,
      precheck_include_in_app_purchases: false,
      automatic_release: true,
      submission_information: {
        add_id_info_uses_idfa: false,
        export_compliance_encryption_updated: false,
        export_compliance_uses_encryption: false
      },
      api_key: api_key
    )
  end
end

사용자가 입력한 버전 상황에 맞게, 버전을 수정하도록 하는 함수를 정의하였습니다.

platform :ios do
  def updateVersion(options)
    ...
  end
  ...
  desc 'Update version'
  lane :version do |options|
    updateVersion(options)
    increment_build_number(xcodeproj: 'Runner.xcodeproj')
  end
  ...
end

Fastlane 명령어를 사용하여 배포할 앱의 버전을 업데이트할 수 있는 version이라는 명령어를 만들었습니다. 이 명령어는 다음과 같이 실행하여 앱의 버전을 업데이트 할 수 있습니다.

# fastlane version version:1.0.0
# fastlane version version:major
# fastlane version version:minor
fastlane version version:patch

만약 파라메터를 입력하지 않으면, 사용자 입력을 기다리도록 처리하였습니다.

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
...

Fastlane을 사용하여 배포하기 위해서는 인증서(Certification)과 프로비저닝 파일(Provisioning) 그리고 앱 스토어 커넥션 API 키(App store connect API key)가 필요합니다.

이 파일들을 준비하였다면 다음과 같이 certificate 함수를 통해 파일들을 등록하고, API 키를 반환하여 필요한 곳에서 사용할 수 있도록 만들었습니다.

# frozen_string_literal: true

default_platform(:ios)

platform :ios do
  ...
  def certificate(options)
    if options[:type] == 'github'
      create_keychain(
        name: 'ios_app_keychain',
        password: '****************',
        timeout: 1800,
        default_keychain: true,
        unlock: true,
        lock_when_sleeps: false
      )
      import_certificate(
        certificate_path: 'distribution.p12',
        certificate_password: '****************',
        keychain_name: 'ios_app_keychain',
        keychain_password: '****************'
      )
    end
    install_provisioning_profile(path: 'distribution.mobileprovision')
    update_project_provisioning(
      xcodeproj: 'Runner.xcodeproj',
      target_filter: 'github',
      profile: 'distribution.mobileprovision',
      build_configuration: 'Release'
    )
    api_key = app_store_connect_api_key(
      key_id: '**************',
      issuer_id: '***********************',
      key_filepath: 'distribution.p8'
    )
    api_key
  end
  ...
end

이렇게 만든 API 키는 다음과 같이 Testflight나 앱 스토어에 배포할 때 활용됩니다.

# frozen_string_literal: true

default_platform(:ios)

platform :ios do
  ...
  desc 'Push a new beta build to TestFlight'
  lane :beta do |options|
    api_key = certificate(options)
    build_app(
      workspace: 'Runner.xcworkspace',
      scheme: 'Runner',
    )
    upload_to_testflight(api_key: api_key)
  end

  desc 'Push a new release build to the App Store'
  lane :release do |options|
    api_key = certificate(options)
    build_app(
      workspace: 'Runner.xcworkspace',
      scheme: 'Runner',
    )
    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,
      precheck_include_in_app_purchases: false,
      automatic_release: true,
      submission_information: {
        add_id_info_uses_idfa: false,
        export_compliance_encryption_updated: false,
        export_compliance_uses_encryption: false
      },
      api_key: api_key
    )
  end
end

Testflight용으로 배포하는 스크립트를 보면, 인증서를 등록한 후, 앱을 빌드하고, Fastlane가 제공하는 upload_to_testflight 함수를 사용하여 배포하는 것을 확인할 수 있습니다.

...
platform :ios do
  ...
  desc 'Push a new beta build to TestFlight'
  lane :beta do |options|
    api_key = certificate(options)
    build_app(
      workspace: 'Runner.xcworkspace',
      scheme: 'Runner',
    )
    upload_to_testflight(api_key: api_key)
  end
  ...
end

앱 스토어 용으로 배포하는 코드도 Testflight용과 동일하게 인증서를 등록한 후, 앱을 빌드합니다. 이후 upload_to_app_store 함수를 사용하여 앱을 배포하게 됩니다.

...
platform :ios do
  ...
  desc 'Push a new release build to the App Store'
  lane :release do |options|
    api_key = certificate(options)
    build_app(
      workspace: 'Runner.xcworkspace',
      scheme: 'Runner',
    )
    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,
      precheck_include_in_app_purchases: false,
      automatic_release: true,
      submission_information: {
        add_id_info_uses_idfa: false,
        export_compliance_encryption_updated: false,
        export_compliance_uses_encryption: false
      },
      api_key: api_key
    )
  end
end

upload_to_app_store 함수에서 사용한 옵션은 다음과 같습니다.

  • force: Fastlane가 생성하는 HTML report를 생성하지 않도록 합니다.
  • reject_if_possible: 심사 대기중인 버전이 있다면 취소합니다
  • skip_metadata: 앱 스토어의 정보를 등록할지 결정합니다. 자동 배포를 할 때, 버전의 수정 내용을 작성해야하므로 이 정보를 등록하도록 설정해야합니다.
  • skip_screenshots: 저는 이미 배포한 앱에 대해서 자동 배포를 적용하고 있습니다. 따라서 스크린 샷을 다시 업로드할 필요가 없습니다
  • languages: 현재 스토어에 등록된 앱의 지역화를 설정합니다. 사용 가능한 언어는 ar-SA, ca, cs, da, de-DE, el, en-AU, en-CA, en-GB, en-US, es-ES, es-MX, fi, fr-CA, fr-FR, he, hi, hr, hu, id, it, ja, ko, ms, nl-NL, no, pl, pt-BR, pt-PT, ro, ru, sk, sv, th, tr, uk, vi, zh-Hans, zh-Hant 이며 자세한 내용은 공식 홈페이지를 참고하시기 바랍니다(공식 홈페이지)
  • release_notes: iOS는 앱을 재배포할 때, Release notes를 꼭 작성해야합니다. 제가 사용하는 스크립트는 3개국어를 지원하는 앱이므로 default와 3개국어에 해당하는 Release notes를 작성하였습니다.
  • submit_for_review: 앱 심사에 제출하도록 합니다.
  • automatic_release: 심사후 앱을 자동으로 배포하도록 설정합니다. 이 값이 설정되지 않으면, 앱 심사를 통과한 후 개발자가 수동으로 배포해야 합니다.
  • submission_information: 배포전에 암호화, 광고 포함 여부등을 물어보는 옵션을 설정합니다.

이 밖에도 많은 옵션들이 있습니다. 자세한 내용은 공식 홈페이지를 참고하시기 바랍니다.

iOS용 Fastlane을 실행하여 Testflight에 배포하기

iOS용 Fastlane을 사용하여 자동으로 배포할 준비가 끝났습니다. 이제 Fastlane을 사용하여 앱을 자동 배포해 봅시다.

아래에 명령어를 사용하여 Flutter로 제작한 앱을 Testflight에 배포해 봅니다.

# cd ios
fastlane beta version:patch

배포가 완료될 때까지, 상단한 시간이 걸립니다. 배포가 완료되면 아래와 같은 화면을 확인할 수 있습니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - iOS Testflight 배포

물론 App store connect의 Testflight에도 잘 배포된 것을 확인할 수 있습니다.

iOS용 Fastlane을 실행하여 앱 스토어에 배포하기

그럼 이제 아래에 Fastlane 명령어를 실행하여 Flutter로 개발한 앱을 앱 스토어에 배포해 봅시다.

# cd ios
fastlane release version:patch

배포가 완료되면 아래와 같은 화면을 확인할 수 있습니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - iOS 앱 배포

또한 App store connect에도 잘 배포된 것을 확인할 수 있습니다.

안드로이드

이제 Flutter로 개발한 안드로이드 앱을 Fastlane을 사용해서 배포를 자동화 해보도록 하겠습니다.

API access를 위한 Service Account 생성

Fastlane을 통해 안드로이드를 배포할 때, 구글 API를 사용하기 때문에 Google Developer Service Account를 생성할 필요가 있습니다.

Google Developer Service Account를 생성하기 위해, 아래의 링크를 통해 구글 플레이 콘솔(Google Play Console)로 이동합니다.

구글 플레이 콘솔로 이동하면 아래와 같은 화면을 확인할 수 있습니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - 구글 플레이 콘솔

왼쪽 메뉴의 Settings를 선택합니다. 그리고 Developer account 하위에 있는 API access 메뉴를 선택합니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - 구글 플레이 콘솔 api access 메뉴

위와 같은 화면이 보인다면, CREATE NEW PROJECT 버튼을 눌러, 새로운 프로젝트를 생성합니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - 구글 플레이 콘솔 api access, service account

새로운 프로젝트가 생성되면, 위와 같은 화면을 볼 수 있습니다. 하단에 있는 CREATE SERVICE ACCOUNT 버튼을 선택하면, 아래와 같은 화면을 확인할 수 있습니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - 구글 플레이 콘솔 api access, how to create service account

위와 같은 화면에서 Google API Console 링크를 선택합니다. 선택하고 나면, 아래와 같은 화면을 볼 수 있습니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - Google API Console

상단에 있는 CREATE SERVICE ACCOUNT 버튼을 선택합니다. 선택하고 나면 아래와 같이 새로운 Service account를 생성하는 화면을 확인할 수 있습니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - Google API Console, service account 생성

위와 같은 화면에서, Service account name에 이름을 입력하고, CREATE 버튼을 눌러 Service account를 생성합니다.(저는 Service account name에 google-play-fastlane-deployment을 입력하였습니다.)

Fastlane을 사용한 Fastlane 앱 자동 배포 - Google API Console, service account 역할 설정

위와 같은 화면이 보이면, Role을 선택하고 Service Account User를 검색하여 선택합니다. Role에 Service Account User를 설정하였다면, 하단에 있는 CONTINUE 버튼을 눌러 다음으로 진행합니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - Google API Console, service account 키 생성

위와 같은 화면이 보이면, 하단에 있는 CREATE KEY를 선택하고 JSON이 선택된 상태에서 CREATE 버튼을 눌러 키를 생성합니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - Google API Console, service account JSON 키 생성

CREATE 버튼을 눌러 키를 생성하면 JSON 형식에 파일이 자동으로 다운로드 됩니다. 이 파일을 Flutter 프로젝트의 android 폴더 하위에 복사합니다. 마지막으로 DONE 버튼을 눌러 Service Account를 생성합니다.

그리고 원래 화면으로 돌아와서 오른쪽 하단에 있는 DONE 버튼을 눌러 Service Account 생성을 종료합니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - 구글 플레이 콘솔 api access, how to create service account

그러면 이전과는 다르게 아래와 같은 화면을 확인할 수 있습니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - 구글 플레이 콘솔 Service Account 생성 완료

이제 권한을 부여하기 위해 오른쪽 하단의 GRANT ACCESS 버튼을 선택합니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - 구글 플레이 콘솔 Service Account 권한 설정

위와 같은 화면이 나오면 하단으로 스크롤하여 ADD USER를 선택하여 사용자를 등록합니다.

안드로이드를 위한 Fastlane 초기화

이제 안드로이드용 Fastlane을 생성해 봅시다. 아래에 명령어를 사용하여 안드로이드용 Fastlane을 생성합니다.

cd android
fastlane init

위에 명령어를 실행하면 아래와 같은 화면을 확인할 수 있습니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - 안드로이드 초기화: package name

안드로이드 프로젝트의 Package Name을 입력합니다.(ex> io.github.dev.yakuza.kumoncho) 그러면 다음과 같이 JSON 파일의 경로를 입력하라는 화면을 확인할 수 있습니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - 안드로이드 초기화: JSON path

Service Account를 생성했을 때, 다운로드한 JSON 파일을 android 폴더에 복사하였습니다. 이 파일의 경로를 지정합니다. (ex> app-xxx.json)

Fastlane을 사용한 Fastlane 앱 자동 배포 - 안드로이드 초기화: 다운로드 metadata

다음으로 안드로이드를 배포할 때, 등록한 스토어 정보(metadata)를 다운로드할 것인지 물어봅니다. 저는 이미 배포한 앱을 자동화하기 때문에 스토어 정보를 갱신할 필요가 없었습니다. 따라서 n 입력을 입력하여 스토어 정보를 다운로드하지 않았습니다.

위와 같은 화면 이외에도 여러차례 Continue by pressing Enter를 입력하는 화면이 나옵니다. Enter 키를 눌러 진행하여 설정을 완료합니다.

안드로이드용 Fastlane 폴더 및 파일

안드로이드의 설정을 완료하면 Flutter의 android 폴더 밑에 아래에 폴더 및 파일이 생성되는 것을 확인할 수 있습니다.

|- fastlane
|  |- Appfile
|  |- Fastfile
|- Gemfile
|- Gemfile.lock

각 폴더 및 파일을 자세히 살펴보도록 하겠습니다.

  • fastlane 폴더: Fastlane의 설정 및 실행 파일들이 들어 있는 폴더입니다.
  • Gemfile, Gemfile.lock: Fastlane은 Ruby로 개발되었습니다. 이 파일들을 Ruby에서 라이브러리 관리하기 위한 파일들입니다.

Fastlane을 실행하기 위한 설정 내용이 들어 있는 fastlane/Appfile 파일에서 주석을 제거하고 확인하면 아래와 같습니다.

json_key_file("api-xxx.json")
package_name("io.github.dev.yakuza.kumoncho")

우리가 설정한 Pacakge Name과 JSON 파일 위치가 설정된 것을 확인할 수 있습니다. 그 다음은 실제로 앱을 배포하기 위한 실행 파일인 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"
  lane :beta do
    gradle(task: "clean assembleRelease")
    crashlytics
  end

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

iOS와는 다르게 betadeploy, 두개의 lane이 생성된 것을 확인할 수 있습니다. 위에 Fastlane도 역시 아래와 같은 명령어로 실행할 수 있습니다.

# cd android
fastlane beta
fastlane deploy

하지만 역시 완벽한 자동화를 위해서는 Fastfile을 수정할 필요가 있습니다.

안드로이드용 실행 파일 수정

기본적으로 제공하는 fastlane 파일로는 완벽하게 자동화를 할 수 없습니다. 따라서 안드로이용 배포를 자동화하기 위해 fastlane/Fastfile 파일을 아래와 같이 수정합니다.

# frozen_string_literal: true

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 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

    s[re, 1] = if version_number
                 "\"#{version_number}\""
               else
                 "\"#{major}.#{minor}.#{patch}\""
               end

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

  def updateVersion(options)
    version = options[:version] || prompt(text: "Enter the version type or specific version\n(major, minor, patch or 1.0.0): ")

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

    if versionNum
      increment_version_number(
        version_number: versionNum
      )
    elsif %w[major minor patch].include?(version)
      increment_version_number(
        bump_type: version
      )
    else
      UI.user_error!('[ERROR] Wrong version!!!!!!')
    end
  end

  desc 'Update version'
  lane :version do |options|
    updateVersion(options)
  end

  desc 'Submit a new Beta'
  lane :beta do |_options|
    gradle(task: 'clean bundleRelease')
    upload_to_play_store(
      skip_upload_metadata: true,
      skip_upload_screenshots: true,
      skip_upload_images: true,
      skip_upload_apk: true,
      track: 'internal',
      aab: '../build/app/outputs/bundle/release/app-release.aab'
    )
  end

  desc 'Deploy a new version to the Google Play'
  lane :release do |_options|
    gradle(task: 'clean bundleRelease')
    upload_to_play_store(
      skip_upload_metadata: true,
      skip_upload_screenshots: true,
      skip_upload_images: true,
      skip_upload_apk: true,
      aab: '../build/app/outputs/bundle/release/app-release.aab'
    )
  end
end

추가한 내용을 자세히 살펴보도록 하겠습니다. 안드로이드는 iOS와 다르게 앱 버전을 업데이트하는 기능을 제공하지 않습니다.(제가 못찾은 걸 수 도 있습니다. 혹시 아시는 분은 피드백 주세요.) 따라서 안드로이드의 versionCode와 versionName을 업데이트하는 기능을 구현하였습니다.

platform :android do
  def increment_version_code
    ...
  end

  def increment_version_number(bump_type: nil, version_number: nil)
    ...
  end

  def updateVersion(options)
    ...
  end
  ...
end

버전을 업데이트하는 updateVersion 함수는 iOS에서 설명하였으므로 자세한 설명은 생략하도록 하겠습니다.

안드로이드의 테스트용 Fastlane

테스트용으로 앱을 배포하기 위한 Fastlane을 살펴보도록 하겠습니다.

platform :android do
  ...
  desc 'Submit a new Beta'
  lane :beta do |_options|
    gradle(task: 'clean bundleRelease')
    upload_to_play_store(
      skip_upload_metadata: true,
      skip_upload_screenshots: true,
      skip_upload_images: true,
      skip_upload_apk: true,
      track: 'internal',
      aab: '../build/app/outputs/bundle/release/app-release.aab'
    )
  end
  ...
end

안드로이드는 Gradle의 cleanbundleRelease로 앱을 빌드하도록 하였습니다.(assembleRelease이 아님) 또한, upload_to_play_store 함수의 track: 'internal'을 사용하여 internal test용으로 배포하도록 하였습니다.

안드로이드는 iOS와 다르게 Release notes(change log)를 작성할 필요가 없기 때문에 스토어와 관련된 모든 기능은 skip 하도록 설정하였습니다.

마지막으로 bundleRelease를 통해 aab 파일을 생성하고 업로드할 예정이므로 skip_upload_apktrue로 설정하였습니다.

안드로이드의 구글 플레이 스토어용 Fastlane

다음으로 구글 플레이 스토어에 배포하기 위한 Fastlane 코드를 살펴보도록 하겠습니다.

...
platform :android do
  ...
  desc 'Deploy a new version to the Google Play'
  lane :release do |_options|
    gradle(task: 'clean bundleRelease')
    upload_to_play_store(
      skip_upload_metadata: true,
      skip_upload_screenshots: true,
      skip_upload_images: true,
      skip_upload_apk: true,
      aab: '../build/app/outputs/bundle/release/app-release.aab'
    )
  end
end

iOS와 동일하게 하기 위해 lane :deploylane :release로 이름을 변경하였습니다. upload_to_play_store 함수의 track 파라메터가 없는 것외에는 beta용과 동일하기 때문에 자세한 설명은 생략하도록 하겠습니다.

안드로이드용 Fastlane 실행

이제 Fastlane을 사용하여 안드로이드에서 자동으로 배포할 준비가 끝났습니다. 이제 Fastlane을 사용하여 앱을 자동 배포해 봅시다.

아래에 명령어를 사용하여 Flutter로 제작한 앱을 internal test에 배포해 봅니다.

# cd android
fastlane beta version:patch

배포가 완료될 때까지, 조금 시간이 걸립니다. 배포가 완료되면 아래와 같은 화면을 확인할 수 있습니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - 안드로이드 internal test 배포

물론 Play store console에서도 internal test에 잘 배포된 것을 확인할 수 있습니다.

그럼 이제 아래에 명령어를 실행하여 실제로 배포를 진행해 봅니다.

# cd android
fastlane release version:patch

배포가 완료되면 아래와 같은 화면을 확인할 수 있습니다.

Fastlane을 사용한 Fastlane 앱 자동 배포 - 안드로이드 앱 스토어 배포

또한 Google Play store에도 잘 배포된 것을 확인할 수 있습니다.

gitignore

Fastlane을 통해 배포를 하면, 그에 따른 파일들이 생성됩니다. 이 파일들 중 Git에서 관리할 필요가 없는 파일들을 .gitignore 파일을 열고 다음과 같이 추가합니다.

...
# fastlane
ios/*.mobileprovision
ios/*.cer
ios/*.dSYM.zip
android/fastlane/README.md
ios/fastlane/README.md

완료

이것으로 Fastlane을 사용해서 Flutter로 제작한 앱을 자동으로 배포하는 방법에 대해서 알아보았습니다. 이번 블로그에서 설명한 내용은 물론 Native로 개발된 앱에서도 활용할 수 있으므로 많은 분들께 도움이 되었으면 좋겠습니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.

스무디 한 잔 마시며 끝내는 React Native, 비제이퍼블릭
스무디 한 잔 마시며 끝내는 리액트 + TDD, 비제이퍼블릭
[심통]현장에서 바로 써먹는 리액트 with 타입스크립트 : 리액트와 스토리북으로 배우는 컴포넌트 주도 개발, 심통
Posts