🐝매일 한입
Dev Life & Opinion📖 22분 읽기

AI 코드리뷰는 내 사고를 막을 수 있었을까 — 블로그를 망친 97줄 스크립트로 리뷰어 4종 실측

구글 색인 사고를 수습하려다 오히려 키운 스크립트가 있습니다. 그 실제 코드를 eslint·node·Claude Sonnet·Claude Opus에 블라인드로 먹여, 누가 결함을 잡는지 측정했습니다. 결함은 git log 플래그 하나였고, 가장 자연스러운 수정법도 틀렸습니다.

📖 22분 읽기👁 1회
#AI 코드리뷰#코드리뷰 자동화#git#Claude Code#eslint#개발 생산성

2026-08-24 전면 재작성. 이 글의 원래 버전에는 제가 겪지 않은 팀 이야기와 측정하지 않은 수치가 적혀 있었습니다. "시니어 개발자 2명이 5명의 PR을 돌아가며 본다", "리뷰 시간이 35분에서 20분으로 줄었다" 같은 문장들입니다. 저는 그런 팀에 있은 적이 없고, 그 숫자를 재본 적도 없습니다. 전부 지웠습니다.

대신 실제로 있었던 일로 다시 썼습니다. 이 블로그에는 제가 직접 돌렸다가 사태를 키운 스크립트가 남아 있습니다. 그 코드를 리뷰어 4종에 블라인드로 넣고, 누가 무엇을 잡는지 측정했습니다. 아래는 전부 이번에 실행한 결과입니다.


97줄짜리 "안전한" 스크립트

2026년 5월 6일, 저는 이런 커밋을 남겼습니다.

commit 6cbec76
Date:   Wed May 6 09:10:52 2026 +0900

    fix: GSC 색인 거부 대응 — 백데이트 114건·ai 카테고리 404·인바운드 강화

    - 백데이트 frontmatter 114건 → git first-seen 일자로 교정 (KO 78 + EN 36)
      사이트 launch(2026-03-31) 이전 날짜 표기로 freshness gaming 시그널 약화

 121 files changed, 546 insertions(+), 118 deletions(-)

배경은 이렇습니다. 4월에 이 블로그의 구글 색인이 122페이지에서 0으로 무너졌습니다. 직접적인 방아쇠는 다국어 라우팅 개편 과정의 canonical 오류였고, 그건 따로 고쳤습니다. (그 사고의 전말은 여기 있습니다)

문제는 그다음이었습니다. 색인이 돌아오지 않으니 "혹시 다른 마이너스 신호가 있나" 싶어 저장소를 뒤졌고, 사이트 오픈 이전 날짜로 표기된 글이 100건 넘게 있다는 걸 발견했습니다. 날짜를 부풀려 최신처럼 보이게 하는 건 구글이 싫어하는 패턴이니, 실제 날짜로 교정하기로 했습니다.

그래서 scripts/fix-backdate.mjs를 썼습니다. 97줄이고, 지금도 저장소에 그대로 있습니다.

const SITE_LAUNCH = '2026-03-31'; // 첫 git 커밋

function gitFirstSeen(file) {
  try {
    const out = execSync(`git log --reverse --format=%aI -- "${file}"`, {
      cwd: ROOT,
      encoding: 'utf8',
      stdio: ['ignore', 'pipe', 'ignore'],
    });
    return out.split('\n')[0]?.slice(0, 10) || null;
  } catch {
    return null;
  }
}

논리는 단순합니다. frontmatter의 date가 사이트 오픈 이전이면, git 이력에서 그 파일이 처음 등장한 날짜를 찾아 그 값으로 바꾼다.

저는 이 스크립트를 꽤 신뢰했습니다. 이유가 있었습니다. --dry-run 플래그가 있고, 본문은 건드리지 않고 frontmatter만 정규식으로 치환하고, 파일 상단에는 이런 주석 블록까지 달아뒀거든요.

// 보호장치:
//   - frontmatter 외 본문은 절대 건드리지 않음
//   - 수정 전 스냅샷은 git이 자동으로 가짐 (commit 안 했을 때만 위험)
//   - SITE_LAUNCH 이후 날짜는 건드리지 않음

dry-run으로 한 번 돌려보고, 출력 목록을 훑어보고, 플래그를 빼고 다시 돌렸습니다.

결과: 56편이 하루로 뭉쳤다

지금 이 저장소의 한국어 글 140편에서 date 분포를 세어보면 이렇습니다.

$ find content/posts/ko -name "*.mdx" -exec grep -h "^date:" {} \; \
    | sed 's/date: *//; s/"//g' | sort | uniq -c | sort -rn | head -5
  56 2026-03-29
   2 2026-07-06
   2 2026-05-14
   2 2026-05-13
   2 2026-05-07

56편이 2026년 3월 29일 하루에 몰려 있습니다. 두 번째로 많은 날짜가 2편이니, 이건 분포가 아니라 절벽입니다.

이 값은 사이트맵의 lastmod와 각 글 JSON-LD의 datePublished로 그대로 나갑니다. 구글 입장에서 이 사이트는 오픈 이틀 전에 56편을 동시 발행한 사이트로 보입니다. 대량 생산 콘텐츠를 걸러내려는 정책이 정확히 겨냥하는 지문입니다.

색인을 죽인 건 4월의 canonical 오류였습니다. 하지만 회복을 막은 건 5월의 이 커밋일 가능성이 큽니다. 저는 마이너스 신호를 지우려다 더 선명한 마이너스 신호를 새겼습니다.

진짜 결함은 플래그 하나였다

왜 56편이 같은 날짜를 받았을까요. 저장소 이력을 보면 답이 나옵니다.

$ git log --format='%aI %h %s' --since=2026-03-25 --until=2026-04-01 | sort
2026-03-26T11:35:21+09:00 f2f0f49 feat: 새 글 - Cerebras WSE-3 ...
2026-03-27T14:28:42+09:00 624fe38 feat: 새 글 - Microsoft Copilot Cowork ...
2026-03-28T19:16:10+09:00 7418aa2 feat: 새 글 - Google Search Live ...
2026-03-29T02:00:02+09:00 5f96c76 feat: i18n 다국어 지원 + SEO/GEO 강화     ← 55개 파일
2026-03-29T13:19:28+09:00 b06bdc0 feat: 전체 55개 글 영어 번역본 추가

5f96c76이 범인입니다. 다국어 지원을 넣으면서 글 파일들을 content/posts/에서 content/posts/ko/로 옮긴 커밋입니다. 내용은 그대로고 경로만 바뀌었습니다.

그런데 git log -- <경로>는 기본적으로 경로의 이력만 봅니다. 파일이 이동하면 그 이전 이력은 다른 경로에 속하므로 보이지 않습니다. 즉 gitFirstSeen()이 돌려준 "이 파일이 처음 등장한 날"은 글을 쓴 날이 아니라 디렉터리를 재편성한 날이었습니다.

실제로 측정해 봤습니다. git 2.50.1 기준입니다.

$ f=content/posts/ko/ai-tools/adobe-firefly-ai.mdx
$ git log --follow --name-status --format='%h %aI' -- "$f" | tail -4
bcf7569 2026-03-25T13:17:24+09:00
M	content/posts/ai/adobe-firefly-ai.mdx
239bc48 2026-03-25T13:04:28+09:00
A	content/posts/ai/adobe-firefly-ai.mdx

이 글의 진짜 최초 등장은 2026-03-25, 경로는 content/posts/ai/였습니다. 스크립트는 여기에 2026-03-29를 썼습니다. 나흘을 틀린 게 문제가 아니라, 55개 파일이 전부 같은 이동 커밋을 가리켰다는 게 문제였습니다.

그리고 가장 자연스러운 수정도 틀립니다

여기가 이 글에서 가장 쓸모 있는 부분입니다.

"--follow를 안 붙였구나"까지는 어렵지 않게 도달합니다. 그럼 기존 명령에 --follow를 붙이면 될까요? 세 파일로 측정한 결과입니다.

### content/posts/ko/ai-tools/adobe-firefly-ai.mdx
  A) --reverse            : 2026-03-29T02:00:02+09:00
  B) --follow --reverse   : 2026-03-29T02:00:02+09:00
  C) --follow (마지막 줄) : 2026-03-25T13:04:28+09:00
     커밋 수 A/B/C        : 4 / 1 / 11
### content/posts/ko/dev-life/ai-3.mdx
  A) --reverse            : 2026-03-29T02:00:02+09:00
  B) --follow --reverse   : 2026-03-29T02:00:02+09:00
  C) --follow (마지막 줄) : 2026-03-25T13:04:28+09:00
     커밋 수 A/B/C        : 2 / 1 / 7
### content/posts/ko/seo/blog-seo-3-weeks.mdx
  A) --reverse            : 2026-03-29T02:00:02+09:00
  B) --follow --reverse   : 2026-03-29T02:00:02+09:00
  C) --follow (마지막 줄) : 2026-03-25T17:22:38+09:00
     커밋 수 A/B/C        : 5 / 1 / 6

B를 보세요. --follow를 기존 --reverse 명령에 그대로 붙이면 커밋 목록이 1개로 붕괴하고, 결과는 고치기 전과 똑같은 2026-03-29가 나옵니다. 세 파일 모두 동일합니다.

--follow는 git 안에서 단일 경로를 따라가며 pathspec을 다시 쓰는 방식으로 구현돼 있어서, 이력 순서를 뒤집는 --reverse와 조합하면 이렇게 무너집니다. 에러도 경고도 없습니다. 조용히 한 줄만 뱉습니다.

제대로 된 형태는 C입니다. --reverse를 빼고 --follow만 쓴 뒤 마지막 줄을 취하는 것.

// 틀림 — 이동 커밋 날짜가 나옴
git log --reverse --format=%aI -- <file> | head -1

// 역시 틀림 — 커밋 1개로 붕괴, 결과 동일
git log --follow --reverse --format=%aI -- <file> | head -1

// 맞음
git log --follow --format=%aI -- <file> | tail -1

한 가지 더. 스크립트의 SITE_LAUNCH는 '2026-03-31'이고 주석에 "첫 git 커밋"이라고 적혀 있는데, 실제 첫 커밋은 2026-03-25T12:09:36입니다. 상수 자체가 엿새 틀렸습니다.

실험: 리뷰어 4종에 이 코드를 먹였다

여기서 원래 질문으로 돌아갑니다. AI 코드리뷰가 이걸 잡았을까요?

측정 가능한 형태로 설계했습니다.

대상: scripts/fix-backdate.mjs 97줄 원본 그대로.

격리: 이 저장소의 CLAUDE.md에는 "날짜 일괄 수정이 56편을 하루에 몰아넣어 대량발행 신호를 만든 전례가 있다"는 문장이 들어 있습니다. 답안지입니다. 그래서 빈 디렉터리를 만들어 스크립트 파일 하나만 복사하고 거기서 돌렸습니다. 파일 읽기 도구도 막았습니다(--allowed-tools ""). 리뷰어가 보는 건 프롬프트와 코드 텍스트뿐입니다.

프롬프트: 답을 유도하지 않는 평범한 리뷰 요청입니다.

아래 Node.js 스크립트를 코드리뷰해 주세요.

용도: 블로그 저장소에서 마크다운 글의 frontmatter `date` 필드가 실제 발행일과
다르게 표기된 경우, git 이력상 그 파일이 처음 등장한 날짜로 교정하는 일회성
도구입니다.

버그, 엣지 케이스, 위험한 동작을 지적해 주세요. 심각도 순으로 정리해 주세요.

돌리지 못한 것도 밝힙니다. CodeRabbit은 GitHub App 설치와 계정 연동이 필요하고, Qodo Merge는 셀프호스팅에 Docker·webhook·LLM API 키가 필요합니다. 둘 다 지금 환경에서 실행하지 않았습니다. 그래서 이 글은 그 둘에 대해 아무 수치도 주장하지 않습니다.

결과

리뷰어--follow 결함--follow+--reverse 함정소요산출
eslint (저장소 설정)❌❌1초 미만No issues found
node --check❌❌1초 미만통과
Claude Sonnet✅ 3번 항목❌1분 42초5,365자
Claude Opus✅ 4번 항목✅2분 41초9,911자

정적 분석은 통과시킵니다. 당연합니다. 이 코드는 문법적으로 완벽하고, 미사용 변수도 없고, 타입 오류도 없습니다. eslint가 이 파일을 실제로 검사하긴 하는지 확인하려고 일부러 미사용 변수를 넣어봤더니 경고 2건이 떴습니다. 설정 누락이 아니라 진짜로 깨끗하다고 판정한 겁니다.

두 AI 리뷰어는 모두 잡았습니다. Sonnet의 3번 항목입니다.

--follow 누락 → rename/이동된 파일의 히스토리 유실 content/posts 구조 개편이나 ko/en 디렉토리 분리처럼 파일이 이동/rename된 적이 있으면, --follow 없는 git log는 rename 이전 히스토리를 못 찾습니다. 그 결과 실제로는 오래된 글인데 "최근에 옮겨진 커밋 날짜"로 교정되어 버리는, 오히려 더 틀린 날짜가 나올 수 있습니다.

코드만 보고 ko/en 디렉토리 분리를 짚었습니다. 실제로 일어난 일이 정확히 그것이었습니다.

Opus는 여기에 더해 두 번째 함정까지 짚었습니다.

--follow는 pathspec 하나에만 쓸 수 있고 --reverse와 조합이 지저분하니, 차라리 git log --follow --format=%cs -- <file>의 마지막 줄을 취하는 편이 안전합니다.

제가 측정으로 도달한 결론과 같은 형태입니다. 저는 세 파일을 돌려보고 나서야 알았고, 리뷰어는 코드만 보고 먼저 말했습니다.

Opus가 잡은 것 중 개인적으로 가장 아팠던 건 4번이 아니라 2번이었습니다.

헤더의 "git이 스냅샷을 가짐" 보호장치가 구현되어 있지 않음

제가 스크립트를 믿은 근거가 바로 그 주석 블록이었습니다. 그리고 그 주석은 코드가 실제로 하는 일이 아니라 제가 하려던 일을 적어둔 것이었습니다. 워킹트리가 더러운 상태에서 돌리면 되돌릴 스냅샷은 없습니다. 리뷰어는 주석과 코드를 대조했고, 저는 안 했습니다.

그래서 결론은 "AI 리뷰를 쓰세요"가 아닙니다

이 실험을 시작할 때 저는 반대 결과를 예상했습니다. "코드는 완벽했고 결함은 결과에 있었으니 아무도 못 잡았다" 쪽으로 쓸 생각이었습니다. 틀렸습니다. 3분 걸렸고, 둘 다 잡았습니다.

그러니 진짜 결론은 이겁니다.

도구가 없어서 놓친 게 아닙니다. 제가 안 돌려서 놓쳤습니다.

왜 안 돌렸을까요. 그게 이 글의 핵심입니다.

  • 일회성 스크립트였습니다. 한 번 쓰고 버릴 도구에 리뷰를 붙인다는 발상 자체가 없었습니다.
  • PR이 아니었습니다. 이 저장소는 혼자 쓰는 곳이라 PR이 0건입니다. main에 직접 푸시합니다. PR에만 리뷰를 붙이는 구조였다면 이 커밋은 영원히 리뷰 대상이 되지 않습니다.
  • 안전해 보였습니다. --dry-run이 있고, 주석에 "보호장치" 섹션이 있었습니다. 둘 다 실제 안전성의 증거가 아니라 안전하게 느끼게 하는 장치였습니다.
  • 급했습니다. 색인이 0인 상태로 3주가 지나 있었고, 저는 뭐라도 하고 싶었습니다.

🧭 언제 이 방식이 안 통하는가

정직하게 한계도 적습니다.

리뷰어는 결과를 모릅니다. 두 리뷰어 모두 "날짜가 틀리게 박힐 수 있다"까지는 갔지만, 아무도 "그래서 구글이 대량발행으로 판정해 색인이 안 돌아온다"고는 말하지 않았습니다. 그건 코드에 없는 정보입니다. 도메인 결과를 아는 건 여전히 사람 몫입니다.

그래도 충분했을 겁니다. 저에게 필요했던 건 "이 사이트가 색인을 잃는다"는 예언이 아니라 "이 날짜 값이 이동 커밋 날짜일 수 있다"는 한 줄이었습니다. 그 한 줄만 봤어도 dry-run 출력에서 03-29가 56번 반복되는 걸 다르게 읽었을 겁니다.

모델 등급 차이는 실재했습니다. 둘 다 원인은 맞혔지만, --follow를 그대로 붙이면 여전히 틀린다는 걸 짚은 건 Opus뿐이었습니다. Sonnet의 지적만 보고 순진하게 --follow를 추가했다면 위 측정의 B로 갔을 거고, 결과값은 한 글자도 안 바뀌었을 겁니다. 고쳤다고 믿으면서요.

단발 실행이라 분산은 측정하지 않았습니다. 각 모델을 한 번씩만 돌렸습니다. 같은 프롬프트로 여러 번 돌리면 결과가 흔들릴 수 있고, 이 표는 그 분산을 담고 있지 않습니다.

🔪 진짜 병목은 리뷰 품질이 아니라 리뷰 대상 선정

AI 코드리뷰 도구 비교 글은 보통 "누가 더 잘 잡나"를 묻습니다. 제 경험상 그건 두 번째 질문입니다.

첫 번째 질문은 무엇이 리뷰를 통과하지 않고 프로덕션에 도달하는가입니다. 제 경우 그건 애플리케이션 코드가 아니었습니다. 한 번 쓰고 버리는 스크립트, 설정 파일, 마이그레이션, frontmatter 일괄 수정 — 리뷰 파이프라인이 쳐다보지도 않는 것들이었고, 사이트를 망친 것도 그것들이었습니다.

그래서 지금은 이렇게 합니다. 완벽하진 않지만 규칙은 하나입니다.

되돌리기 어려운 일괄 변경을 실행하기 전에는, 그 스크립트를 리뷰에 한 번 통과시킨다. PR이든 아니든, 일회성이든 아니든. 3분입니다. 제가 아낀 3분의 대가는 넉 달째 갚고 있습니다.

재현 방법

전부 그대로 돌려볼 수 있습니다.

# 1) 결함 재현 — 세 값이 다르게 나오는지
f=<이동 이력이 있는 파일>
git log --reverse --format=%aI -- "$f" | head -1          # 이동 커밋 날짜
git log --follow --reverse --format=%aI -- "$f" | head -1  # 동일 (1커밋으로 붕괴)
git log --follow --format=%aI -- "$f" | tail -1            # 진짜 최초 등장일

# 2) 블라인드 리뷰 — 저장소 컨텍스트 유출 없이
mkdir /tmp/blind && cp scripts/fix-backdate.mjs /tmp/blind/ && cd /tmp/blind
{ cat prompt.txt; echo; echo '```javascript'; cat fix-backdate.mjs; echo '```'; } \
  | claude -p --model opus --allowed-tools ""

# 3) 정적 분석 대조군
npx eslint scripts/fix-backdate.mjs
node --check scripts/fix-backdate.mjs

측정 환경: git 2.50.1 (Apple Git-155), Node.js 기반 Next.js 15 저장소, Claude Code 2.1.241, 2026-08-24.


마지막으로 하나. 이 글의 frontmatter date는 2026-03-29입니다. 네, 그 56편 중 하나입니다. 고치지 않았습니다. 날짜를 일괄로 손댄 것이 애초에 사고였으니, 같은 실수를 반복하는 대신 updated만 적고 기록으로 남겨둡니다.

관련 글:

📚 관련 글

💬 댓글