📝

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

Created
2023/02/10 00:47

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; }
JavaScript
복사
새로운 PR 의 경우 PR Number 가 생성되는데 이를 확인하여 if문의 블록 내에 있는 코드가 실행됩니다.
1.
PR 을 올린 유저가 자체적으로 D-Day 라벨을 붙여주었는지 확인하기 위해 해당 PR의 라벨 이름들로 배열을 만들어 줍니다.
2.
라벨 중 첫 글자가 D-Day 라벨의 첫 글자인 대문자 D와 같은 라벨이 있는지 찾습니다.
(저희 프론트엔드 팀은 D-Day 라벨 만이 D로 시작하는 것으로 합의했습니다. )
3.
만약 유저가 D-Day 라벨을 추가했다면 다음 동작 없이 runAutomaticPRLabeler 함수는 종료됩니다.
4.
만약 D-Day 라벨을 추가하지 않았다면 깃에서 제공하는 request 메소드를 이용하여 디폴트인 D-5라벨과 유저가 추가한 다른 라벨들을 PR에 등록 해줍니다.

2) Workflow

PR 을 올릴 때마다 실행되는 github workflow를 만들어 주었습니다.
사용법은 간단합니다. 프로젝트 내부에서 [github 폴더 > workflows > dday_labeler.yml] 경로로 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 }}
YAML
복사
그리고, 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"); } };
JavaScript
복사
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를 직접 접하고 학습할 수 있었던 소중한 기회였습니다.
긴 글을 읽어주셔서 감사합니다.
Automatic PR Labeler Repo: 제이킴의 automatic-pr-labeler