본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Tools

코드 리뷰 원칙과 체크리스트

작성 2026. 10. 11.추가
목차14개 항목

코드 리뷰는 내가 쓴 코드의 문제를 혼자서는 다 찾을 수 없기 때문에 하는 일이다. 누구의 코드든 다른 사람의 눈을 거치면 사용자에게 닿기 전에 더 많은 버그를 잡을 수 있다. 완벽한 단 하나의 프로세스는 없으니 팀에 맞는 방식을 찾되, 정기적으로 하는 것이 가장 중요하다.

백명석의 「한번 듣고 평생 써먹는 코드 리뷰 노하우」(OKKY 세미나, 2022.01) 강연을 함께 정리했다. 체크리스트는 Ryan McDermott의 code-review-tips를 바탕으로 재구성했다. 팀 정착 부분은 「효과적인 코드 리뷰를 위해서」, 「개발자 Code Review 가이드」를 바탕으로 보강했다.

왜 코드 리뷰를 하나

관찰의미
출시를 거듭할수록 개발자는 늘어도 생산성은 떨어진다(『클린 아키텍처』 1장)설계가 망가지면 새 기능보다 기존 코드와 씨름하는 데 시간을 쓴다
설계 체력 가설(Design Stamina Hypothesis)좋은 설계 없이 빨리 가면 어느 시점부터 누적 기능 속도가 꺾인다. 빨리 가는 유일한 방법은 잘 가는 것이다
소프트웨어는 소스 코드가 곧 설계다다이어그램만으로는 같은 결과물이 나오지 않는다. 설계를 잘하려면 코드를 잘 써야 한다
유지보수가 전체 비용의 대부분이고, 코드는 쓰는 것보다 읽는 데 훨씬 많은 시간이 든다읽기 쉬운 코드가 비용을 줄인다
소프트웨어는 품질이 높을수록 오히려 싸다다른 분야와 달리 품질과 비용의 트레이드오프가 뒤집힌다. 경영진은 장인 정신이 아니라 변경 비용으로 설득한다

그래서 코드 리뷰는 결함을 미리 찾는 것을 넘어 다섯 가지를 얻는 실천이다.

  • 품질: 배포 전에 결함을 잡고, 아키텍처 품질 속성을 개선해 이후 변경 비용을 낮춘다.
  • 학습과 지식 전달: 리뷰는 시니어가 일방적으로 가르치는 자리가 아니라 주고받는 자리다. 리뷰어도 설명하는 법과 공감하는 법을 배운다.
  • 동기 부여: 다른 팀 리뷰까지 열어 두면 잘하는 사람을 보고 따라 하고 싶어진다.
  • 공동 책임과 결속: 리뷰한 사람도 책임을 느끼고(공동 코드 소유), 옆 동료가 무엇을 하는지 알게 된다.
  • 비난이 아니라 학습: 목적은 누가 잘못했는지가 아니라 코드를 낫게 만드는 것이다.

작성자와 리뷰어가 할 일

작성자가 고생해서 리뷰어 여러 명의 시간을 아껴 준다. 그래야 리뷰가 선순환한다.

작성자리뷰어
빌드·테스트가 통과하는 PR만 올린다. IDE 검사로 기계가 찾는 결함은 미리 없앤다받으면 바로 시작한다. 한 라운드가 하루를 넘지 않게 한다
PR은 리뷰어가 한 번에 집중해 볼 수 있을 만큼 작게 만든다고수준에서 저수준으로 내려간다. 요구사항 충족 → 구조·설계 → 세부 구현 → 스타일 순서다. 한 번에 코멘트가 20개를 넘으면 위험 신호다
변경 설명에 무엇을 왜 바꿨는지, 특히 봐 줬으면 하는 곳, 더 나은 방법이 궁금한 곳을 적는다변경 범위 밖은 지적하지 않는다. 변경 때문에 함께 고쳐야 하는 곳만 말한다
올리기 전에 스스로 먼저 읽고 설명이 필요한 곳에 PR 코멘트를 단다예시 코드에 관대하되 다 짜 주지 않는다. 코드 조각 하나가 긴 설명보다 의도를 정확히 전한다
리뷰어를 넉넉히 포함한다. 보는 눈이 많을수록 버그를 더 찾고, 작성자도 더 신경 쓴다사람이 아니라 코드에 대해, 친절하게 말한다. 코멘트 쓰는 요령은 리뷰 코멘트와 PR 소통에 있다

사람이 볼 것과 아닌 것

  • 자동화할 수 있는 것은 사람이 보지 않는다. 포맷, 세미콜론, let/var 같은 논쟁은 formatter와 linter(ESLint, Prettier)에 맡긴다. 리뷰 시간은 기계가 못 보는 것에 쓴다.
  • 설계 논의는 코드를 쓰기 전에 한다. 콘크리트를 부은 뒤에 기초 공사를 논하지 않는다. API·구조에 대한 큰 결정은 PR 이전(설계 문서, 이슈)에서 합의한다.

체크리스트

가독성

볼 것예
오타사소하지만 남으면 검색·이해를 방해한다. 친절하게 수정 제안
이름이 의도를 드러내는가u(names) → namesToUpperCase(names)
함수가 한 가지 일만 하는가"활성 고객 판별"과 "이메일 발송"이 한 함수에 섞여 있으면 분리
파일(모듈)이 응집되어 있는가가짜 이름 생성기 파일 안에 DB 조회 함수가 있으면 분리
공개 함수에 설명이 있는가라이브러리처럼 쓰일 export 함수는 역할·사용법 주석
복잡한 로직에 주석이 있는가비트 연산처럼 이름만으로 설명되지 않는 부분은 "왜"를 주석으로
// Before: 두 가지 일을 한다
function emailClients(clients) {
  clients.forEach((client) => {
    const record = database.lookup(client)
    if (record.isActive()) email(client)
  })
}
 
// After: 판별과 발송을 나눈다
const isActiveClient = (client) => database.lookup(client).isActive()
function emailActiveClients(clients) {
  clients.filter(isActiveClient).forEach(email)
}

부작용

  • 함수는 가능한 한 순수하게. 전역 변수를 읽고 바꾸는 함수는 다른 코드를 예측할 수 없게 깨뜨린다. 필요한 값은 인자로 받고 결과는 반환한다.
  • I/O는 실패를 처리하는가. 파일·네트워크 요청이 실패(reject)하면 무엇을 반환하고 사용자에게 무엇을 보여 줄지 정해져 있어야 한다.
// Before: 전역 name을 문자열에서 배열로 바꿔 버린다
let name = 'Ryan McDermott'
function splitIntoFirstAndLastName() {
  name = name.split(' ')
}
 
// After
function splitIntoFirstAndLastName(fullName) {
  return fullName.split(' ')
}

경계 조건

  • 빈 값(null·빈 배열): 데이터가 하나도 없을 때 화면에 무엇을 보여 주는가?
  • 아주 많은 값: 1만 개가 오면? 페이지네이션이나 무한 스크롤이 필요한가?
  • 딱 하나일 때: "1 dollars"처럼 단수·복수 처리가 어긋나지 않는가?
  • 사용자 입력 크기 제한: 메시지 본문이 수 MB라면 그대로 DB에 저장할 것인가?
  • 예상 못한 입력: 금액 자리에 문자열이 오면? 클라이언트 검증만 믿지 말고 서버에서도 검증한다.
router.post('/transfer-money', (req, res) => {
  const amount = Number(req.body.amount)
  if (!Number.isFinite(amount) || amount <= 0) {
    return res.status(400).json({ message: 'Invalid amount' })
  }
  transferMoney(user.id, req.body.to, amount)
})

보안

  • XSS: 사용자 입력(쿼리 파라미터 포함)을 innerHTML로 그대로 넣지 않는다. textContent를 쓰거나 이스케이프한다. 자세한 내용은 웹 보안.
  • 개인정보(PII) 노출: 주민번호·카드번호 같은 값을 URL 쿼리 파라미터, 로그, 분석 도구로 보내지 않는다. URL은 서버 로그·브라우저 기록·중간자에게 그대로 남는다.
  • 이 목록은 최소한일 뿐이다. 보안 검사는 커밋마다 자동화하고 정기적으로 감사한다.

성능

  • 알맞은 자료구조: 배열을 매번 끝까지 도는 대신 Set·Map으로 바로 찾을 수 있는지 본다.
// Before: O(n) 탐색 + 찾아도 계속 돈다
function isUserMentioned(mentions, user) {
  let mentioned = false
  mentions.forEach((m) => { if (m.user === user) mentioned = true })
  return mentioned
}
 
// After: 조회가 잦다면 Set으로 O(1)
const mentionedUsers = new Set(mentions.map((m) => m.user))
const isUserMentioned = (user) => mentionedUsers.has(user)
  • 중요한 작업은 기록되는가: 오래 걸리거나 비즈니스에 중요한 작업은 앞뒤로 로그·지표를 남겨 소요 시간을 측정한다(개인정보는 빼고).

테스트

  • 새 코드에는 테스트가 있어야 한다. 버그 수정이면 그 버그가 고쳐졌음을 증명하는 테스트를, 기능 추가면 단위 테스트와 다른 부분과 잘 맞물리는지 보는 통합 테스트를.
  • 함수가 하는 모든 일을 테스트하는가? (반환값만 확인하고 콜백 호출은 확인하지 않는 식의 누락)
  • 엣지 케이스: 날짜에 음수·소수·아주 큰 일수를 더하면?

기타

  • TODO 주석은 추적한다. 이슈 트래커에 등록하고 번호를 함께 적어 두어야 언젠가 정리된다.
  • 커밋 메시지는 변경을 정확히 설명한다. "고침", "하나만 더" 같은 메시지는 나중에 git blame으로 찾아왔을 때 아무 도움이 안 된다. 관련 이슈 번호를 함께 적는다. 규칙은 커밋 컨벤션 참고.
  • 코드가 해야 할 일을 하는가. 너무 당연해서 오히려 잊기 쉽다. 세부 지적에 빠져 요구사항 자체를 확인하지 않는 일이 없도록 한다.

더 읽을거리

리뷰를 팀에 정착시키기

상황방법
리뷰가 밀린다"괴로운 일일수록 더 자주 한다." 매일 오전·오후 30분씩 리뷰 시간을 팀 일정으로 박아 둔다. 리뷰가 늦는 것은 개인 문제가 아니라 조직이 리뷰를 중요하게 보지 않는 문제다
PR이냐 페어 프로그래밍이냐PR은 처리량이 좋지만 지연이 길고, 페어 프로그래밍은 지연이 없지만 둘이 묶인다. 내성적이고 비동기에 익숙한 팀은 PR이, 즉각 대화를 선호하는 팀은 페어가 맞다. 글로 설명이 길어지면 만나서 같이 본다
의견이 맞서 교착된다텍스트로 계속 다투지 말고 화상이나 대면으로 이야기한다. 그래도 안 되면 인정(치명적이지 않으면 넘어간다)하거나 팀장에게 에스컬레이션한다. 오늘의 품질보다 계속 함께 일할 관계가 남는다
완벽한 설계를 요구하고 싶다완벽이 아니라 지금 할 수 있는 최선을 고른다. 지적한 설계 문제가 실제 문제가 되는지는 측정해 봐야 안다
무엇이 반려 사유인지 매번 다툰다실패한 테스트, 스타일 위반, PR 규칙(크기·설명) 위반처럼 반려 기준을 미리 합의해 둔다. 필수가 아닌 제안은 Nit:을 붙여 작성자가 고르게 한다
「나중에 정리할게요」나중은 대개 오지 않는다. 리뷰에서 나온 정리는 머지 전에 끝내는 것을 기본으로 하고, 정말 미룰 때는 이슈로 남긴다
리뷰어가 코드를 이해하지 못한다이해하지 못한 코드는 승인하지 않는다. 진전이 없으면 그 영역을 아는 리뷰어를 찾는다
리뷰가 품질을 높이는지 모르겠다결함 추이와 함께 배포 빈도, 변경 리드 타임, 변경 실패율, 복구 시간(MTTR)을 본다. 개별 수치보다 추세를 보고, 커밋마다 품질 지표를 자동으로 알려 즉시 고치게 한다
처음 도입한다리더의 관심과 의지가 필요하다. 바로 효과가 나지 않으니 익숙해질 때까지 꾸준히 투자하고, 좋은 리뷰 사례를 공유하고, 리팩터링·도구 사용을 직접 보여 줘 따라 하고 싶게 만든다

리뷰를 해도 버그는 생긴다. 리뷰는 버그가 없음을 증명하지 못하고, 결함을 찾을 확률을 높일 뿐이다. 리뷰에서 자주 언급되는 기법을 익히려면 『리팩터링』, 『레거시 코드 활용 전략』, 『단위 테스트』가 좋다.

핵심 정리

  • 리뷰는 품질·학습·동기·공동 책임을 얻는 실천이다. 소프트웨어는 품질이 높을수록 변경 비용이 싸다.
  • 작성자가 먼저 읽고 설명해 리뷰어의 시간을 아끼고, 리뷰어는 바로 시작해 하루 안에 끝낸다.
  • 스타일은 도구에, 설계는 PR 전에, 리뷰는 기계가 못 보는 것에.
  • 가독성 → 부작용 → 경계 조건 → 보안 → 성능 → 테스트 순으로 훑는다.
  • 친절하게, 고수준부터, 예시 코드와 함께.

관련 노트: 프론트엔드에서의 TDD, 웹 보안, 코드 리뷰에서 자주 나온 지적, 친절한 엔지니어링, 페어 프로그래밍, 가독성을 높이는 전략

원본 기록6

댓글

GitHub 계정으로 댓글을 남길 수 있어요.