본문 바로가기
2021~_Development Notes/ETC

Automatic PR Labeler 로 코드 리뷰 효율 높이기

by Indie Olineoguli 2023. 2. 19.

Hello & Intro

저는 이번 글을 통해 프론트엔드 팀의 코드 리뷰의 효율을 향상시키기 위해 구현했던

Automatic PR Labeler를 소개하고자 합니다.

 

이 Automatic PR Labeler(D-Day 라벨러)는

PR 코드 리뷰를 받기 위해 새 PR 을 올릴 때 D-Day 라벨을 추가해주고 일자가 바뀔 때마다 D-Day 를 차감시켜주는 기능입니다.

Problem

(혹시 깃(Git) 을 알고 계신가요? 모르신다면, 전체 글의 하단에 설명을 참고해주세요~.)

 

저희 프론트엔드 팀은

새로운 기능이나 변경점을 반영하기 전에 진행하는 코드 리뷰에서 문제가 있었습니다.

바로 새 버전 발행일에 리뷰 해야 할 PR 들이 몰려 양질의 코드 리뷰와 철저한 테스트를 위한 시간이 줄어든 것입니다. 😥

 

저희 회사는 매주 수요일에 새로운 버전을 실제 서비스에 반영하는 버전 발행 을 하는데요,

이를 위해서는

프로덕션(실제 유저들이 사용하는 서비스) 단계 전에

스테이징(실제 서비스와 유사한 환경) 서버에서 기능 및 UI 테스트 과정이 필요합니다.

이 테스트를 위해 각 PR들이 코드 리뷰를 받은 뒤 스테이징 서버에 반영되어야 하는 것입니다.

 

이상적으론, 각 PR들은 해당 PR의 작업 규모에 따라 충분한 테스트를 거친 후 프로덕션 서버에 반영되어야 합니다.

이유는 잘 아시겠지만, 실제 유저가 에러를 경험하지 않고 요청에 대해 즉각적인 응답을 받을 수 있어야

즉, ‘문제없이 편하게 그리고 쾌적하게’ 서비스를 사용할 수 있어야 저희가 흔히 말하는 UX, User Experience 가 향상되기 때문입니다.

 

그런데 개발 작업을 하다 보면 디자인, 기획 상 변경, 백엔드 작업(서버) 또는 프론트엔드 작업(클라이언트) 난이도에 따라

일정을 맞추기 힘든 경우가 발생하곤 합니다. 그로 인해 PR 을 늦게 올리게 되고, 개발 팀원들, 기획자 그리고 디자이너 분들의 리뷰를 받을 수 있는 시간이 적어지고 지연되는 것입니다.

 

또한 각 개발 팀원들의 개발 작업이 바빠 코드 리뷰를 늦게 해줄 수 밖에 없는 상황이 발생하기도 합니다.

결국, 수요일이 곧 늦게 퇴근하는 날😭 이 될 뿐만 아니라

실제 서비스의 안정성을 떨어뜨리는 치명적인 상황이 발생할 수도 있는 것입니다.

 

이를 해결하기 위해 앞서 언급했던 Automatic PR Labeler를 구현 및 적용했습니다.

팀원들이 PR 목록의 타이틀 옆에 붙어있는 라벨을 확인할 수 있기 때문에

신속히 리뷰를 달 수 있도록 유도하는 기능이라 이해하시면 좋을 것 같습니다.

(글 하단에 Automatic PR Labeler의 Repo 링크를 달아놓았습니다:) )

구현 과정 & 시행착오

원했던 기능은 두가지 였습니다.

첫 번째, PR 을 올릴 때마다 자동으로 D-Day 라벨을 붙여줄 것.

두 번째, 매일 D-Day 라벨을 하루씩 차감 시켜줄 것.

첫 번째, PR 을 올릴 때마다 자동으로 D-Day 라벨을 붙여줄 것.

이를 위해서 1) 함수(코드) 와 이를 실행시켜 줄 수 있는 2) Workflow (원하는 시점에 특정 코드가 실행되는 흐름) 가 필요합니다.

1) 함수

Automatic PR Labeler의 runAutomaticPRLabeler 함수는 코드를 보고 코드의 흐름 직접 따라가며 설명드리겠습니다.

전체 코드 보러 가기

 

해당 함수를 호출하면 마지막 줄의 runAutomaticPRLabeler 가 실행됩니다.

함수 내부에서 사용한 git 메소드들은 PR 이 올라갈 때의 Repo 그리고 Repo 내부의 PR 목록의 정보들을 가져와 확인할 수 있고, 이를 업데이트할 때 사용합니다.

 

runAutomaticPRLabeler는 크게 두 조건으로 작동합니다.

첫 번째, 새로운 PR에 D-Day 라벨을 붙여주는 경우

두 번째, 기존 PR의 D-Day 라벨 업데이트가 필요한 경우

입니다.

 

새로운 PR 을 올린 경우에 동작하는 코드는 아래와 같습니다.

const core = require("@actions/core");
const github = require("@actions/github");
...
if (!!pull_request?.number) {
    const prevLabels = pull_request.labels.map((v) => v.name); - (1)
    const isDDayLabelExist = prevLabels.find((v) => v[0] === "D"); - (2)

    if (isDDayLabelExist) return; - (3)
    await octokit.request( - (4)
      "POST /repos/{owner}/{repo}/issues/{issue_number}/labels",
      {
        owner: context.repo.owner,
        repo: context.repo.repo,
        issue_number: pull_request.number,
        labels: ["D-5", ...prevLabels],
      }
    );
    return;
  }

 

새로운 PR 의 경우 PR Number 가 생성되는데 이를 확인하여 if 문의 블록 내에 있는 코드가 실행됩니다.

  1. PR 을 올린 유저가 자체적으로 D-Day 라벨을 붙여주었는지 확인하기 위해 해당 PR의 라벨 이름들로 배열을 만들어 줍니다.
  2. 라벨 중 첫 글자가 D-Day 라벨의 첫 글자인 대문자 D와 같은 라벨이 있는지 찾습니다.
  3. (저희 프론트엔드 팀은 D-Day 라벨 만이 D로 시작하는 것으로 합의했습니다. )
  4. 만약 유저가 D-Day 라벨을 추가했다면 다음 동작 없이 runAutomaticPRLabeler 함수는 종료됩니다.
  5. 만약 D-Day 라벨을 추가하지 않았다면 깃에서 제공하는 request 메소드를 이용하여 디폴트인 D-5라벨과 유저가 추가한 다른 라벨들을 PR에 등록 해줍니다.

2) Workflow

PR 을 올릴 때마다 실행되는 github workflow를 만들어 주었습니다.

사용법은 간단합니다.

프로젝트 내부에서 [github 폴더 > workflows > dday_labeler.yml] 경로로

YAML 파일을 만들어주고 하단의 코드를 작성해 주면 됩니다.

// yaml 형식의 파일

name: PR Labeler
on:
  schedule:
    - cron: "Type UTC Time"
      branches:
        - "Type target Branch Name"
  pull_request:
    types: [opened]
    branches:
      - "Type target Branch Name"
jobs:
  Automatic-PR-labeler:
    runs-on: ubuntu-latest
    steps:
      - uses: JayKim88/automatic-pr-labeler@master
        with:
          token: ${{ secrets.Github-Token }}

 

그리고, cron: "Type UTC Time" 코드에 매일 라벨러가 동작하길 원하는 시간을 넣어주면 됩니다.

 

하지만, 역시 순탄치 않았습니다. git action 이 제공하는 cron의 경우, 요청 시간대에 전 세계 유저로부터 git action 요청이 발생하면 저희의 cron 요청이 무시되어 언제 스케줄에 맞게 실행될 지 모르는 문제가 있었습니다. (전 세계 유저들이 겪는 문제로, 여전히 발생하고 있습니다)

 

이를 해결하기 위해 Google Cloud Platform의 cloud storage와 cloud scheduler를 사용했습니다.

해당 내용은 하단에서 설명 드리겠습니다.

 

두 번째, 매일 D-Day 라벨을 하루씩 차감 시켜줄 것.

기존 PR의 라벨을 업데이트 해주기 위한 함수는 아래와 같습니다.

 

이 함수를 cloud storage에 파일 업로드하고

이를 지정한 시간에 (저희는 business day에 맞게 일요일-목요일 자정 0시로 설정했습니다.) cloud scheduler를 이용, http 요청하여 실행시키는 방법입니다.

const { Octokit } = require("@octokit/core");

const GH_TOKEN = "이 곳에 깃헙 토큰을 저장해주거나 다른 방식으로 토큰을 불러옵니다.";

exports.runAutomaticPRLabeler = async (req, res) => {
  if (req.method === "OPTIONS") { - (1)
    // Send response to OPTIONS requests
    res.set("Access-Control-Allow-Methods", "GET");
    res.set("Access-Control-Allow-Headers", "Content-Type");
    res.set("Access-Control-Max-Age", "3600");
    res.status(204).send("");
    return;
  }

  const octokit = new Octokit({
    auth: GH_TOKEN,
  });

  const prList = await octokit
    .request("GET /repos/{owner}/{repo}/pulls", {
      owner: "bold-9",
      repo: "ezstorage-frontend",
    })
    .then((v) => v.data);

  const prIssuesNeedLabelUpdate = prList.filter((v) => !v.draft); - (2)

  if (!prIssuesNeedLabelUpdate.length) return;

  const updateDDayLabelStatus = async (v) => {
    const prevLabels = v.labels.map((v) => v.name);
    const prevDDayLabels = prevLabels.filter((v) => v[0] === "D");

    if (!prevDDayLabels.length) return;

    const labelsExceptDDay = prevLabels.filter((v) => v[0] !== "D");
    const minDay = Math.min(...prevDDayLabels.map((v) => Number(v.slice(-1))));
    const shortestDDayLabel = prevDDayLabels.find( - (4)
      (v) => Number(v.slice(-1)) === minDay
    );

    const newDDay = Number(shortestDDayLabel.slice(-1)) - 1; - (5)
    const newDDayResult = newDDay >= 0 ? newDDay : 0;
    const newDDayLabel = "D-" + newDDayResult;
    await octokit.request( - (6)
      "PUT /repos/{owner}/{repo}/issues/{issue_number}/labels",
      {
        owner: "bold-9",
        repo: "ezstorage-frontend",
        issue_number: v.number,
        labels: [newDDayLabel, ...labelsExceptDDay],
      }
    );
  };

  try {
    await prIssuesNeedLabelUpdate.forEach((v) => { - (3)
      updateDDayLabelStatus(v);
    });
    res.status(200).send("All PRs Updated"); -(7)
  } catch (e) {
    res.status(410).send("Failed to update PRs");
  }
};

 

  1. CORS preflight인 ‘OPTION’ request인 경우, No Content를 의미하는 204 코드로 응답을 해줍니다.
  2. 실제 POST 요청이 오면 PR 리스트 중 D-Day 업데이트가 필요한[즉, Draft(리뷰가 불필요) 인 상태를 제외한] 목록을 구합니다.
  3. 이 목록(여기선 Array)이 준비가 되면 updateDDayLabelStatus 함수가 실행됩니다.
  4. D-Day 라벨 중(복수의 D-Day 라벨들이 존재할 수 있습니다.) 기간이 가장 짧게 남은 라벨을 구하고
  5. D-Day를 하루 차감해줍니다. 이때 그 값이 0일 미만인 경우엔 0으로 return 해줍니다.
  6. 새로운 D-Day 라벨과 함께 모든 라벨을 업데이트해줍니다.
  7. 위 과정이 끝나면 성공적으로 모든 PR의 라벨이 업데이트되었으므로 OK를 의미하는 200 코드로 응답을 해줍니다.

이로써,

새로운 PR의 D-Day 라벨을 생성할 때는 git action을 통해,

기존 PR의 D-Day 을 업데이트할 땐 gcp cloud storage 와 cloud scheduler로

PR 들의 D-Day를 관리할 수 있게 되었습니다.

 

결과 및 마무리 인사

그렇다면, 버전 발행일인 수요일엔 더 이상 수많은 PR 들을 리뷰할 필요가 없어졌을까요?😃

아니요, 그렇진 않습니다.🥲

하지만 Automatic PR Labeler를 도입한 후, 버전 발행일에 리뷰할 PR 의 개수가 도입 전과 비교해 현저히 감소했다고 말씀드릴 수 있겠습니다!

예를 들어 기존에 5 개의 PR 이 있었다면 → 2~3 개 정도로 (대략 40-50%) 점차 감소하고 있습니다.

 

유저에게 사용성이 높고 안정적인 서비스를 제공하기 위해선 그만큼 철저한 테스트가 필요합니다.

이 Automatic PR Labeler는 Production(유저가 사용하는 버전) 단계 전에 충분히 철저한 테스트를 하기 위한 PR 관리 도구로서 그 역할과 의의를 갖는다고 말씀드리고 싶습니다.

 

 

끝으로,

 

저에게 있어 Automatic PR Labeler 구현 및 적용 과정은

코드 리뷰 효율 향상으로 인한 뿌듯함뿐만 아니라

 

git method 사용법과 git action의 workflow 그리고

Google Cloud Service인 cloud storage 와 cloud scheduler를

직접 접하고 학습할 수 있었던 소중한 기회였습니다.

 

 

긴 글을 읽어주셔서 감사합니다 🙏🙏

 

 

 

Ref

Automatic PR Labeler Repo - https://github.com/JayKim88/automatic-pr-labeler

 

*Git 이란 ?

혼자 사용하기 위한 간단한 어플을 만들거나 다수의 Customer 를 위한 서비스 개발에 참여하는 경우,

개발을 위해 쓰여진 코드들, 그 코드들로 이뤄진 어플의 버전을 저장하고 관리하기 위해 분산 버전 관리 툴을 사용합니다.

저희 개발팀은 분산 버전 관리 툴 Git 을 사용하고 있습니다. (그 외에도 Mecurial, Bazaar, Darcs 등이 있습니다.)

 

Github 이라는 웹사이트에 Repository(원격 저장소, 이하 Repo) 를 만들고

이 곳에 어플의 코드들을 업로드하여 관리한다고 생각하시면 쉽습니다.

 

개발 팀원들은 이 어플을 본인의 Local 환경(컴퓨터)에 내려받아 개발 작업을 하는데요,

새로운 기능을 구현하거나 에러 수정 등 코드를 변경하면

본인의 컴퓨터에서 이 Repo 에 Pull Request

(이 코드를 실제 서비스 앱의 코드에 반영하기 위해 변경 부분 코드를 업로드하는 행위, PR) 를 합니다.

PR 이 올라오면, 보다 좋은 코드(복잡하지 않고 효율적인)를 서비스에 적용하기 위해 개발 팀원들이 양질의 코드 리뷰를 해줍니다.

(좋은 코드는 곧 서비스의 품질 ,즉 유저의 사용성에 직결되기 때문에 코드 리뷰는 소프트웨어 개발의 매우 중요한 부분이라 할 수 있습니다)