주맨의 개발노트

[CD] Firebase App Distribution으로 테스트 앱 배포 자동화하기 본문

안드로이드

[CD] Firebase App Distribution으로 테스트 앱 배포 자동화하기

JooMan 2026. 6. 16. 23:47

사내 테스트 앱을 확인하는 과정은 생각보다 자주 개발 흐름을 끊습니다. PR이 develop에 병합됐다는 알림을 받고 실제 앱에서 확인하려면, 하던 작업을 잠시 멈추고 브랜치를 바꾸고 빌드한 뒤 앱을 설치해야 합니다.

한 번이면 괜찮습니다. 하지만 하루에 여러 PR이 병합되고, 그때마다 변경사항을 확인해야 하는 상황이 반복되면 이야기가 달라집니다. 빌드 시간보다 더 크게 느껴졌던 것은 작업하던 맥락을 내려놓고 확인 모드로 전환해야 하는 비용이었습니다.

그래서 develop 브랜치에 Firebase App Distribution 배포 파이프라인을 붙였습니다. 목표는 단순했습니다. PR이 병합되면 테스트 앱이 자동으로 배포되고, 팀원은 새 빌드를 받아 바로 확인할 수 있는 흐름을 만드는 것이었습니다.

수동 확인 흐름에서 생긴 비용

기존에는 다른 팀원의 PR이 병합된 뒤 앱에서 확인하려면 로컬 환경에서 직접 최신 빌드를 만들어야 했습니다. 보통 다음과 같은 순서였습니다.

Step 1 · 작업 중단 현재 작업하던 변경사항을 stash 하거나 커밋합니다.
Step 2 · 브랜치 전환 develop을 최신화하고 체크아웃합니다.
Step 3 · 빌드와 설치 프로젝트를 빌드하고 테스트 기기에 앱을 설치합니다.
Step 4 · 작업 복귀 다시 원래 브랜치로 돌아와 작업 맥락을 복원합니다.

이 흐름의 문제는 단순히 시간이 걸린다는 데 있지 않았습니다. 확인할 때마다 작업 중이던 화면, 브랜치, 변경사항을 잠시 내려놓아야 했고, 확인이 끝난 뒤 다시 원래 맥락으로 돌아오는 데도 비용이 들었습니다.

배포하는 사람 입장에서도 비슷한 문제가 있었습니다. 앱만 올리면 끝나는 것이 아니라, 어떤 변경이 포함됐는지도 함께 전달해야 했습니다. 설치하는 사람이 무엇을 확인해야 하는지 모르면 테스트 앱을 받아도 검수의 방향이 흐려집니다.

기존 방식

각자가 로컬에서 develop을 빌드하고 앱을 설치합니다. 변경사항은 PR 목록이나 커밋 히스토리를 다시 찾아봐야 합니다.

개선하고 싶었던 방식

PR이 병합되면 테스트 앱이 자동 배포되고, 배포 알림 안에서 이번 빌드의 변경사항까지 바로 확인할 수 있습니다.

Firebase App Distribution으로 테스트 앱 배포하기

테스트 앱 배포 도구로는 Firebase App Distribution을 선택했습니다. Play Store 심사 없이 APK를 테스터 그룹에 배포할 수 있고, Firebase 콘솔에서 테스터와 그룹을 관리할 수 있기 때문입니다.

GitHub Actions와 연결하면 develop에 push가 발생할 때마다 Debug APK를 빌드하고, 지정한 테스터 그룹에 새 빌드를 배포할 수 있습니다. 내부 개발자들이 빠르게 최신 상태를 확인해야 하는 목적에는 충분히 잘 맞았습니다.

Step 1 · PR 병합 기능 PR이 develop 브랜치에 병합됩니다.
Step 2 · Actions 실행 develop push 이벤트로 CD 워크플로우가 실행됩니다.
Step 3 · APK 빌드 ./gradlew assembleDebug로 Debug APK를 생성합니다.
Step 4 · Firebase 배포 Firebase App Distribution에 APK와 릴리즈 노트를 업로드합니다.

워크플로우의 시작점은 develop push입니다. PR 병합은 결국 대상 브랜치에 새 커밋이 들어오는 일이므로, 이 이벤트를 배포 트리거로 사용했습니다.

.github/workflows/cd-firebase.yml YAML
on:
  push:
    branches:
      - develop
  workflow_dispatch:

workflow_dispatch도 함께 열어두었습니다. 기본은 자동 배포지만, 필요한 경우 GitHub Actions UI에서 수동으로 다시 배포할 수 있게 하기 위함입니다.

빌드 단계는 단순합니다. CI 환경에서는 Gradle daemon을 유지할 이점이 크지 않기 때문에 --no-daemon 옵션을 붙였습니다.

.github/workflows/cd-firebase.yml YAML
- name: Build Debug APK
  run: ./gradlew assembleDebug --no-daemon

Firebase 업로드는 GitHub Action을 사용했습니다. Firebase 앱 ID와 서비스 계정 인증 정보는 모두 GitHub Secrets에 등록해두고, 워크플로우에서는 secret 값을 참조하도록 구성했습니다.

.github/workflows/cd-firebase.yml YAML
- name: Upload to Firebase
  uses: wzieba/Firebase-Distribution-Github-Action@v1
  with:
    appId: ${{ secrets.FIREBASE_APP_ID }}
    serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
    groups: android-developers
    file: app/build/outputs/apk/debug/app-debug.apk
    releaseNotes: |
      브랜치: ${{ github.ref_name }}
      빌드: #${{ github.run_number }}

      ${{ steps.notes.outputs.notes }}

여기까지 하면 자동 배포 자체는 동작합니다. 하지만 실제로 써보면 곧바로 다음 문제가 보입니다. 앱은 배포됐는데, 이번 빌드에서 무엇을 확인해야 하는지는 여전히 알기 어렵다는 점입니다.

자동 배포만으로는 부족했다

처음 Firebase 업데이트 알림을 받았을 때 릴리즈 노트에는 브랜치와 빌드 번호 정도만 표시됐습니다. 배포 파이프라인이 성공했다는 것은 알 수 있었지만, 어떤 PR이 포함됐는지는 알 수 없었습니다.

결국 팀원은 앱을 열기 전에 GitHub로 돌아가 병합된 PR을 찾아봐야 했습니다. 자동 배포로 로컬 빌드 비용은 줄였지만, 변경사항을 파악하는 비용은 그대로 남아 있었습니다.

테스트 앱 배포에서 릴리즈 노트는 부가 정보가 아니었습니다. 무엇을 확인해야 하는지 알려주는 검수의 시작점에 가까웠습니다.

그래서 배포 자동화의 범위를 한 단계 더 넓혀야 했습니다. APK를 Firebase에 올리는 것뿐 아니라, 이번 배포에 포함된 변경사항도 자동으로 붙여야 했습니다.

Release Drafter를 릴리즈 노트 원천으로 사용하기

이미 PR에는 feat, fix, design, ci 같은 라벨을 붙이고 있었습니다. 이 라벨을 기준으로 변경사항을 모으면 배포 노트의 기본 구조를 만들 수 있겠다고 판단했습니다.

이 역할을 맡긴 도구가 Release Drafter입니다. Release Drafter는 PR이 병합될 때마다 GitHub Draft Release를 생성하거나 갱신하고, PR 라벨에 따라 변경사항을 카테고리별로 정리해줍니다.

Release Drafter 자체의 설정 방식과 주요 옵션은 이전에 [CD] Release Drafter로 릴리즈 노트 자동화 글에서 따로 정리했습니다. 이 글에서는 그 Draft Release body를 Firebase CD 파이프라인에서 어떻게 가져다 썼는지에 집중하겠습니다.

1Label

PR에 변경 성격을 나타내는 라벨을 붙입니다

feat, fix, design, ci처럼 릴리즈 노트 분류에 사용할 라벨을 PR 단위로 관리합니다.

2Draft

Release Drafter가 Draft Release body를 갱신합니다

PR이 병합될 때마다 마지막 릴리즈 이후의 변경사항이 Draft Release에 누적됩니다.

3CD

CD 워크플로우가 Draft body를 Firebase 릴리즈 노트로 사용합니다

배포 직전에 GitHub Release API로 Draft body를 읽고, Firebase 업로드 step의 releaseNotes에 주입합니다.

연결 구조는 단순합니다. Release Drafter는 변경사항을 정리하는 역할만 맡고, Firebase CD는 정리된 내용을 가져다 배포 노트에 붙이는 역할만 맡습니다.

.github/release-drafter.yml YAML
name-template: 'v$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'

categories:
  - title: ✨ 새 기능
    labels:
      - feat
      - feature
  - title: 🐛 버그 수정
    labels:
      - fix
      - bug
  - title: 🎨 디자인 변경
    labels:
      - design
      - ui
  - title: ⚡️ 성능 개선
    labels:
      - perf
      - performance
  - title: 🔧 내부 작업
    labels:
      - chore
      - refactor
      - ci
      - test
      - docs

exclude-labels:
  - skip-changelog

version-resolver:
  major:
    labels:
      - breaking-change
  minor:
    labels:
      - feat
      - feature
  default: patch

no-changes-template: '변경사항 없음'

change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
change-title-escapes: '\<*_&'

template: |
  ## 변경사항

  $CHANGES

이렇게 생성된 Draft Release body는 테스트 앱 배포 시점에 그대로 사용할 수 있습니다. PR 라벨만 꾸준히 붙이면 변경사항 분류와 릴리즈 노트 작성은 자동화됩니다.

Draft Release body를 CD에서 읽어오기

Firebase 업로드 직전에 Draft Release body를 가져오는 step을 추가했습니다. 이 step은 GitHub API에서 draft release를 찾고, 해당 release의 body를 GitHub Actions output으로 내보냅니다.

멀티라인 문자열을 output으로 넘길 때는 delimiter를 직접 지정해야 합니다. 릴리즈 노트는 여러 줄의 마크다운이기 때문에 단순히 echo "notes=$NOTES" 형태로 처리하면 줄바꿈이 깨질 수 있습니다.

.github/workflows/cd-firebase.yml YAML
- name: Generate release notes
  id: notes
  env:
    GH_TOKEN: ${{ github.token }}
  run: |
    NOTES=$(gh api "repos/$GITHUB_REPOSITORY/releases" \
      --jq '[.[] | select(.draft == true)] | first | .body // ""')

    DELIMITER="EOF_$(openssl rand -hex 8)"
    echo "notes<<$DELIMITER" >> $GITHUB_OUTPUT
    echo "$NOTES" >> $GITHUB_OUTPUT
    echo "$DELIMITER" >> $GITHUB_OUTPUT

이후 Firebase 업로드 step에서는 steps.notes.outputs.notesreleaseNotes에 넣습니다. 그러면 Firebase App Distribution 알림에서 브랜치, 빌드 번호, 변경사항을 함께 볼 수 있습니다.

gh release list가 Draft를 가져오지 못했던 함정

처음에는 Draft Release를 가져오기 위해 gh release list를 사용했습니다. CLI가 release 목록을 반환하고, 그중 isDraft가 true인 항목을 찾으면 된다고 생각했습니다.

처음 시도한 방식 Bash
DRAFT_TAG=$(gh release list --json tagName,isDraft \
  --jq '[.[] | select(.isDraft == true)] | first | .tagName // ""')

NOTES=$(gh release view "$DRAFT_TAG" --json body --jq '.body // ""')

논리만 보면 자연스러운 코드입니다. Draft release 목록에서 태그를 찾고, 그 태그로 body를 조회하는 흐름입니다. 하지만 실제 워크플로우에서는 DRAFT_TAG가 계속 빈 문자열로 나왔습니다.

원인은 gh release list가 draft release를 목록에서 제외한다는 점이었습니다. 목록에 draft가 포함되지 않으니 isDraft == true 조건을 아무리 걸어도 결과가 나올 수 없었습니다.

gh release list

CLI가 반환하는 release 목록에 draft가 포함되지 않아 Draft Release body를 찾을 수 없었습니다. 태그 조회와 body 조회도 두 단계로 나뉘었습니다.

gh api

GitHub REST API를 직접 호출해 draft release를 포함한 목록을 받고, 그 자리에서 body를 바로 추출했습니다.

해결은 gh api로 GitHub REST API를 직접 호출하는 것이었습니다. GET /repos/{owner}/{repo}/releases 응답에서 draft가 true인 항목을 찾고, 그 body를 바로 가져오면 됩니다.

수정한 방식 Bash
NOTES=$(gh api "repos/$GITHUB_REPOSITORY/releases" \
  --jq '[.[] | select(.draft == true)] | first | .body // ""')

이 방식으로 바꾸면서 조회 단계도 줄었습니다. Draft tag를 먼저 찾고 다시 body를 조회하는 대신, release 목록에서 draft body를 바로 꺼내면 되기 때문입니다.

중복 배포를 막기 위한 concurrency

CD를 붙인 뒤에는 또 다른 운영 문제가 생길 수 있습니다. develop에 PR이 짧은 간격으로 여러 번 병합되면 이전 배포와 최신 배포가 동시에 실행될 수 있습니다.

테스트 앱 배포에서는 최신 커밋 기준의 빌드가 가장 중요합니다. 오래된 워크플로우가 늦게 끝나면서 나중에 알림을 보내거나, 팀원이 어떤 빌드를 받아야 하는지 헷갈리면 자동화의 의미가 줄어듭니다.

.github/workflows/cd-firebase.yml YAML
concurrency:
  group: cd-firebase-${{ github.ref }}
  cancel-in-progress: true

concurrency를 설정하면 같은 브랜치에서 실행 중인 이전 배포를 취소하고 최신 실행만 남길 수 있습니다. 테스트 배포에서는 모든 중간 빌드를 보존하는 것보다 최신 상태를 빠르게 전달하는 것이 더 중요했습니다.

배포 알림이 검수 안내가 되었다

최종적으로 Firebase 업데이트 알림에는 브랜치, 빌드 번호, 그리고 Release Drafter가 정리한 변경사항이 함께 들어가게 됐습니다. 이제 팀원은 앱을 열기 전에 이번 빌드에서 무엇을 봐야 하는지 먼저 알 수 있습니다.

Firebase 릴리즈 노트 예시 Text
브랜치: develop
빌드: #42

## 변경사항

### ✨ 새 기능
- 위시리스트 생성 기능 추가 @Jooman-Lee (#38)

### 🐛 버그 수정
- 로그인 크래시 수정 @Jooman-Lee (#41)

### 🔧 내부 작업
- CI Node.js 24 마이그레이션 @Jooman-Lee (#39)

이 변화는 작은 편의 기능처럼 보일 수 있지만, 실제 협업에서는 효과가 컸습니다. 확인하는 사람은 별도로 PR 목록을 뒤지지 않아도 되고, 배포하는 사람은 매번 변경사항을 손으로 정리하지 않아도 됩니다.

무엇보다 테스트 앱 배포가 단순히 "최신 APK를 올리는 일"에서 "이번 변경사항을 팀에 전달하는 일"로 바뀌었습니다. CD 파이프라인에 릴리즈 노트를 붙인 이유도 결국 여기에 있습니다.

정리하며

이번 작업에서 가장 크게 느낀 점은 배포 자동화의 끝이 빌드 업로드가 아니라는 것이었습니다. 테스트 앱을 받는 사람이 무엇을 확인해야 하는지 모른다면, 자동 배포는 절반만 완성된 상태에 가깝습니다.

Firebase App Distribution은 테스트 앱을 빠르게 전달해줬고, Release Drafter는 변경사항을 꾸준히 정리해줬습니다. 두 도구를 연결하니 로컬 빌드 비용과 릴리즈 노트 작성 비용을 동시에 줄일 수 있었습니다.

다만 이 구조도 PR 라벨이 꾸준히 관리된다는 전제가 필요합니다. Release Drafter는 라벨을 기준으로 분류할 뿐이기 때문에, 라벨을 빠뜨리면 릴리즈 노트에서도 빠집니다. 자동화가 잘 동작하려면 도구 설정만큼 팀의 작은 습관도 함께 맞춰져야 했습니다.

핵심 정리
  • 테스트 앱 배포의 병목은 빌드 시간만이 아니었습니다. 브랜치 전환, 설치, 변경사항 확인까지 포함한 컨텍스트 스위칭 비용이 반복됐습니다.
  • Firebase App Distribution으로 최신 APK 배포를 자동화했습니다. develop push를 트리거로 Debug APK를 빌드하고 테스터 그룹에 배포했습니다.
  • 자동 배포만으로는 변경사항 전달 문제가 남았습니다. 릴리즈 노트가 없으면 팀원은 여전히 PR 목록을 다시 확인해야 했습니다.
  • Release Drafter의 Draft Release body를 Firebase 릴리즈 노트로 사용했습니다. PR 라벨 기반 변경사항을 배포 시점에 자동으로 첨부했습니다.
  • gh release list 대신 gh api를 사용했습니다. Draft Release를 포함해 조회하려면 REST API를 직접 호출하는 방식이 필요했습니다.
Comments