[React Native] GitLab CI/CD와 Fastlane으로 Android, iOS 배포 자동화하기 (ft. 소규모 팀)

안개자·2025년 10월 6일
1

💡 CI/CD 도입 배경: 안정성과 효율을 위한 프로세스 개선

1. 초기 배포 구조에서의 리팩토링 및 현 배포 구조

우리 팀은 React Native로 앱을 개발해 Android와 iOS 각각 빌드 후 배포하는 구조였다.
서버는 개발/운영으로 나뉘어 있었지만, 내가 입사했을 당시에는 단일 패키지에서 필요에 따라 개발/운영 서버를 번갈아 적용해가며 배포하는 방식이었다.

나는 Android 개발을 주로 담당하고 있었는데, 개발 서버가 적용된 APK를 따로 빌드해서 Google Drive 같은 경로로 전달하는 방식이 비효율적이라고 느꼈다. 그래서 Android도 패키지를 분리하고, App Distribution까지 활용해 배포 프로세스를 개선하는 리팩토링을 진행했다. → 관련 포스팅

개선 후의 현 배포 구조는 다음과 같다.

  • Android

    • 운영 서버 → Google Play Store
    • 개발 서버 → Firebase App Distribution
  • iOS

    • 운영/개발 서버 모두 TestFlight (iOS 개발자 담당)

2. 소규모 팀 운영을 위한 배포 시스템 확보의 필요성

앱 개발팀은 팀장님, iOS 개발자, 나(Android 개발자) 이렇게 세 명으로 이루어진 소규모 팀이다.
React Native 기반이라 플랫폼은 공유하지만, 각자 주력 플랫폼이 있어 나는 Android, 다른 분은 iOS 쪽을 중심으로 개발과 테스트를 진행한다. 배포도 각각 맡아서 진행했다.

문제는 타 플랫폼 담당자의 부재 상황이었다.
물론 문서화된 절차와 인터넷 자료를 참고하면 가능은 하지만, 익숙하지 않은 플랫폼이기에 시간도 더 걸리고, 검증도 여러 번 하게되는 듯 했다. 이는 곧 휴먼 에러 리스크 증가와 팀 생산성 저하로 이어질 가능성이 있었다.


3. 환경 변수(.env) 수동 복사 작업의 한계

사실 Android도 패키지 분리를 했지만, React Native 내부에서 사용하는 .env, .env.prod, .env.dev 파일까지 productFlavor로 완전히 제어할 수는 없었다.
react-native-config에서 제공하는 스크립트(ENVFILE=.env.example )를 적용해보려 했지만, 프로젝트 설정 문제로 정상 동작하지 않았다. 이미 상용화된 앱을 대규모 리팩토링하기엔 리스크가 크다고 판단했다.

그래서 차선책으로 빌드 타입에 따라 .env.prod.env.dev.env로 복사한 뒤 빌드하는 방식을 택했다.
하지만 빌드 과정 자체가 복잡한 상황에서 이 과정을 매번 수동으로 작업하면 캐시 누락 같은 휴먼 에러가 발생할 수 있었다.

즉, 반복적이고 단순하지만 "제대로 빌드됐는지" 확인까지 해야 하는 비효율적 작업이었다. 그래서 이 부분을 GitLab CI/CD로 자동화하기로 했다. 그리고 빌드 아티팩트 내부에 올바른 서버 주소(API URL)가 포함되어 있는지 자동으로 검증하는 단계를 추가했다.

마침 서비스가 리브랜딩 기간을 거치며 특히 QA 기간에 시간이 여유로워 팀장님께 제안 후 시작했다.😀


🛠 CI/CD 환경 구축: GitLab Runner 등록

사이드 프로젝트를 진행할 당시 GitHub Actions로 Android Native 앱 CI/CD를 구축한 경험이 있었는데, GitLab은 Runner 등록부터 시작해야 해서 진입장벽이 더 높게 느껴졌다.

  • GitHub: 바로 Action 작성 가능
  • GitLab: Runner 등록 필수

GitLab Runner 등록 과정

  1. Project > Settings > CI/CD > Runners > New project runner 클릭
  • Runner 종류: Project / Instance / Group runnersProject runner를 로컬 환경에 등록

    • Project Runner 선택 이유

      • 회사 내 별도의 CI/CD 실행 서버가 없었으며, 해당 프로젝트에만 종속적으로 작동하는 환경이 필요했다.
      • 현재의 동작 규모를 고려했을 때 로컬 Runner만으로도 충분하다고 판단했다.
    • macOS Runner 필요성

      • iOS 빌드에는 macOS 환경이 필수적이다.
      • 현재 macOS 개발 환경을 그대로 활용하기 위해, Android와 iOS 빌드를 모두 처리할 수 있는 로컬 환경만으로도 충분했다.
  1. New project runner 버튼을 클릭하여 진행하면, 비교적 쉽게 단계를 진행할 수 있다. 나는 Tags만 작성해주고 다음 단계로 넘어갔다.

2-1. 여기서 살짝 당황했는데, ssh 주소가 여전히 localhost로 되어있어 발생한 페이지였다. 주소창에서 localhost 부분만 https 로 설정해둔 주소로 바꾸면 오류없이 정상적인 페이지가 보인다.

  1. 어느 환경에서 Runner를 실행시킬 지 선택하는 화면
  1. Runner를 등록하는 과정,
    마찬가지로 Step 1 실행 시 주소의 localhost 를 변경해주었다.

🚩 미니 트러블슈팅: localhost 주소 문제

Runner 등록 과정에서 주소가 localhost로 설정되어 있어 몇 번 혼란이 있었다. 이는 기존 회사 GitLab 설정에서 SSH 및 HTTP 주소가 모두 localhost로 잡혀 있었기 때문이다.

  • SSH 주소: GitLab 서버 자체를 내가 관리하는 것이 아니었기 때문에 수정할 수 없었다.
  • HTTP 주소: 다행히 HTTP 주소는 GitLab Admin 메뉴에서 직접 변경할 수 있었고, 이를 통해 Runner 등록 및 전반적인 CI/CD가 정상 동작했다. 브라우저 주소창에서 localhost를 실제 GitLab 주소로 변경하는 방식으로도 임시 해결이 가능했다.
Menu > Admin > Settings > General > Visibility and access controls
- Custom Git clone URL for HTTP(s) 항목 수정

출처: https://alakjkj.tistory.com/104#google_vignette


📁 CI/CD 파이프라인 설계

CI/CD 파이프라인을 구축하며 복잡한 iOS 배포를 자동화하기 위해 Fastlane을 도입했다. 반면, Android 배포는 Fastlane 도입 이전에 이미 공식 문서를 참고하여 CLI 방식으로 구축을 완료했다.

YML 파일 구조

전체 파이프라인은 .gitlab-ci.yml에서 정의하고, 각 플랫폼/배포 환경별 파이프라인을 별도의 파일로 분리하여 관리했다.

.gitlab-ci.yml
ci
 ├─ pipeline-android-dev-app-distribution.yml
 ├─ pipeline-android-release-playstore.yml
 └─ pipeline-ios-deploy-testflight.yml

각 파이프라인 역할

파일명대상배포처단계
pipeline-android-dev-app-distribution.ymlAndroid (개발)Firebase App Distributionbuild, test (API_URL_DEV 검증), deploy
pipeline-android-release-playstore.ymlAndroid (운영)Google Play Storeversion (버전 업데이트 및 Push), build, test (API_URL_PROD 검증), deploy
pipeline-ios-deploy-testflight.ymliOS (개발/운영)TestFlightversion, build, test (API_URL_DEV/PROD 검증), deploy

프로젝트 환경을 고려한 CI/CD 워크플로우 설계

이전 사이드 프로젝트에서는 브랜치별로 Pull Request 기반의 자동 실행 워크플로우를 설계했지만, 현재 프로젝트의 상황은 조금 달랐다.

  1. 브랜치 규칙 고려: release, develop 브랜치 대신 major 버전 단위 브랜치가 생성되는 규칙을 따랐다.

  2. MR 비활용: 작업물을 합칠 때 GitLab 내 MR 활용보다는 로컬에서 주로 Merge 작업을 진행했다. 따라서 MR 기반 자동 실행은 적합하지 않다.

이러한 배경을 바탕으로 다음과 같이 CI/CD 파이프라인을 설계했다.

  1. 자동 실행이 아닌 필요할 때만 파이프라인 실행 (수동 트리거 방식)

    • 대부분의 브랜치에 gitlab-ci.yml 파일이 존재할 예정이므로, 매번 Push할 때마다 파이프라인이 생성되는 것은 비효율적이라고 판단했다.
    • 대신 필요할 때 GitLab UI에서 직접 Pipelines 실행을 트리거하도록 설정했다.
  2. Android와 iOS 버전 관리 차이 반영

    • iOS: Build Number만 높여주면 같은 Marketing Version으로도 TestFlight 업로드가 가능하기에 버전 업데이트가 자주 필요하지 않다.
    • Android(Google Play Store): 매 업데이트마다 버전 코드와 버전 이름 업데이트가 필수다. Fastlane을 사용하여 이 과정이 build.gradle에 자동 반영되도록 했다.

📄 세부 파이프라인 및 Fastlane 코드

1. 메인 파이프라인

메인 파일은 수동으로 하위 파이프라인을 트리거하는 역할을 한다.

.gitlab-ci.yml

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "web"'
      when: always
    - when: never

stages:
  - trigger

# Android: app distribution - 개발서버 .apk 배포
trigger_android_dev_pipeline:
  stage: trigger
  trigger:
    include:
      - local: 'ci/pipeline-android-dev-app-distribution.yml'
    strategy: depend   # 트리거된 파이프라인이 끝날 때까지 기다림 (종속적 실행)
    forward:
      yaml_variables: true    # 변수를 하위 파이프라인에 전달
      pipeline_variables: true # 파이프라인 변수도 전달
  rules:
    - if: '$CI_PIPELINE_SOURCE == "web"'
      when: manual
      allow_failure: true
  variables:
    PIPELINE_TYPE: "dev"

# Android: google play store - 운영서버 .aab 배포
trigger_android_release_pipeline:
  stage: trigger
  trigger:
    include:
      - local: 'ci/pipeline-android-release-playstore.yml'
    strategy: depend
    forward:
      yaml_variables: true
      pipeline_variables: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "web"'
      when: manual
      allow_failure: true
  variables:
    PIPELINE_TYPE: "release"

# iOS: TestFlight - $IOS_DEPLOY_TYPE 에 따른 운영서버 / 개발서버 배포
trigger_ios_testflight_pipeline:
  stage: trigger
  trigger:
    include:
      - local: 'ci/pipeline-ios-deploy-testflight.yml'
    strategy: depend
    forward:
      yaml_variables: true
      pipeline_variables: true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "web"'
      when: manual
      allow_failure: true

2. Android - App Distribution 배포 (Firebase CLI)

가장 먼저 Android 개발 서버용 App Distribution 배포를 구현했다. test 단계에서 APK 내부의 환경 변수를 검증한다.

ci/pipeline-android-dev-app-distribution.yml

# App Distribution 에 개발서버 어플 배포

stages:
  - build
  - test
  - deploy

# 1. 빌드
build_for_android_dev:
... (기존 코드 유지)
  script:
    # dev 환경으로 전환
    - cp .env.dev .env
    - cd android && ENVFILE=.env.dev ./gradlew assembleDevelopmentRelease && cd ..
  artifacts:
...

# 2. 테스트 (API_URL 검증)
test_build_env_dev:
  stage: test
  needs: ["build_for_android_dev"]
  image: openjdk:17
  script:
    - echo "=== 빌드 아티팩트 검증 시작 ==="

    # APK 파일이 존재하는지 확인
    - APK_PATH=$(ls android/app/build/outputs/apk/development/release/*.apk 2>/dev/null | sort | tail -n 1)
...
    # APK 안에서 API_URL 검증 확인
    - |
      if [ -n "$APK_PATH" ]; then
        echo "검증: APK -> $APK_PATH";
        unzip -p "$APK_PATH" assets/index.android.bundle | grep "$API_URL_DEV" \
        || { echo "ERROR: APK에 dev API URL 없음"; exit 1; }
      fi

    - echo "=== 빌드 검증 성공 ==="
...

# 3. 배포
deploy_app_distribution:
...
  before_script:
    - npm install -g firebase-tools
    # Firebase 인증 JSON 키를 CI/CD 변수로 주입
    - echo "$FIREBASE_SERVICE_ACCOUNT" | base64 -d > ${CI_PROJECT_DIR}/firebase-key.json
    - export GOOGLE_APPLICATION_CREDENTIALS="${CI_PROJECT_DIR}/firebase-key.json"
  script:
...
      # Firebase App Distribution 배포
      firebase appdistribution:distribute "$APK_PATH" \
        --app "$FIREBASE_APP_ID" \
        --groups "$TEST_GROUP_VALUE" \
        --release-notes "$AOS_RELEASE_NOTES"
...

이후 iOS TestFlight 배포를 구현하면서 fastlane을 접했는데, firebase_app_distribution(...)을 통해 App Distribution도 지원한다는 걸 알게 되었다.
이미 Android CLI 배포는 검증까지 마친 상태라 그대로 두었지만, 추후 기회가 되면 fastlane으로 통합할 예정이다.


3. Android - Google Play Store 배포 (Fastlane)

Android 운영 환경(Google Play Store) 배포는 버전 업데이트(Git Push) → 빌드 → 검증 → 배포의 4단계로 구성된다. Fastfile 내에서 Gradle 파일에 직접 접근하여 버전 업데이트를 실행하며, 실제 배포 과정은 Fastlane을 활용한다.

android/fastlane/Fastfile

# This file contains the fastlane.tools configuration
# ... (생략)
default_platform(:android)

platform :android do
  # 버전 업데이트
  desc "Bump version (patch/minor/major)"
  lane :bump_version do |options|
... (버전 업데이트 코드)
    # 파일 업데이트
    sh("sed -i '' 's/^VERSION_CODE=.*/VERSION_CODE=#{new_version_code}/' #{gradle_file}")
    sh("sed -i '' 's/^VERSION_NAME=.*/VERSION_NAME=#{new_version_name}/' #{gradle_file}")

    UI.message "✅ Updated VERSION_CODE=#{new_version_code}, VERSION_NAME=#{new_version_name}"
  end

  # AAB 파일 빌드
  desc "Build AAB"
  lane :build do
...
  end

  # play store 배포
  desc "Deploy only"
  lane :deploy do |options|

    upload_to_play_store(
      track: "production",
      aab: "app/build/outputs/bundle/operationRelease/app-operation-release.aab",
      json_key_data: ENV["GOOGLE_PLAY_JSON"]
    )
  end
end

fastlane/Appfile

json_key_file("./fastlane/play-store-credentials.json")
package_name("packaged이름")

ci/pipeline-android-release-playstore.yml

Android 버전 업데이트 후 자동으로 Git Commit & Push를 수행한다. CI/CD 전용 CI_TOKEN을 사용하여 리모트 URL을 변경 후 Push한다.

# Google Play Console 에 운영서버 어플 배포

stages:
  - version
...
# 1. 버전 bump 및 커밋
bump_version:
  stage: version
...
  script:
...
      # 3. git commit & push
      git config --global user.email "ci-bot@example.com"
      git config --global user.name "CI Bot"

      if git diff --quiet android/gradle.properties; then
        echo "No version changes detected"
      else
        # 실제 GitLab 주소와 프로젝트 경로는 'GitLab_프로젝트_주소'로 대체
        git remote set-url origin "http://ci-bot:${CI_TOKEN}@GitLab_서버_주소:포트/GitLab_프로젝트_경로.git" 
        echo "Current remote URL:"
        git remote get-url origin
        git add android/gradle.properties
        git commit -m "CI: project - Android 'bump ${AOS_BUMP_TYPE} version' 운영 배포"
        git push origin HEAD:$CI_COMMIT_REF_NAME
        echo "CI_COMMIT_REF_NAME: $CI_COMMIT_REF_NAME"
        echo "[Android] Version bumped and pushed successfully"
      fi
...
# 3. 테스트 (AAB 내 API_URL 검증)
test_build_env_prod:
  stage: test
  needs: ["build_aab"]
  image: openjdk:17
  script:
...
    # AAB 안에서 API_URL 확인
    - |
      if [ -n "$AAB_PATH" ]; then
        echo "검증: AAB -> $AAB_PATH";
        unzip -p "$AAB_PATH" base/assets/index.android.bundle | grep "$API_URL_PROD" \
        || { echo "ERROR: AAB에 API_URL 없음"; exit 1; };
      fi

    - echo "=== 빌드 검증 성공 ==="
...
# 4. 배포
deploy_playstore:
...
  script:
    # .env.prod 환경설정 적용
    - |
      echo "Deploying to PlayStore with type: $AOS_BUMP_TYPE"
      cd android && bundle exec fastlane android deploy type:$AOS_BUMP_TYPE && cd ..
...

참고: https://imleaf.tistory.com/102
https://docs.fastlane.tools/actions/upload_to_play_store/


4. iOS - Testflight 배포 (Fastlane)

iOS 배포는 Fastlane을 활용하여 TestFlight로 진행한다. 이 과정 역시 Android와 마찬가지로 빌드 후 IPA 내부의 환경 변수 검증 단계를 포함한다.
Android 배포와 다른 점은, 하나의 TestFlight 경로를 사용하며, CI/CD Variables를 통해 개발 서버와 운영 서버를 구분하여 빌드 및 배포한다는 점이다.

ios/fastlane/Fastfile

# ...
default_platform(:ios)

platform :ios do

  # Marketing Version 업데이트
  desc "Set Marketing and Build version"
  lane :set_version do |options|
...
  end

  desc "Build only"
  lane :build_ios do
    build_app(
...
    )
  end

  desc "Upload testflight"
  lane :upload_testflight do
    upload_to_testflight(
...
    )
  end
end

ci/pipeline-ios-deploy-testflight.yml

# TestFlight 개발서버/운영서버 어플 배포

stages:
...
# 1. 버전 업데이트 및 git push
version_for_ios:
  stage: version
...
  script:
...
      # 3. git commit & push
      PBXPROJ_PATH=ios/seedpayments.xcodeproj/project.pbxproj

      git config --global user.email "ci-bot@example.com"
      git config --global user.name "CI Bot"

      if git diff --quiet $PBXPROJ_PATH; then
        echo "No version changes detected"
      else
        # 실제 GitLab 주소와 프로젝트 경로는 'GitLab_프로젝트_주소'로 대체
        git remote set-url origin "http://ci-bot:${CI_TOKEN}@GitLab_서버_주소:포트/GitLab_프로젝트_경로.git" 
        echo "Current remote URL:"
        git remote get-url origin
        git add $PBXPROJ_PATH
        git commit -m "CI: project - iOS v${IOS_NEW_VERSION} 운영 배포"
        git push origin HEAD:$CI_COMMIT_REF_NAME
        echo "CI_COMMIT_REF_NAME: $CI_COMMIT_REF_NAME"
        echo "[iOS] Version bumped and pushed successfully"
      fi
...
# 3. 테스트 (IPA 내 API_URL 검증)
test_build_ios:
  stage: test
...
  script:
...
    # IPA 내부에서 API_URL 확인
    - |
      if [ -n "$IPA_PATH" ]; then
        echo "검증: IPA -> $IPA_PATH"
        unzip -o "$IPA_PATH" -d ipa_unzip > /dev/null
        APP_NAME=$(ls ipa_unzip/Payload)
        echo "APP_NAME: $APP_NAME"
        if [ "$IOS_DEPLOY_TYPE" == "prod" ]; then
          grep "$API_URL_PROD" "ipa_unzip/Payload/$APP_NAME/main.jsbundle" \
          || { echo "ERROR: IPA에 API_URL 없음"; exit 1; }
        else
          grep "$API_URL_DEV" "ipa_unzip/Payload/$APP_NAME/main.jsbundle" \
          || { echo "ERROR: IPA에 API_URL 없음"; exit 1; }
        fi
      fi

    - echo "=== iOS 빌드 검증 성공 ==="
...

🚩 미니 트러블슈팅: TestFlight 배포 인증 오류

Fastlane을 사용하여 TestFlight 배포를 시도할 때, Failed to get authorization for username과 같은 인증 오류가 발생하며 배포에 실패했다.

구글링을 통해 이 오류는 2단계 인증 환경에서 일반 Apple ID 비밀번호 대신 앱 암호를 사용해야 발생함을 확인했다. 이에 따라 FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD 환경 변수에 앱 암호를 설정하여 반영하니 정상적으로 작동했다.


🔑 CI/CD Variables 구성

CI/CD 파이프라인에서 사용되는 환경 변수 목록이다. GitLab의 Settings > CI/CD > Variables에 저장하여 사용하고 있다.

구분변수명설명 및 획득 경로비고
변동 값API_URL_DEV개발 서버 API 주소배포 환경 설정에 사용
API_URL_PROD운영 서버 API 주소빌드 검증 단계에서 사용됨
고정 값CI_USERCI/CD 작업 수행 시 Git 사용자 이름 (ci-bot)Git 커밋에 사용
CI_TOKENGitLab API 접근 토큰 (glpat-...)Settings > Access Tokens에서 발급 후, CI/CD 파이프라인의 Git Push 권한에 사용
FirebaseFIREBASE_APP_IDFirebase 프로젝트 앱 IDFirebase 콘솔 > 프로젝트 설정에서 확인
FIREBASE_SERVICE_ACCOUNTBase64 인코딩된 Firebase 서비스 계정 JSONIAM 및 관리자 > 서비스 계정에서 키 파일을 생성 후 Base64 인코딩하여 등록
Google PlayGOOGLE_PLAY_JSONBase64 인코딩된 Google Play 서비스 계정 JSONPlay Console API 접근 자격 증명 파일을 Base64 인코딩하여 등록

참고 이미지

CI_USER, CI_TOKEN: Project > Settings > Access tokens

FIREBASE_APP_ID: Firebase → 프로젝트 설정 → 앱 ID

FIREBASE_SERVICE_ACCOUNT: IAM 및 관리자 > 서비스 계정 > FastlaneClient


🏁 마무리 및 성과

기존에는 운영/개발 서버 빌드 과정에서 캐싱 문제로 빌드 시간이 오래 걸렸고, 같은 작업을 반복하면서도 매번 수동 검증을 해야 했다.

이 과정을 GitLab CI/CD로 자동화한 덕분에, 지금은 Android/iOS 모두 환경별 배포 시간을 빠르게 단축하고 반영할 수 있게 됐다.

결과적으로 휴가나 부재 상황에도 팀 전체가 안정적으로 대응할 수 있게 되었고, QA 시간 낭비도 크게 줄일 수 있었다. 이번 자동화는 단순히 '편의성 증대'를 넘어서, 휴먼 에러를 방지하고 팀의 안정성과 생산성을 끌어올린 중요한 계기가 됐다.

💫 아쉬운 점 및 개선 목표

  • 메신저 알림 기능: Fastlane이 지원하는 기능 중 배포 완료 후 Slack을 통해 안내 메시지를 발송하는 기능이 있다. 이런 부분을 우리 프로젝트에도 적용하고 싶었지만, 현재 회사에서 사용하고 있는 메신저 환경에서는 적용하기 어려워 아쉬웠다.
  • YAML 숙련도 부족: YAML 문법에 익숙하지 않다 보니 파이프라인을 적용하고 검증하는 데 초기 시간이 오래 걸렸다.
  • Fastlane으로 통합 리팩토링: 현재 App Distribution 배포는 Firebase CLI로 반영되어 있는데, 장기적으로는 이 부분을 Fastlane 액션으로 통합하여 리팩토링하고 싶다.
  • 런타임 테스트 단계 부재: 현재 파이프라인에는 런타임 테스트 단계가 포함되어 있지 않은 점이 아쉽다. 추후 App Distribution에서 제공하는 앱 테스트 에이전트 기능을 활용하여 이 부분을 보완할지 고민 중이다.

📎 참고 링크

0개의 댓글