
Claude Code를 위해 「냄새나는 코드 탐지기」를 개발하여 Hooks에 설정해 본 이야기
요약
Claude Code 사용 시 발생하는 코드 리뷰 부하를 줄이기 위해 구문 트리 분석을 활용한 '냄새나는 코드 탐지기' 개발 사례를 소개합니다. CI 단계에서 차단하거나 Claude Code Hooks를 통해 AI가 스스로 코드를 재검토하도록 유도하는 효율적인 워크플로우를 제안합니다.
핵심 포인트
- 구문 트리 분석을 통해 커스텀 코딩 규약을 자동 검출 가능
- Claude Code Hooks를 활용한 자율적 코드 수정 유도
- CLAUDE.md의 한계를 보완하는 기술적 피드백 루프 구축
- AI 코딩 에이전트의 결과물 품질 관리 자동화
ManaLink의 CTO 메이진(Meijin)입니다.
코딩 에이전트(Coding Agent)가 작성한 방대한 양의 코드, 리뷰하기 싫으시죠?
물론 명확하게 잘못된 것은 CI에서 걸러낼 수 있도록 하고 싶지만, CI를 통과하더라도 인간의 리뷰 시점에서 문제시되는 구현이 대량으로 존재합니다.
본 기사는 기존의 리뷰 지적 사항 중 구문 트리 분석(Syntax Tree Analysis)으로 잡아낼 수 있는 것을 늘려, CI에서 걸러내거나 Claude Code Hooks를 통해 **"이것은 절대 NG는 아니지만, 한 번 재검토해 주길 바란다"**라고 Claude Code 스스로에게 반환하는 방식으로 운영해 나가는 이야기에 관한 것입니다.
단적으로 말하자면
- ESLint나 PHPStan이 공식적으로 지원하지 않는 규칙을 쉽게 직접 만들 수 있는 시대가 되었다!
- 직접 만든 규칙은 CI에서 차단할 뿐만 아니라, Claude Code의 Stop Hook 등을 통해 "냄새나는 코드"로서 통지하고 자율적으로 수정하게 하는 것도 방법이다!
라는 이야기입니다.
특히 다음과 같은 분들이 읽어주셨으면 합니다.
- Claude Code에게 코드를 작성하게 하는 양이 늘어나 인간 리뷰의 부하가 커지고 있다
- CLAUDE.md에 규칙을 써두어도 확률적으로 무시되는 것에 지쳐 있다
- CI에서 걸러낼 정도는 아니지만 리뷰 관점에 포함시키고 싶은 내용을 개발 프로세스에 잘 녹여내고 싶다
그럼 구체적인 방법을 써 내려가겠습니다. 참고로 Claude Code Hooks가 익숙하지 않은 분은 아래 문서와 함께 읽으시면 이해하기 쉬울 것입니다.
레벨 1: 명확하게 잘못된 구현을 구문 트리 분석으로 검출하기
해결하는 리뷰 유형
- 구문 트리(Syntax Tree)로 거의 특정할 수 있는 코딩 규약이면서, 지금까지 CI 등에서는 걸러내지 않았던 것
AI 코딩 이전에도 실력 있는 엔지니어가 개인적으로 해왔을 것이라 생각합니다만, 지금은 Claude Code에게 상당히 맡길 수 있으므로 꼭 해보자는 이야기입니다.
예를 들어, **"Laravel 표준 Logger가 아니라, 자체 제작한 Logger Wrapper를 사용해 달라"**와 같은 규약이 해당합니다.
인간이 코드를 쓴다면 한 번 전달하거나 규약을 읽게 하면 대부분 지켜질 만한 것도, AI 코딩에서는 아무렇지 않게 어겨버립니다.
Claude Code가 지키게 하려면
이런 내용을 CLAUDE.md 등에 쓰기 시작하면 끝이 없고, 확률적으로 무시되기 때문에 지치게 됩니다.
이 정도라면 커스텀 PHPStan 규칙으로 바로 만들 수 있습니다.
본 예시는 간단하지만, "이 케이스는 명확하게 잘못되었다"라고 인간이 단언할 수 있다면 꽤 넓은 범위에서 구현할 수 있다는 인상을 받았습니다. 규칙이 제대로 작동하는지 Claude Code에게 시도하게 하거나, 테스트 코드(Test Code)를 작성하게 할 수도 있어서 실제로 해보니 생각보다 효율적이었습니다.
커스텀 규칙을 만들 수 있다면 커밋 훅(Commit Hook)이나 Pull Request의 CI에서 돌리고, 그 결과를 Claude Code에 피드백하면 좋습니다.
이미 CI가 있는 프로젝트라면 우선 그곳에 얹는 것이 가장 이해하기 쉬운 타협점이라고 생각합니다.
아래에 규칙 예시를 둡니다. 인간 코딩 시대에는 이런 것을 대량으로 만들고 있으면 마음이 꺾였겠지만, 지금은 순식간이라 편합니다. (예전부터 꺾이지 않았던 분들은 정말 대단하십니다.)
if (!$node->class instanceof Name) {
return [];
}
...
레벨 2: CI에서 걸러내기 어려운 구현을 "냄새나는 코드"로 반환하기
해결하는 리뷰 유형
- 허용할 때도 있지만, 기본적으로는 안 된다고 하고 싶은 구현
예를 들어, 클린 아키텍처(Clean Architecture) 기반의 프로젝트에서는 **"UseCase 계층에서 DB에 접근하는 Model (Laravel이나 Rails의 Model)을 직접 읽어서는 안 된다"**라는 규칙이 있죠.
완전 신규 프로젝트라면 모를까, 현실적으로는 "사실은 안 되지만, 기존 모듈의 사정 때문에 이렇게 할 때도 있다. 결국 리뷰 시점에 판단할 수밖에 없다"라는 케이스도 종종 있다고 생각합니다.
그런데 이런 종류의 문제를 AI 코딩은 엄청난 양으로 저질러 놓습니다. 인간이 가치관이나 경험으로 케어하던 부분을 Claude Code는 가차 없이 배신해 오기 때문입니다.
Claude Code가 지키게 하려면
먼저 앞 절과 마찬가지로, 이번과 같은 "냄새나는 코드"를 검출하는 커스텀 규칙을 구현합니다.
하지만 그대로 CI 등에 포함시키면, 오탐(False Positive)이 다발하여 Claude Code도 당황하여 폭주하고, 말도 안 되는 구현에 도달하여 혼돈의 극치를 달성하게 됩니다.
그래서 우선 냄새나는 코드(Smelly Code)를 검출하는 커스텀 규칙(Custom Rule)만 실행하는 전용 커맨드를 준비합니다. 예를 들어 PHPStan에는 customRulesetUsed라는 옵션이 있는데, 이를 ON으로 설정하면 커스텀 규칙만 실행하는 설정 파일을 만들 수 있습니다.
아마 대부분의 린트(Lint) 도구에도 이와 유사한 위치의 기능이 있을 것입니다.
다음으로, Claude Code의 Stop 계열 훅(Hook)에서, 본 세션에서 Diff를 발생시킨 파일을 인자로 받아 냄새나는 코드를 검출하는 PHPStan을 실행하고, 출력 결과에 "이것은 냄새나는 코드를 검출한 것이며, 수정이 필수적인 것은 아니지만 구현의 재검토를 요구한다"라는 취지의 코멘트를 붙여 표준 출력(Standard Output)으로 내보내 Claude Code 본체에 반환하는 스크립트를 설정합니다.
"hooks": {
"Stop": [
{
...
이 TypeScript 파일에서는 해당 세션 내에서 편집한 파일을 받아, 파일명이나 경로를 통해 적절한 핸들러(Handler)로 라우팅(Routing)합니다.
저희 회사는 실질적으로 모노레포(Monorepo) 환경이며, Claude Code가 모든 리포지토리를 가로질러 작업하고 있기 때문에, 각 핸들러 내에서 편집된 파일 경로를 확인하여 PHPStan, ESLint, 로컬 DB에 대한 건전성 체크 등 필요한 도구 세트(Toolset)를 호출합니다.
예를 들어 PHPStan 핸들러에서는 마지막에 다음과 같은 문구를 반환합니다. 아직 실험 중이라 간이적이지만, 요컨대 "이것은 에러가 아니라 리뷰에서 빈번하게 지적되는 항목입니다. 수정하지 않을 거라면 그 이유를 사용자에게 보고해 주세요"라는 뜻입니다.
return {
message: [
`[${this.id}] 커스텀 리뷰 관점 규칙 (${PHPSTAN_CONFIG})에서 지적이 발생했습니다.`,
...
훅의 표준 출력은 Claude Code의 스레드로 돌아오기 때문에, Claude Code는 이를 받아 즉시 작업을 재개합니다. 중요한 것은, 사람이 재확인을 지시하기 전에, 혹은 애초에 냄새나는 코드가 있었는지 신경 쓰기 전에, Claude Code가 스스로 움직여 준다는 점입니다.
그 외에 어떤 문제들을 검출시키고 있는가
그 외에는 다음과 같은 사례들도 있습니다.
-
useEffect안에서setState계열 함수 호출을 검출하여, 이벤트 핸들러(Event Handler)로의 변경을 "제안"useEffect라고 해서 무조건 안 된다고 할 수는 없지만, 사람이 작성하는useEffect는 6할 정도 잘못되었고, Claude Code가 작성하는 것은 9할 정도 잘못되기 때문에, 일률적으로 NG라고 하기보다는 그 자리에서 피드백(Feedback)을 주고 싶습니다.
-
Validation 규칙을 다루는 클래스에서 최댓값(Maximum Value) 설정 누락이 있는 경우
- 절대 안 되는 것은 아니지만, 요구사항 정의 시 무심코 명시하지 않았을 때 발생하기 쉽습니다. 보통은 최댓값이 존재하므로 "냄새가 납니다".
- 즉시 수정하게 만들기보다, 요구사항 정의서를 재확인하고 지시자에게 질문하게 하는 것이 올바른 방향성인 것 같습니다.
-
특정 테이블 X의 기본 키(Primary Key)에 외래 키(Foreign Key)를 걸고 있는 테이블은 보통
ON DELETE CASCADE를 사용하지만, 드물게 예외가 있는 경우- 마이그레이션(Migration) 파일에 Diff가 있을 때, 로컬 DB에 SQL을 발행하여 정적 검출을 수행합니다.
- 테이블 설계 시의 누락이나, Claude Code에게 마이그레이션 파일을 작성하게 했을 때의 누락을 검출할 수 있습니다.
-
테스트에서
assertStatus(500)를 기대하고 있는 부분을 검출- AI에게 테스트를 작성하게 하면, 구현 중에 500 에러가 났을 때 "현재 동작이 500이니까"라며 그대로 기대값(Expected Value)으로 설정해 버리는 경우가 있습니다.
- 500이라고 해서 반드시 안 되는 것은 아니지만, 구현이 끝난 후 훅(Hook)을 통해 반환함으로써, AI가 자율적으로 "어라, 이건 좋지 않은 일을 해버렸을지도 몰라"라고 깨달을 수 있게 합니다.
-
본래는 한 곳에서만 접근해야 하는 인터페이스(Interface)를 다른 클래스가 직접 DI(Dependency Injection)하고 있는 부분을 검출
- PHP에는 패키지 프라이빗(Package Private)과 같은 경계가 표준으로 존재하지 않으므로, 접근 가능한 것은 접근하게 됩니다.
- 이런 몇 개의 클래스뿐인 의존 관계라면, 전용 도구를 도입하지 않아도 PHPStan의 커스텀 규칙으로 충분히 검출할 수 있습니다.
Validation의 최댓값 누락은 이런 식입니다. Claude Code가 작성하게 해서 메시지가 영어지만, 뭐 괜찮겠죠.
$ruleStrings = $this->extractRuleStrings($item->value);
if ($ruleStrings === null) {
continue;
...
설정 결과 관측된 동작
Stop Hook에서 실행되고 결과 메시지가 흘러 들어오면, Claude Code는 다시 작업을 재개하여 검토하고 필요에 따라 구현을 진행합니다. 말하자면 Ralph Loop의 응용편이라고 할까, 정적 분석의 강도를 높인 버전이네요.
또한, 알아서 해결하기만 하는 것이 아니라, "Hook에서는 안 된다고 돌아왔지만, 해결할 방법도 없고 의미도 없다고 생각해서 하지 않았습니다"라는 경우도 물론 있습니다.
여기서 중요한 것은, 대량의 차분(Diff) 중에서 "이 부분이 수상할 수 있다"라고 인간이 사전에 정의한 곳을 Claude Code가 보고해 준다는 점입니다.
개인적으로는 이것만으로도 상당한 진보입니다. 인간도 구현 시 판단이 망설여지는 부분은 테크 리드(Tech Lead)에게 상담하곤 하는데, 그와 유사한 움직임이 됩니다.
그런 이유로, 수상한 코드의 탐지 × Stop Hook의 조합은 현재로서는 마음에 듭니다.
스탠스 (Stance)
스탠스로서는 이것을 비교적 간편하게 양산해도 좋다고 생각합니다. "CI가 실패하느냐"라는 명제로 접근하면 팀의 허가를 받아야 하는가 마느냐 하는 이야기로 흐르기 쉽지만, Stop Hook을 통해 Claude Code에 응답을 돌려주는 것뿐이라면 치명적인 문제는 발생하기 어렵습니다. 실제로, 다소 주관이 강한 경고(Warn)를 넣은 며칠 뒤에 동료로부터 "명인님, 그 규칙은 역시 너무 주관적인 거 아닌가요?"라는 핀잔을 들은 적도 있지만, 개발을 어떻게든 멈추게 만드는 것은 아니므로, 그런 말을 들으면 나중에 생각하면 됩니다.
이 규칙을 늘리는 것 자체는 프로덕트 코드(Product Code)에 변화를 주지 않으므로 경솔하게 개발할 수 있습니다. 그렇기에 양산하여 돌려보며, 어느 정도의 규칙이 현재의 조직이나 페이즈(Phase)에 맞는지 실험하는 것에 가치가 있다고 생각합니다.
여담
실제로 구현을 시작하면 알게 되지만, 이것을 어설프게 구현하면 무한 루프(Infinite Loop)에 빠질 수 있으므로, 다음과 같은 가드(Guard)를 넣는 것이 좋습니다 (어차피 구현은 Claude Code에게 시킬 것이므로, 이것을 보여주는 것만으로도 전달될 것입니다).
# stop_hook_active=true 일 때는 "Claude가 이미 이 hook에 응답하여 계속 진행 중"이므로,
# 무한 루프를 피하기 위해 지적을 스킵한다.
if [ "$(echo "$payload" | jq -r '.stop_hook_active // false' 2>/dev/null)" = "true" ]; then
...
여담 2
이미 성숙한 팀에서 이것을 시작하고 싶은 분은 settings.local.json에서 설정하여, 로컬 환경에서만 테스트하는 것부터 시작하면 좋을 것 같네요.
여러 사정으로 후크(Hook)를 건드릴 수 없는 경우에는, 임의의 Diff를 해석하여 수상한 코드를 반환하는 래퍼(Wrapper)를 만들어, 한 단락이 끝날 때마다 수동으로 트리거하는 방식으로도 대신할 수 있을 것 같습니다. 따라서 반드시 Hook일 필요는 그리 크지 않습니다. 확장성(Scalability)의 차이 정도입니다.
또한, Stop Hook이 아니라 Git 커밋 후크(Commit Hook)로도 불가능한 것은 아닙니다. 다만, 커밋 후크에는 명시적으로 안 되는 규칙을 넣고 싶고, "수상한 코드"에 대한 이야기는 Claude Code 자체의 출력 능력(Output Capability)을 끌어올리는 스코프이므로, Claude Code 내부에 한정하고 싶습니다. 그런 감각적인 이유로 Stop Hook 쪽으로 기울였습니다.
Claude Code Rules & 서브 디렉토리의 CLAUDE.md
다음은 구문 트리 분석(AST Analysis) 이야기에서 벗어나지만, 리뷰의 수고를 줄이는 맥락에서 Claude Code Rules나 CLAUDE.md를 활용하는 이야기도 언급해 두겠습니다.
지금까지의 이야기는 구현 후의 파일에 즉각적으로 피드백(Feedback)을 주는 지향성이었습니다. 그 때문에 구조적인 한계가 있습니다.
예를 들어, 특정 지식을 알지 못해서 설계 판단에서 실수하고, 말도 안 되는 구현을 완성해 오는 케이스는 케어하기 어렵습니다.
그래서 구현에 착수하기 전부터 알아차려 주길 바라는 항목은, Claude Code Rules에서 경로(Path)를 지정하여 작성하거나, 관련 모듈에 가까운 서브 디렉토리에 CLAUDE.md를 배치하기도 합니다.
다만, 이 방식은 구문 트리 분석과 같은 근거 있는 탐지를 사용할 수 없기 때문에, 어느 정도 범용적인 지시밖에 작성할 수 없다는 한계가 있습니다.
개선안으로는, 읽으려는 파일이 특정 구문 트리 분석 조건에 걸리는 경우 "설계 시 여기에 주의하라"고 전달할 수 있다면 더 정적으로 만들 수 있을 것 같아 앞으로 연구해 보고 싶습니다 (Plan 모드이면서 Read 도구의 PreToolUse Hook 내에서 발화할 수 있다면, 계획 단계에서 하고 싶은 지시를 할 수 있을지도 모릅니다).
이러한 규칙을 만들어 주는 SKILL을 개발한다
어느 정도 방향성이 잡히고 사례가 몇 가지 생기면, 규칙 자체를 만들어 주는 SKILL을 만들면 작업이 훨씬 수월해집니다.
정적 분석 (Static Analysis)이 가능한 것을 최우선으로 두고, 정적 분석한 것을 "냄새나는 것"으로 간주하여 Claude Code에 피드백하는 것을 차선으로 하며, 그래도 불가능한 것을 Claude Code Rules 등에 반영하는 우선순위로 움직입니다.
인자로 Git 커밋 등을 전달하며, **"이 커밋에서 수행한 수정을 앞으로는 사전에 방지할 수 있도록 규칙화하고 싶어. 계획을 세워줘"**라고 말하면, 대응하는 구문 트리 (AST) 분석 조사나 기존 사례의 적절한 수위를 고려하여, 그럴듯한 탐지기와 규칙을 제안해 주게 됩니다.
이 단계에 이르면, Claude Code가 "그건 좀 아닌데..." 싶은 구현을 했을 때, 먼저 지시하여 수정하게 한 뒤 커밋 후에 별도의 스레드에서 이 스킬을 호출하는 습관을 들일 수 있습니다. 하면 할수록 재발 방지책이 쌓여갑니다.
인간의 Pull Request 리뷰도 남긴다
적어도 저희 회사에서는 인간의 리뷰도 프로세스로 남겨두고 있습니다. 다만, 리뷰 시점에서도 "이 시책의 특성상 이 부분이 냄새가 날 것 같은데, 특별한 케어를 하고 있는가"와 같이, 코드를 보기 전에 미리 짐작하는 것이 당연해져 있습니다.
그렇게 되면, 리뷰어에게 "이 시책은 이 부분이 수상할 것 같습니다"라고 얼마나 잘 제시할 수 있느냐의 싸움이 됩니다. 현재로서는 "냄새나는 코드"로 취급하고 있는 구문 트리 분석 중 몇 가지는, Claude Code에게 보여주는 것보다 인간 리뷰어에게 보여주는 것이 더 나을 것 같다고 생각되는 것들이 있습니다. 그런 것들은 Pull Request에 "리뷰어를 위한 힌트"로서 ReviewDog로 코멘트를 남기는 메커니즘을 도입하기도 했습니다.

이런 것들이 PR 코멘트로 달리기 때문에, 인간의 눈으로 리뷰할 때 "이런 구현이 있다는 것은 뭔가 큰 설계의 왜곡이 있는 것 아닌가!?"라고 생각하며 그 지점부터 심층적으로 파고들 수 있습니다.
향후 과제
- Stop Hook이 아니라 PostToolUse가 더 좋을 가능성
"matcher": "Edit|Write|MultiEdit"등으로 한정한다면, 유사한 훅을 거의 그대로 실행할 수 있을 것 같음- 스크립트가 늘어나면 Claude Code에 피드백을 돌려주는 부분이 비대해지므로, 프레임워크처럼 아키텍처를 정비하고 싶음
- 여러 언어를 가로질러 동작하게 될 것이고, 파일 경로에 대한 지식을 어디에 둘 것인지 등 개선할 여지가 있음
- API나 DB 스키마 등, 릴리스 후에 쉽게 바꿀 수 없는 차이점에 대해 더 강력한 피드백을 돌려주는 메커니즘
- API 설계를 제안한 후 그대로 구현해 주었지만, 나중에 보니 설계가 왜곡되어 프론트엔드에서 엄청나게 흡수(Absorb)하고 있었던 적이 있음. 지시 추종성 (Instruction Following)이 높아진 만큼, 워크플로우상에 다른 의도를 가진 Claude Code를 기동하는 메커니즘이 필요해 보임
요약
AI 코딩 시대의 리뷰 부하를 낮추기 위해서는 단순히 "AI가 제대로 작성하게 해"라고 부탁하는 것만으로는 부족합니다.
- 명확하게 안 되는 것은 커스텀 Lint나 PHPStan 규칙으로서 CI에서 걸러낸다
- CI에서 걸러내기는 어렵지만 리뷰 시 신경 쓰이는 구현은 "냄새나는 코드"로서 Claude Code Hooks로 돌려준다
- 정적으로 탐지하기 어려운 설계 지식은 CLAUDE.md나 Rules, SKILL에 녹여낸다
이와 같이 리뷰 관점을 몇 가지 형태로 나누어 개발 프로세스에 태워 나가는 것이 좋아 보입니다.
특히 "냄새나는 코드"를 Stop Hook 등으로 Claude Code 자신에게 돌려주는 운용은, 인간이 리뷰하기 전에 한 번 멈추게 할 수 있으므로 현재로서는 상당히 마음에 듭니다.
보충: 구문 트리 분석 이외에 이미 시도하고 있는 것
본 기사의 본론과는 결이 다르지만, 소스 리뷰 자동화 맥락에서 제가 시도한 것들도 가볍게 나열해 두겠습니다.
-
정적 분석 도구의 기존 규칙을 늘리거나 새로운 도구를 도입함
-
지난 3년 이상의 제 소스 리뷰 코멘트를 GitHub API로 다운로드하여 AI 스스로 분석하게 하고, 그것을 SKILL 파일로 정리함
-
"사양·데이터·로직이 '올바른 장소'에 살고 있는가. 문제를 하류에서 처리하는 것이 아니라, 상류에서 해결하고 있는가"가 meijin 리뷰의 가장 근본적인 원칙이라고 SKILL.md에 적혔습니다. 그렇군요~ - 몇 번 실제로 Claude Code가 대량 생성한 PR에 적용해 보았는데, 꽤 효과가 있습니다. 확실히 없는 것보다는 낫습니다.
-
순환 복잡도 (Cyclomatic Complexity) 등 정적으로 측정할 수 있는 아키텍처 관련 지표를 우선 대량으로 돌려서, 정해둔 기준을 밑돌면 경고를 내보낸다.
-
특정 지표가 이렇다면 이렇게 하는 것이 좋겠다는 기준을 도무지 확정 지을 수 없어서, 도중에 그만두었습니다.
-
뭐랄까, 구체적인 예시 없이 추상적으로 과제를 정의하면 그것으로 모든 것이 해결될 것 같다는 식의 아이디어는 죄다 잘 풀리지 않더라고요... 그야 당연한 건가요.
시도해 본 경험에서 배운 점
이상의 경험을 통해 다음과 같은 점을 배웠습니다.
- 구체적인 사례를 보여줄수록, AI는 현실적인 리뷰 항목을 제시해 준다.
- 리뷰 실적이 많은 프로덕트나 개인은 그 자산을 활용하는 것이 좋다.
- 구체적인 예시 없이 추상적으로 과제를 정의하며 "AI가 본 적 없는 과제를 찾아내 주지 않을까"라고 기대하는 것은 환상이다. 우선 자신이나 팀이 리뷰해 본 적이 있거나 지적해 본 적이 있는 과제부터 시작하는 것이 무난하다.
소감
Claude Code의 상태 표시줄(Status line)에 이전부터 고양이를 키우고 있는데, 이것 덕분에 Hooks의 개념 이해가 진전되었고, 그래서 그곳에서 리뷰 지적을 반환하겠다는 발상에 이르게 되었습니다. 고양이 덕분입니다(?)
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기