Menu

코드 리뷰 프롬프트: AI로 코드 리뷰하는 법

좋은 코드 리뷰 프롬프트는 무엇을 볼지 말하고, 모든 지적에 심각도를 요구하고, 문제를 일으키는 구체적인 입력과 diff를 요청하고, 스타일은 건너뛰라고 합니다. 그다음 각 지적을 직접 확인하세요. 일부는 틀릴 테니까요.

이 페이지의 모든 프롬프트는 수정한 뒤 ChatGPT, Claude 등 AI 앱에서 바로 열 수 있습니다.

코드 리뷰 프롬프트는 모델에게 무엇을 볼지, 각 지적이 얼마나 심각한지, 어떤 증거를 댈지 알려 줘야 합니다. "이 코드 리뷰해 줘"라고 하면 친절한 제안 목록이 나오는데, 거기서 보안 구멍이 docstring 조언과 이름 취향 사이에 끼어 있습니다. 체크리스트, 심각도 등급, diff 요청이 있는 리뷰 프롬프트는 지적 하나씩 실행에 옮길 수 있는 보고서를 받습니다.

"리뷰해 줘"에서 심각도가 있는 체크리스트로

두 탭의 코드에는 실제 버그가 두 개 있습니다. 각 답변이 그것을 어떻게 보여 주는지 비교해 보세요.

이 코드 리뷰해 줄 수 있어? def get_user(conn, username): cur = conn.cursor() cur.execute(f"SELECT id, email FROM users WHERE name = '{username}'") row = cur.fetchone() return {"id": row[0], "email": row[1]}
Try it
Example replyReplies vary between models and runs.

전반적으로 함수가 짧고 따라가기 쉽습니다. 몇 가지 제안을 드리면:

  • 타입 힌트를 추가하세요. 예를 들어 username: str을 쓰면 시그니처가 더 명확해집니다.
  • docstring을 추가하세요. 함수가 무엇을 반환하는지 설명해 주세요.
  • 매개변수화된 쿼리를 쓰세요. f-string으로 SQL을 만들면 SQL 인젝션으로 이어질 수 있습니다.
  • 없는 사용자를 처리하세요. fetchone()은 None을 반환할 수 있습니다.
  • 이름: 데이터베이스 접근을 더 잘 나타내려면 fetch_user가 나을 수 있습니다.

첫 번째 답도 두 버그를 모두 찾았고, 바로 그것이 리뷰를 언급한 내용으로 판단하는 것의 문제입니다. 인젝션은 다섯 개 중 세 번째에 있고, "이어질 수 있습니다"로 누그러졌고, 수정도 없이 docstring 조언 옆에 놓여 있습니다. 훑어보는 사람이라면 타입 힌트나 추가하고 넘어갈 것입니다.

두 번째 프롬프트는 네 가지를 바꿨습니다.

  • 범위. "버그, 보안, 에러 처리만. 스타일은 건너뛰어"가 잡음을 없앱니다. 스타일은 린터와 포매터의 몫이고, 그 도구들은 더 빠르고 스스로와 의견이 엇갈리는 일도 없습니다.
  • 심각도. 모델이 순위를 매기게 하고, 순위가 매겨진 목록은 어디서 시작할지 알려 줍니다.
  • 문제를 일으키는 입력. "x' OR '1'='1"은 막연한 위험을 실행해 볼 수 있는 시연으로 바꿉니다. 마지막 섹션에서 보듯, 오탐을 거르는 최고의 필터이기도 합니다.
  • diff. 지적마다 작은 diff가 있으면 따로 적용하거나 거절할 수 있습니다. 다시 쓴 함수는 모든 수정을 아무도 요청하지 않은 변경과 섞어 버립니다.

"어떤 심각도 등급에 해당하는 것이 없으면 지어내지 마"는 보기보다 중요합니다. 리뷰를 요청받은 모델은 찾을 것이 별로 없어도 지적을 만들어 내는 경향이 있고, 가장 약한 지적들이 목록을 부풀립니다. 어떤 등급에 아무것도 보고하지 않아도 된다는 명시적인 허락이 그 부풀림을 줄여 줍니다.

리뷰어에게 필요한 맥락

사람 리뷰어는 코드가 무엇을 위한 것이고 입력이 어디서 오는지 압니다. 모델은 붙여 넣은 것만 압니다. "username은 로그인 폼에서 그대로 들어와"가 SQL 인젝션을 이론적인 지적이 아니라 심각도 높은 지적으로 만드는 문장입니다. 리뷰에 쓸모 있는 맥락은 다음과 같습니다.

  • 입력이 어디서 오는지: 사용자, 다른 내부 서비스, 직접 관리하는 설정 파일.
  • 무엇이 코드를 호출하는지와 무엇을 돌려받기를 기대하는지.
  • 실행 환경: 언어 버전, 프레임워크, 데이터베이스 드라이버. 위 인젝션의 수정이 ?를 쓰는 것은 sqlite3가 그 자리 표시자를 쓰기 때문이고, 다른 드라이버 중에는 %s를 쓰는 것도 있습니다.
  • 이번 변경에서 가장 중요한 것: 정확성, 보안, 성능, 또는 셋 다.

재사용할 수 있는 리뷰 프롬프트

이 블록은 체크리스트를 템플릿으로 만든 것입니다. 언어, 초점, 코드를 바꾸고 규칙은 그대로 두세요. 제약 조건 부분을 꺼 보면 그것 없이 잡음이 얼마나 돌아오는지 빠르게 확인할 수 있습니다.

코드 리뷰 템플릿
Fill in
Parts
너는 시니어 JavaScript 리뷰어야. 실제 문제만 보고하고 그 밖의 것은 보고하지 않아.
버그와 에러 처리에 집중해서 아래 코드를 리뷰해 줘.
이 코드는 브라우저에서 실행돼. `ids`는 우리 API에서 받은 숫자 id 배열이야.
async function loadAll(ids) { const results = []; ids.forEach(async (id) => { const res = await fetch(`/api/items/${id}`); results.push(await res.json()); }); return results; }
지적마다 심각도(높음, 중간, 낮음), 해당 줄, 문제를 일으키는 입력이나 상황, 최소한의 diff. 가장 심각한 것부터.
스타일, 이름, 포맷팅에 대한 언급은 하지 마. 지적이 실제 문제인지 확실하지 않으면 그렇다고 말하고, 무엇이 그것을 확인해 줄지 말해 줘. 아무것도 찾지 못하면 "지적 없음"이라고 말해.
Try it
Example replyReplies vary between models and runs.

1. 높음: 요청이 하나도 끝나기 전에 함수가 반환함 (3번 줄) forEach는 async 콜백을 기다리지 않습니다. loadAll은 빈 results 배열을 곧바로 반환하고, push는 나중에 일어납니다. 호출하는 쪽은 모두 []를 받습니다.

2. 중간: 실패한 요청이 데이터로 취급됨 (5번 줄) fetch에서 404나 500 응답은 예외가 아닙니다. JSON 에러 본문은 파싱되어 항목인 것처럼 push되고, JSON이 아닌 본문이면 아무도 await하지 않는 콜백 안에서 res.json()이 예외를 던집니다.

둘 다 고치는 수정이며, 결과를 ids와 같은 순서로 유지해 줍니다.

 async function loadAll(ids) {
-  const results = [];
-  ids.forEach(async (id) => {
-    const res = await fetch(`/api/items/${id}`);
-    results.push(await res.json());
-  });
-  return results;
+  return Promise.all(
+    ids.map(async (id) => {
+      const res = await fetch(`/api/items/${id}`);
+      if (!res.ok) throw new Error(`Item ${id}: HTTP ${res.status}`);
+      return res.json();
+    })
+  );
 }

역할 줄은 기준("실제 문제만 보고하고 그 밖의 것은 보고하지 않아")을 정하는데, 역할 프롬프팅이 가장 도움이 되는 곳이 바로 여기입니다. 풀 리퀘스트에 지적을 댓글로 다는 스크립트처럼 프로그램이 리뷰를 읽는다면, 산문 대신 필드가 고정된 JSON을 요청하세요. 방법은 구조화된 출력에서 보여 줍니다.

오탐 다루기

일부 지적은 틀릴 것입니다. 모델은 입력을 검증하는 호출부, 어떤 경우를 불가능하게 만드는 데이터베이스 제약, 이상해 보이는 줄이 의도된 이유를 볼 수 없습니다. 라이브러리의 동작에 대해 틀릴 수도 있습니다. 자신 있는 주장이 확인된 주장은 아닌 이유는 AI 할루시네이션에서 설명합니다.

각 지적을 그 유발 입력으로 테스트하세요. 여러분의 시스템에서 문제에 도달하는 입력을 만들 수 없다면, 수정을 받아들이지 말고 빠진 맥락을 들어 반박하세요.

지적에 반박하기
`cents`가 undefined이거나 문자열일 수 있다는 이유로 이 함수를 지적했지: function formatPrice(cents) { return `$${(cents / 100).toFixed(2)}`; } 이 함수는 `renderCart`에서만 호출되고, `renderCart`는 `Number.isInteger(cents)`가 참이 아니면 일찍 반환해. 그래도 여전히 문제야? 아니라면 지적을 철회해 줘.
Try it
Example replyReplies vary between models and runs.

그 검사가 있다면 문제가 아닙니다. formatPrice는 항상 정수만 받으므로 이 지적은 해당되지 않고, 철회하겠습니다.

다만 그 검사가 다루지 않는 것이 하나 있습니다. 음수 정수도 Number.isInteger를 통과하고, formatPrice(-500)은 "$-5.00"을 돌려줍니다. 환불이나 할인이 이 함수에 도달할 수 있다면 "-$5.00"이 더 나을 수 있습니다. 그럴 일이 없다면 바꿀 것은 없습니다.

새로운 증거를 본 모델은 지적을 철회하거나, 그 증거를 고려해도 왜 여전히 문제인지 설명해야 합니다. 둘 다 쓸모 있습니다. 모든 반박에 그냥 동의하는 모델은 리뷰를 하는 것이 아니므로, 동의했는지가 아니라 그 이유로 답변을 판단하세요. 모델이 직접 쓴 코드라면 새 대화에서 리뷰를 실행하세요. 그래야 코드를 만든 바로 그 추론을 통해 리뷰가 읽히지 않습니다. 범위, 맥락, 테스트라는 같은 습관이 애초에 코드를 더 좋게 만들어 줍니다. 코드 작성 프롬프트를 참고하세요.

자주 묻는 질문

코드 리뷰에 좋은 프롬프트는 무엇인가요?

무엇을 리뷰할지(버그, 보안, 에러 처리) 밝히고, 지적마다 심각도를 요청하고, 문제를 일으키는 구체적인 입력과 그것을 고치는 diff를 요구하세요. 요청하지 않는 한 포맷팅과 이름 짓기는 건너뛰라고 하세요. 사람 리뷰어가 가질 맥락, 즉 코드가 무엇을 위한 것이고 무엇이 그 코드를 호출하는지도 주세요.

AI가 사람의 코드 리뷰를 대신할 수 있나요?

아니요. 하지만 쓸모 있는 1차 검토입니다. 빠르고, 처리하지 않은 None, 빠진 await, 문자열로 만든 SQL 같은 흔한 버그 패턴을 잘 잡습니다. 하지만 제품의 규칙, 코드베이스의 나머지, 어떤 결정의 이유는 모르고, 지적 중 일부는 틀립니다. 사람 리뷰 대신이 아니라 사람 리뷰 전에 쓰세요.

AI 코드 리뷰는 왜 실제로는 없는 문제를 보고하나요?

모델은 붙여 넣은 코드만 봅니다. 호출하는 쪽에서 값을 검증하거나 여러분의 시스템에서는 절대 일어날 수 없는 경우라도, 모델은 그것을 알 수 없으므로 위험을 지적합니다. 문제를 일으키는 구체적인 입력을 요청하면 이런 것을 많이 걸러 낼 수 있습니다. 현실적인 유발 입력이 없는 지적은 대개 오탐입니다.

리뷰하면서 AI에게 코드를 다시 쓰게 해야 하나요?

다시 쓴 파일 대신 지적마다 하나씩 작은 diff를 요청하세요. 전체를 다시 쓰면 수정 사항과 아무도 요청하지 않은 변경이 섞이고, 그 다시 쓴 코드도 리뷰해야 합니다. diff가 있으면 각 지적을 따로 받아들이거나 거절할 수 있습니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기