GIT

“이거 무슨 버전에 반영된거에요?”라는 질문에 5초만에 대답하는 조직되기

최수빈 프로필 이미지

최수빈

2024.06.27

12 min read

“이거 무슨 버전에 반영된거에요?”라는 질문에 5초만에 대답하는 조직되기

안녕하세요! 코멘토에서 앱을 개발하고 있는 최수빈입니다. "이 기능이 언제 반영되었나요?"라는 질문에 대답하기까지 다들 몇 초 정도 걸리시나요? 아마 버전이 명시적으로 잘 관리되고 있는  조직이라면 “이게 그렇게까지 어려운 질문인가?”하고 의문이 드실 수 있어요. 하지만 저는 이 질문에 답하는 것이 상당히 어려웠습니다.

저희 팀은 앱이 배포가 되면 자동으로 릴리즈 노트가 작성되었습니다. 하지만 릴리즈 노트만으로 어떤 기능이 포함된 버전인지 추적하기 어려웠으며, 사실상 릴리즈 노트를 제외하곤 어디서도 버전에 대한 정보를 알 수 없었습니다.

특히나 데이터를 기반으로 일하는 팀에서는 특정 기능이 반영된 버전에 대한 사용자들의 데이터를 추적해야하기 때문에 버전에 대한 관리가 더욱 중요합니다.  위와 같은 질문이 여전히 곤란한 분들을 위해 저희 팀이 버전 관리 시스템을 개선한 경험을 공유해 드리려고 합니다.

기존 배포 프로세스

개선한 시스템을 보여 드리기 전에 코멘토 앱팀의 개발 환경과 기존 배포 프로세스를 소개해 드리겠습니다. 저희 팀은 버전 컨트롤 시스템으로 Git을 사용하고 GitHub을 통해 관리합니다. 전사 커뮤니케이션 도구로는 agit를 사용하고 있습니다. 배포는 Github workflow를 통해 아래의 과정으로 진행하고 있습니다.

  1. master merge를 trigger로 master CI/CD GitHub workflow가 실행
  2. pubspec.yml에 기재된 버전으로 iOS/AOS 앱 자동 배포
  3. 배포된 버전에 포함된 커밋 히스토리로 릴리즈 노트 본문 생성
  4. 배포 후 agit에 릴리즈 노트가 자동으로 업로드

기존 릴리즈 노트, 뭐가 문제였을까?

기존 너굴맨이 만들어주던 릴리즈 노트

앞서 소개해 드린 과정을 통해 최종적으로 위와 같은 릴리즈 노트가 agit에 올라갔습니다. 배포가 되고 나서 자동으로 릴리즈 노트가 작성돼서 agit로 알람이 오는 것은 좋았지만, 보시다시피 릴리즈 노트 자체가 커밋에 대한 내용으로만 이루어져 있었습니다. 커밋에는 보통 자잘한 변경 사항 또한 포함되기 때문에, 기획자가 궁금해하는 기능이 반영된 버전을 찾는데 시간이 꽤 소요됐습니다.

또한 릴리즈 노트의 위치가 GitHub이 아니라 전사 커뮤니케이션 도구에만 있는 것 또한 불편했습니다. Git이 기본으로 제공하는 tag와 GitHub의 release 기능을 활용하면 버전을 통합적으로 관리할 수가 있습니다. 따라서 저희 팀은 tag와 release를 활용해 버전의 히스토리를 관리하도록 버전 관리 시스템을 도입하기로 했습니다.

Tag란?
tag는 특정 커밋에 이름을 붙여주는 기능입니다. 읽기 전용 커밋이라고도 볼 수 있습니다. 많은 회사가 tag를 활용해 버전을 명시하고 있습니다. 이를 통해 배포 시점이나 특정 기능이 추가된 시점을 명확히 기록할 수 있고, 언제든지 해당 시점의 코드로 되돌아가거나 참조할 수 있습니다.
Release란?
release는 tag를 기반으로 배포를 도와주는 기능입니다. tag를 생성하면 release를 만들 수 있고, 해당 tag가 붙은 시점의 코드를 포함하여 릴리즈 노트를 생성할 수 있습니다. 코멘토 앱 팀은 배포를 따로 해주고 있기 때문에 release의 배포 기능은 쓰지 않고, 릴리즈 노트를 생성하기 위해서 사용했습니다.

새로운 시스템, 어떤게 편리해졌나요?

새로운 시스템을 도입한 이후엔 배포 workflow가 실행되면 자동으로 tag가 달리고, GitHub에 release에 릴리즈 노트가 작성됩니다. PR 제목을 기반으로 자동 생성되므로 어떤 기능이 포함되었는지 바로 파악이 가능한 릴리즈 노트가 만들어집니다. 또한 tag가 붙으면서 커밋 히스토리나 Git Graph를 활용해 버전을 빠르게 확인할 수 있습니다.

새롭게 바뀐 릴리즈 노트
GIt Grpah를 통해 tag로 현 버전 확인

Tag/Release 자동화를 위한 workflow 작성하기

그럼 이제 workflow의 각 단계를 살펴보면서 위 시스템을 어떻게 구현했는지 확인해보겠습니다. 아래 코드들은 실제 코멘토 앱에 사용되는 workflow입니다. 이 파일을 기반으로 각 단계가 어떤 역할을 하는지 자세히 설명하겠습니다.

Extract PR Numbers

우선 최근 tag 이후의 커밋 메시지에서 PR 번호를 추출하는 과정입니다. 저희 팀은 PR 제목에 기능에 대한 내용을 명시해두었기 때문에 릴리즈 노트 내용에 커밋이 아닌 PR 제목을 넣기로 했습니다. PR 제목을 추출하는 방법을 찾다가 선택한 방법은 커밋 내역에 포함된 PR number를 추출하여 해당 PR을 추적하는 것이었습니다.

  • needs: [deploy-ios, deploy-android]
  • deploy-ios, deploy-android 단계 이후에만 실행이 됩니다. 이 단계에서 실패할 시 아래 단계들은 실행되지 않습니다.
  • git describe --tags --abbrev=0 <commit-hash>
  • 이 명령어는 <commit-hash>에서 가장 가까운 tag 이름을 출력합니다. <commit-hash>는 특정 커밋의 해시입니다. 생략하면 가장 최근의 tag 이름을 가져올 수 있습니다.
  • commits=$(git log $prev_tag..HEAD --pretty=format:"%s")
  • 이전 tag 이후의 모든 커밋 메시지를 가져옵니다.
  • pr_numbers=$(echo "$commits" | grep -oP 'Merge pull request #\\\\d+' | grep -oP '\\\\d+')
  • 커밋 메시지에서 PR 번호를 추출하여 pr_numbers 변수에 저장합니다.
  • echo "pr_numbers=$pr_numbers_string" >> $GITHUB_OUTPUT
  • 추출된 PR 번호를 출력하여 이후 단계에서 사용할 수 있도록 설정합니다.
extract-pr-numbers:
  runs-on: ubuntu-latest
  needs: [deploy-ios, deploy-android]
  outputs:
    pr_numbers: ${{ steps.extract.outputs.pr_numbers }}
  steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
    - name: Extract PR numbers from commit messages
      id: extract
      run: |
        prev_tag=$(git describe --tags --abbrev=0)
        commits=$(git log $prev_tag..HEAD --pretty=format:"%s")
        echo "Commits: $commits"
        pr_numbers=$(echo "$commits" | grep -oP 'Merge pull request #\\\\d+' | grep -oP '\\\\d+')
        echo "PR Numbers: $pr_numbers"
        pr_numbers_string=$(echo "$pr_numbers" | tr '\\\\n' ' ')
        echo "pr_numbers=$pr_numbers_string" >> $GITHUB_OUTPUT
      shell: bash

Get PR Titles

다음 단계는 PR 번호를 이용해 제목을 가져오는 것입니다.

  • github-script
  • GitHub API를 사용하여 PR 정보를 가져오는 데 사용됩니다
  • needs.extract-pr-numbers.outputs.pr_numbers
  • 이전 단계인 extract-pr-numbers 단계에서 output으로나온 outputs.pr_numbers 를 가져옵니다.
  • github.rest.pulls.get
  • 배열을 순회하며 각 PR 번호에 대해 github.rest.pulls.get 메서드를 사용하여 GitHub API에서 PR 정보를 가져옵니다.
  • 가져온 PR 정보에서 pull.title을 추출하여 prTitles 배열에 #PR번호 - PR 제목 형식으로 추가합니다.
  • 추출된 PR 번호를 통해 각 PR의 제목을 가져오고, 이를 pr_titles 변수에 저장하고 뒤에 단계에서 쓸 수 있도록 setOutput을 해줍니다.
get-pr-titles:
  runs-on: ubuntu-latest
  needs: [extract-pr-numbers]
  outputs:
    pr_titles: ${{ steps.fetch-pr-titles.outputs.pr_titles }}
  steps:
    - uses: actions/github-script@v6
      id: fetch-pr-titles
      with:
        github-token: ${{ secrets.GITHUB_TOKEN }}
        script: |
          const prNumbers = "${{ needs.extract-pr-numbers.outputs.pr_numbers }}".trim().split(' ');
          let prTitles = [];
          for (const prNumber of prNumbers) {
            if(prNumber) {
              const { data: pull } = await github.rest.pulls.get({
                owner: context.repo.owner,
                repo: context.repo.repo,
                pull_number: parseInt(prNumber, 10)
              });
              prTitles.push(`#${prNumber} - ${pull.title}`);
            }
          }
          const prTitlesString = prTitles.join('\\\\n- ');
          core.setOutput("pr_titles", prTitlesString ? `- ${prTitlesString}` : "No PR titles found");

Get Version

저희 팀은 앱의 버전을 pubspec.yaml에서 수동으로 올려주는 방식을 사용하고 있습니다. 따라서 파일에서 해당 버전을 참고하여 릴리즈 노트를 작성하고 tag를 생성할 수 있도록 버전을 읽는 과정입니다.

  • yq -r .version pubspec.yaml > version.file
  • pubspec.yaml 파일에서 현재 버전을 읽어와 version.file이라는 파일에 저장해준 후, 해당 값을 APP_VERSION 변수에 저장합니다.
get-version:
  runs-on: ubuntu-latest
  outputs:
    APP_VERSION: ${{ steps.set-version.outputs.APP_VERSION }}
  steps:
    - uses: actions/checkout@v2
      with:
        fetch-depth: 0
    - name: Set version
      id: set-version
      run: |
        yq -r .version pubspec.yaml > version.file
        APP_VERSION=$(<version.file)
        echo APP_VERSION="$APP_VERSION" >> "$GITHUB_ENV"
        echo "::set-output name=APP_VERSION::$APP_VERSION"

Create Tag

새로운 tag를 생성하고 릴리즈 노트의 본문을 작성하는 단계입니다. 본문에 원하는 형식이나 내용이 있다면 generate body 부분을 수정하여 사용하면 됩니다.

  • generate-body
  • 본문을 생성하는 부분으로 릴리즈 노트 본문의 형식을 바꾸고 싶다면 이 부분을 수정하면 됩니다.
  • create-tag
  • 새로운 tag를 생성합니다. 같은 tag가 존재하면 오류가 발생합니다.
create-tag:
  runs-on: ubuntu-latest
  needs: [get-version, get-pr-titles]
  outputs:
    tag-exists: ${{ steps.create-tag.outputs.tag_exists }}
    release-body: ${{ steps.generate-body.outputs.body }}
  steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
    - name: Generate body
      id: generate-body
      run: |
        EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
        pr_titles="${{ needs.get-pr-titles.outputs.pr_titles }}"
        {
          echo "body<<$EOF"
          echo "# Notable Changes"
          echo "$pr_titles"
          echo ""
          echo "$EOF"
        } >>"$GITHUB_OUTPUT"
      shell: bash
    - uses: rickstaa/action-create-tag@v1
      id: create-tag
      with:
        tag: ${{ needs.get-version.outputs.APP_VERSION }}
        tag_exists_error: true
        message: ${{ needs.get-version.outputs.APP_VERSION }}

Create Release

최종적으로 GitHub의 release를 생성하는 과정입니다. 앞선 단계에서 만들어진 tag와 릴리즈 노트의 본문을 기반으로 새로운 release를 생성합니다.

create-release:
  runs-on: ubuntu-latest
  needs: [get-version,create-tag]
  if: ${{ needs.create-tag.outputs.tag-exists == 'false' }}
  steps:
    - uses: actions/checkout@v3
    - name: Create a GitHub release
      uses: ncipollo/release-action@v1
      with:
        tag: ${{ needs.get-version.outputs.APP_VERSION }}
        name: ${{ needs.get-version.outputs.APP_VERSION }}
        body: ${{ needs.create-tag.outputs.release-body }}

위 모든 작업들을 통해 완성된 스크립트를 돌리면 이와 같은 릴리즈 노트가 민들어집니다.

짠!

아직은  PR 제목들로만 이루어진 리스트지만, 추후 PR의 라벨에 따라 카테고리를 묶어 본문을 정리해준다면 좀 더 완성된 릴리즈 노트가 될 것 같습니다. 이 작업들을 도와주는 Release Drafter라는 템플릿도 있으니 tag/release를 빠르게 적용시켜보고싶으신 분들은 참고해보시면 좋을 것 같습니다.

최종적으로는 “언제 반영되었나요”라는 질문을 애초에 할 필요도 없는 조직이 되는 것이 목표입니다. 모든 구성원들이 앱에 어떤 기능이 언제 배포되었는지를 개발자에게 물어보지 않아도 빠르게 알 수 있도록 공유되도록 말이죠. 이를 위해 자동으로 작성된 릴리즈 노트를 기반으로 학습된 gpt를 만들어보기 위한 공부 중입니다.

앞으로도 더욱 효율적인 버전 관리 시스템을 구축하기 위한 여러 삽질을 해보려합니다. 버전 관리를 위한 여러 시도를 해본 분들, 새로운 아이디어가 있는 분들은 댓글로 공유해주시면 감사하겠습니다. 읽어주셔서 감사합니다.


커리어의 성장을 돕는 코멘토에서 언제나 함께 성장할 개발자를 기다리고 있습니다. 채용 페이지에서 코멘토가 어떤 회사인지, 어떤 사람을 찾는지 더 자세히 확인해보세요. 😊