이런 것 있잖아요. 화요일 오후 리뷰 대기열에 도착합니다: "fixes"라는 제목의 pull request, 41개 파일에 걸쳐 2,347줄의 diff, 설명 없음, 그리고 댓글에 쾌활한 "PTAL". 마음이 가라앉습니다. 그래도 열어봅니다. 20분 후 같은 메서드를 세 번 읽었지만 어떤 문제를 해결하는지 여전히 알 수 없고, 하나의 긴장한 댓글만 남겼습니다: "전반적으로 좋아 보이는데, 이 변수 이름을 바꿀까요?" 이해하지 못한 코드에 고무 도장을 찍었습니다.
작성자는 악의가 없었습니다 — 아마도 집중하고 있었고, 작업이 자랑스러웠으며, 코드를 배포하는 것이 일의 반쪽일 뿐임을 잊었습니다. 나머지 반쪽은 리뷰받고, 이해받고, 안전하게 머지되는 것입니다. Pull request는 머지 전에 해치우는 형식이 아닙니다; 그것은 대화이며, 모든 대화처럼 누군가가 이해받으려는 노력을 기울일 때 더 잘 됩니다.
이 글은 그 노력에 관한 것입니다: 팀원들이 진정으로 즐겁게 리뷰하는 PR을 작성하는 방법 — 작고, 목적 있으며, 따라가기 쉽고, 승인하기 쉬운.
PR은 코드가 아닌 소통입니다
단 한 줄의 diff 이전에, pull request는 또 다른 사람에게 보내는 메시지입니다. 그 사람은 제한된 시간, 밤 11시에 무슨 생각을 하고 있었는지에 대한 제한된 맥락, 그리고 주의를 경쟁하는 다른 십여 가지가 있습니다. 작성자로서 당신의 일은 그들의 일을 쉽게 만드는 것입니다.
리뷰어를 판사가 아닌 청중으로 생각하세요. 그들은 당신을 잡으려는 게 아닙니다 — 당신이 견고한 것을 배포하도록 돕는 협력자입니다. 변경 뒤의 이유를 이해하도록 많이 도울수록, 리뷰는 더 빠르고 더 좋아집니다. 훌륭한 PR 설명은 코드에 대한 커버 레터와 같습니다: 장면을 설정하고, 동기를 설명하며, 리뷰어에게 정확히 무엇을 봐야 하는지 알려 줍니다.
이 마음가짐 전환이 모든 것을 바꿉니다. 자신을 위해 PR을 작성하는 것을 멈추고 리뷰어를 위해 작성하기 시작하면, 자연스럽게 더 작게 만들고, 더 잘 설명하며, 눈을 요청하기 전에 정리하고 싶어질 것입니다.
리뷰어가 변경이 왜 존재하는지 이해하기 위해 코드를 읽어야 한다면, PR 설명이 할 일을 다 하지 못한 것입니다. 코드는 무엇이 변경되었는지 설명합니다; 설명은 왜 변경이 필요했는지를 설명합니다.
작고 집중적으로 유지하세요
pull request를 개선하기 위해 할 수 있는 가장 효과적인 단 하나의 일은 더 작게 만드는 것입니다. PR이 클수록 덜 철저하게 리뷰되고, 머지하는 데 더 오래 걸리며, 더 많은 버그가 통과합니다.
좋은 PR은 하나의 논리적 변경을 담습니다. 하나의 티켓이 아니고, 하나의 기능도 아니며, 한 스프린트의 작업도 아닙니다 — 독립적으로 이해되고, 테스트되고, 롤백될 수 있는 하나의 일관된 아이디어. "사용자 아바타 업로드 추가"는 하나의 변경입니다. "사용자 아바타 업로드 추가, 프로필 컨트롤러 리팩터링, 세 가지 의존성 버전 업"은 하나의 코트를 입은 세 가지 변경입니다.
큰 작업을 나누는 방법
대형 기능이 큰 PR로 올 필요는 없습니다. 몇 가지 실용적인 분리 전략:
- 리팩터링과 동작 변경을 분리하세요. 첫 번째 PR: 이름 변경, 재구조화, 추출 — 새로운 로직 없음. 두 번째 PR: 실제 기능. 리뷰어는 리팩터링에 사이드 이펙트가 없음을 검증하고 나서 새 로직에만 집중할 수 있습니다.
- 인프라를 먼저 배포하세요. UI 없이 데이터베이스 컬럼, 마이그레이션, 타입을 추가하세요. 그런 다음 그것을 사용하는 UI를 추가하세요. 각 PR은 독립적으로 배포 가능합니다.
- 기능 플래그를 사용하세요. 기본적으로 꺼진 플래그 뒤에 미완성 작업을 머지하세요. PR은 작고 안전합니다; 기능은 플래그를 뒤집을 때까지 "완료"가 아닙니다.
- 스택된 PR. PR B는 PR A에 의존합니다. 일부 팀은 각 레이어를 독립적으로 리뷰하기 위해 스택된 PR 도구를 사용합니다. 팀이 스택킹을 사용하지 않는다면 단순하게 유지하세요: A를 먼저 완료하고 머지한 다음 B를 엽니다.
"and" 없이 한 문장으로 PR을 설명할 수 없다면, 아마도 하나 이상의 논리적 변경이 포함되어 있습니다. 이음새를 찾아서 분리하세요.
읽을 가치 있는 제목과 설명 작성하기
제목은 헤드라인입니다. 한 줄에 무엇이 변경되었고 왜 중요한지를 말해야 합니다. "update", "fix stuff", 또는 "changes" 같은 모호한 동사는 피하고 구체성을 목표로 하세요. 일반적인 PR 제목 문제와 수정 방법입니다:
| PR 문제 | 무엇이 잘못됐는가 | 더 나은 버전 |
|---|---|---|
| "fixes" | 아무런 맥락도 없습니다 — 리뷰어는 무슨 일인지 추측하려면 모든 줄을 읽어야 합니다. | "동시 세션 충돌 시 장바구니 결제의 경쟁 조건 수정" |
| "WIP: 여러 변경" | 작성자가 준비되지 않았음을 알립니다. 이런 용도로 Draft PR이 있습니다. | 진정으로 리뷰 준비가 될 때까지 GitHub Draft로 엽니다. |
| "Update UserService" | 어디인지 설명하지, 무엇인지 또는 왜인지를 설명하지 않습니다. 모든 PR은 무언가를 업데이트합니다. | "UserService에 이메일 변경 인증 플로우 추가" |
| "JIRA-1234" | 리뷰어가 동기를 이해하려면 두 번째 탭을 열어야 합니다. | "IP당 API 속도 제한 100 req/min으로 설정 (JIRA-1234)" — 티켓은 대체가 아닌 맥락으로. |
| "리팩터링 + 새 기능 + 의존성 업" | 트렌치코트를 입은 세 개의 PR. 각 관심사는 자체 리뷰를 받을 자격이 있습니다. | 세 개의 별도 집중된 PR로 분리합니다. |
설명이 진짜 소통이 일어나는 곳입니다. 좋은 설명은 모든 리뷰어가 처음 10초 안에 가지는 세 가지 질문에 답합니다:
- 무엇이 변경되었고 어디서?
- 왜 이 변경이 필요했는가 — 맥락, 버그, 사용자 스토리?
- 어떻게 검증하는가 — 테스트 단계, 스크린샷, 영향받는 엣지 케이스?
구체적으로 만들기 위한 before-and-after입니다:
--- 이전 (2,000줄의 "fixes" PR) ---
Title: fixes
Description: (비어 있음)
--- 이후 (리뷰어를 위해 작성된 같은 변경) ---
Title: 동시 세션에서 장바구니 결제의 경쟁 조건 수정
## 무엇
같은 사용자의 두 개의 동시 결제 요청이 둘 다
재고를 감소시키기 전에 재고 확인을 통과할 수 있어서
과매도가 발생합니다. 이 PR은 재고 감소 주위에
행 수준 DB 잠금을 추가합니다.
## 왜
고객 지원이 지난 30일간 12건의 과매도 사건을 지적했습니다 (Sentry
이슈 #4421 참조). 근본 원인은 재고 테이블의
SELECT와 UPDATE 사이에서 두 요청이 경쟁하는 것입니다.
## 테스트 방법
1. 새 동시성 회귀 테스트 실행: npm test -- cart.concurrency
2. 스테이징에서: 브라우저 탭 두 개를 열고, 재고 = 1인
제품에서 동시에 "구매"를 클릭합니다. 하나만 성공해야 합니다.
## 위험 / 참고
- 잠금은 단일 제품 행으로 범위가 지정됩니다; 다른 제품이 동시에
결제할 때 처리량에 영향을 미치지 않습니다.
- 마이그레이션 불필요 — 기존 테이블에서 SELECT FOR UPDATE 사용.
커밋 위생
커밋을 구성하는 방법이 중요합니다 — 단순히 프로세스를 만족시키기 위해서가 아니라, 커밋은 변경이 어떻게 이루어졌는지에 대한 영구적인 기록이기 때문입니다. 일 년 후, 누군가 (아마도 당신)가 한 줄에 git blame을 실행하고 커밋 해시를 따라 역사 속으로 갑니다. 그들이 찾는 것은 의식의 흐름이 아닌 이야기를 말해야 합니다.
좋은 커밋 메시지는 일반적인 형식을 따릅니다: 짧은 명령형 제목 줄 (50자 이하), 선택적 빈 줄, 그리고 왜를 설명하는 선택적 본문 — diff가 이미 보여 주는 것이 아닌.
# 일반적인 형식:
type(scope): 짧은 명령형 제목
선택적 본문: 이 변경이 왜 만들어졌는지, 어떤 문제를 해결하는지,
비자명적인 결정과 그 뒤의 추론.
# 실제 예시:
fix(cart): add row-level lock to prevent oversell on concurrent checkout
feat(auth): add email-change verification flow
refactor(profile): extract avatar upload to dedicated service
메시지 형식을 넘어서, 커밋 모양에 대해 생각하세요. 각 커밋은 하나의 일관된 단계를 나타내야 합니다 — 리팩터링, 테스트, 새로운 동작. 개별 커밋을 (전체 diff가 아닌) 보는 리뷰어는 논리적 진행을 따라갈 수 있어야 합니다: "메서드 추출 → 테스트 추가 → 동작 구현"은 마지막 순간에 스쿼시된 20개의 "WIP" 커밋보다 나은 이야기를 합니다.
PR을 열기 전에 지저분한 브랜치를 정리하기 위해 대화형 리베이스를 사용하세요. 처음부터 완벽한 커밋이 필요하지 않습니다 — 눈을 요청하기 전에 다듬으세요.
리뷰 요청 전 셀프 리뷰
팀원에게 알리기 전에, 자신의 첫 번째 리뷰어가 되세요. GitHub (또는 선호하는 코드 호스트)에서 diff를 열고 코드를 처음 보는 것처럼 읽으세요. 이 컨텍스트 전환은 예상보다 더 많은 문제를 잡습니다: 남아 있는 디버그 출력, 너무 커진 함수, 제거하는 것을 잊은 임포트, 한때 맞았던 댓글.
실행할 가치 있는 셀프 리뷰 체크리스트:
- 처음부터 끝까지 diff를 읽으세요. 훑어보지 마세요. 처음 보는 것처럼 가장하세요.
- 디버그 잔재물을 확인하세요.
console.log, "제거"로 표시된 TODO 댓글, 주석 처리된 블록, 임시 하드코딩된 값들. - 테스트가 있는지 확인하세요. 로직을 추가했다면, 테스트가 있나요? 버그를 수정했다면, 회귀 테스트가 있나요?
- 의도하지 않은 변경을 찾으세요. 공백 노이즈, 재포맷된 파일, 우발적인 범위 확장. 별도 PR로 옮기거나 되돌리세요.
- 직접 안내 댓글을 남기세요. 섹션이 자명하지 않다면, 리뷰어가 보기 전에 추론을 설명하는 PR 댓글을 추가하세요. 이것은 약함의 표시가 아닙니다 — 배려입니다.
시니어 엔지니어가 지금 당장 당신의 설명 없이 PR을 콜드 리뷰하는 것이 편안하지 않다면 — 아직 내보낼 준비가 되지 않은 것입니다. 먼저 고치고, 그런 다음 리뷰를 요청하세요.
셀프 리뷰는 이기적인 배당금도 지불합니다: "리뷰 요청" 버튼을 누르고 10초 후에 발견하게 되는 당혹스러운 실수를 미리 잡습니다. 모든 사람이 그 오타에 대한 이야기를 갖고 있습니다. 10분의 셀프 리뷰가 그런 것들의 대부분을 없애 줍니다.
리뷰에 응답하기
PR은 코드를 푸시할 때 끝나는 것이 아닙니다 — 머지될 때 끝납니다. 그 두 이벤트 사이에는 리뷰 사이클이 있으며, 피드백을 처리하는 방법이 코드 자체만큼이나 중요합니다.
리뷰를 이동시키고 관계를 온전히 유지하는 몇 가지 원칙:
- 모든 댓글에 답하세요. 단지 "완료" 또는 "최신 커밋에서 수정했습니다 — 로직을 헬퍼로 이동하고 테스트 추가"라도. 침묵은 리뷰어들이 메모를 봤는지 궁금하게 만듭니다.
- 수정을 푸시할 때 설명하세요. "최신 커밋에서 수정 — 로직을 헬퍼로 이동하고 테스트 추가"는 맥락 없는 새 커밋보다 훨씬 더 명확합니다.
- 스레드를 사려 깊게 해결하세요. 대부분의 팀에서, 댓글 작성자는 만족했을 때 해결합니다. 피드백을 인정하지 않고 셀프 해결하지 마세요. 관례가 불확실하다면, 물어보세요.
- 정중하게 반박하세요. 제안이 틀렸다고 생각하면, 추론과 함께 명확히 말하세요 — 그냥 무시하지 마세요. "그 접근법을 고려했지만 [이유] — 여전히 강하게 느끼신다면 기꺼이 논의하겠습니다"는 대화를 전문적이고 생산적으로 유지합니다. 친절하게 코드 리뷰하는 방법에서 같은 대화에 대한 리뷰어의 관점도 볼 수 있습니다.
- 리뷰를 명시적으로 다시 요청하세요. 피드백을 처리한 후, 리뷰어들이 알아채기를 기다리지 마세요. 리뷰를 다시 요청하고 짧은 요약을 남기세요: "모든 댓글을 처리했습니다. 주요 변경: 유틸을 추출하고, 두 가지 테스트를 추가했으며, X는 Y 때문에 원래 접근법을 유지했습니다."
팀 크기에 따른 PR 규범의 확장
훌륭한 PR이 무엇인지는 고정되어 있지 않습니다 — 팀이 성장하고 코드베이스가 성숙함에 따라 바뀝니다. 기대치가 어떻게 발전하는지입니다:
| 팀 맥락 | 가장 중요한 것 | 실용적인 규범 |
|---|---|---|
| 솔로 / 프리랜서 | 미래의 당신이 리뷰어입니다. PR은 여섯 달 후에 코드로 돌아올 때를 위한 결정 문서입니다. | 리뷰어가 없더라도: 명확한 제목, 설명에 간단한 "이유". 나중에 git log를 읽을 수 있게 하는 커밋 메시지. |
| 소규모 팀 (2–8명) | 속도가 중요합니다. 모두가 코드베이스를 알고 있습니다. 리뷰가 대화식입니다. 신뢰가 높습니다. | PR을 작고 빠르게 유지하세요. 설명은 더 가벼울 수 있습니다 — 한두 문장의 맥락. 당일 비동기 리뷰가 합리적인 기대입니다. |
| 중간 규모 팀 (10–50명) | 리뷰어가 전체 맥락을 모를 수 있습니다. 온보딩이 지속적입니다. 변경이 팀을 걸칩니다. | 전체 what/why/how 설명. UI 작업의 스크린샷. 테스트 플랜. 크로스 팀 변경은 추가 주의와 가능하면 더 넓은 리뷰가 필요합니다. |
| 대규모 / 기업 (50명 이상) | 리뷰어가 때로는 낯선 사람입니다. 컴플라이언스, 감사 추적, 정확성 리뷰가 중요합니다. 처리 시간 SLA가 있습니다. | 도구로 강제되는 공식 PR 템플릿. 필수 승인. 민감한 변경에 대한 보안 리뷰. 설계 문서와 티켓 링크. 위험한 변경에 대한 롤백 플랜. |
네 명의 스타트업 팀은 모든 PR 설명에 롤백 플랜이 필요하지 않습니다. 은행은 그렇습니다. 위험 부담에 의례를 맞추고, 팀이 성장하면서 규범을 재검토하세요 — 열 명의 엔지니어에게 효과적인 것이 오십 명에게는 부족할 것입니다.
핵심 요약
- PR은 사람에게 보내는 메시지입니다. 자신이 아닌 리뷰어를 위해 작성하세요. 그들의 시간과 맥락은 제한됩니다.
- 작고 집중적으로 유지하세요. PR당 하나의 논리적 변경. 동작 변경과 리팩터링을 분리하세요. 기능 플래그를 사용해 미완성 작업을 기본적으로 꺼진 토글 뒤에 안전하게 머지하세요.
- 제목 + 설명 = 방향 설정. 무엇이 변경되었는지, 왜 변경이 필요했는지, 어떻게 검증하는지에 답하세요. 코드는 무엇을 보여 줍니다; 설명은 이유를 제공합니다.
- 깔끔한 커밋 기록이 이야기를 합니다. 일반적인 메시지, 논리적 커밋, 리뷰를 요청하기 전 깔끔한 리베이스.
- 먼저 셀프 리뷰하세요. 다른 사람이 하기 전에 GitHub에서 자신의 diff를 읽으세요. 안내 댓글을 남기세요. 명백한 실수를 스스로 잡으세요.
- 피드백 루프를 닫으세요. 모든 댓글에 답하고, 메모를 처리한 후 리뷰를 다시 요청하며, 추론과 함께 반박하세요 — 침묵으로는 절대로.
- 팀 크기에 의례를 맞추세요. 네 명의 스타트업과 500명의 은행은 다른 PR 규범이 필요합니다. 성장하면서 검토하세요.
다음 단계는 테이블의 다른 쪽입니다: 정성껏 만든 PR을 받은 사람이 도움이 되는 피드백을 주는 방법. 정성껏 만든 PR을 냈는데 "이건 틀렸어요, 다시 작성하세요"라는 짧은 답을 받은 경험이 있다면, 잘 전달되는 피드백 — 이 글의 후속편 — 을 감사히 여길 것입니다.