Free

좋은 CLAUDE.md는 기능을 설명하지 않는다 — Claude가 코드에서 볼 수 없는 것만 쓴다

좋은 CLAUDE.md는 README가 아니다. Claude가 코드에서 유추 못 하는 invariant만 담는다. 써야 할 6개, 빼야 할 4개, 5가지 질문.


내 Pickful 프로젝트에는 탈중앙 커뮤니티 배심원 시스템, x402 암호화폐 결제, Sign-In with Ethereum, 다중 데이터베이스, 실시간 푸시 — 지난 1~2년 사이에 등장한 기술 스택이 전부 들어가 있다. Claude는 이 기능들을 빠르고 깔끔하게 구현한다.

그런데 프로젝트의 CLAUDE.md를 열어보면 배심원 시스템x402라는 단어는 한 번도 등장하지 않는다.

이건 누락이 아니다. CLAUDE.md의 역할은 "기능을 설명하는 것"이 아니라, Claude가 코드를 한 번 읽어서는 절대로 알아낼 수 없는 것을 적는 것이다.

기능 설명을 적는 건 토큰 낭비

CLAUDE.md를 처음 쓰는 사람은 대개 README처럼 핵심 기능 하나하나를 설명하려 든다.

  • "배심원 시스템은 사용자가 콘텐츠를 신고할 수 있고, 신고된 항목은 공개 투표에 들어가며 배심원은..."
  • "x402 결제는 HTTP 402 상태 코드로 온체인 전송을 트리거해서..."
  • "좋아요는 포인트를 주고, 400점에서 VIP가 되며..."

이런 내용은 Claude가 topic_review_service.rb / x402.rb / like_points_service.rb를 여는 편이 당신이 쓴 것보다 더 정확하다. 천 자짜리 비즈니스 로직 설명을 Claude는 코드에서 수백 토큰이면 읽고, 해석 왜곡도 없다 — 코드는 사실, 설명은 2차 자료다.

Claude가 정말로 넘어지는 지점은 아래 6가지다.

실제로 구해주는 6가지 카테고리

1. 반직관적인 아키텍처 선택

Pickful의 CLAUDE.md에는 이런 줄들이 있다:

Propshaft (not Sprockets)
ImportMap (no JavaScript bundler)
Hotwire: Turbo Frames, Turbo Streams, Stimulus
Lexxy gem overrides ActionText:
  config.lexxy.override_action_text_defaults = false

모든 줄이 기본 추측에 맞서고 있다. Rails 프로젝트를 보면 Claude는 기본적으로 이렇게 가정한다:

  • 정적 자산은 Sprockets (예전 프로젝트의 관성)
  • JS는 Webpacker 또는 esbuild
  • 프런트는 React이거나 Stimulus + Turbo 혼합
  • 리치 텍스트는 기본 ActionText

CLAUDE.md에 적어두지 않으면, Claude에게 새로운 JS 기능을 추가시켰을 때 Webpacker를 설치하고 package.json을 고치고 bundler 설정을 쓸 가능성이 크다 — 모두 틀렸고, 조용히 틀린다 (앱은 돌아가지만 자산 파이프라인이 오염된다).

CLAUDE.md의 이 몇 줄은 Claude에게 말하고 있다: 추측하지 마라, 이미 결정됐다.

2. 다중 데이터베이스 구조

PostgreSQL with 4 separate databases:
- primary - Main application data
- cache   - Solid Cache storage
- queue   - Solid Queue jobs
- cable   - Action Cable subscriptions

문장은 평범하지만 막아주는 함정은 하룻밤 분량일 수 있다. Rails 8의 기본 멀티 DB는 새 동작이고, Claude는 몇 개 DB를 쓰는지 자발적으로 확인하지 않는다. 별 상관없어 보이는 migration이 잘못된 DB에 착지해도 개발 중에는 에러가 안 난다 (4개 다 PostgreSQL, schema는 어디서든 통과). 하지만 프로덕션에서 Solid Queue의 job 테이블이 primary 백업에 섞이거나, primary의 모델이 cache DB를 조회하는 일이 벌어지고 — 이런 버그는 시간이 지나야 수면 위로 올라온다.

CLAUDE.md의 두 줄 vs. 하루치 프로덕션 디버깅.

3. URL 라우팅의 "보이지 않는 관례"

/p-{slug} - Short post URLs (4-5 char alphanumeric)
/t-{slug} - Topic URLs (3-4 char alphanumeric)
/s-{code} - Short URL redirects (3-4 char alphanumeric)
/r-{referral} - Referral links

라우트 자체는 Claude가 routes.rb에서 읽을 수 있다. 하지만 길이 관례(4-5자, 3-4자)는 모델이나 service의 slug 생성 로직에 묻혀 있다. 새 단축 URL 타입을 추가시키면 Claude는 6자리, UUID 스타일, 또는 순수 숫자로 slug를 만들 가능성이 크다 — 전체 시스템의 시각 언어와 어긋난다.

이런 "관례"의 특징: 위반해도 에러 안 나지만, 나중에 코드를 보는 사람이 "뭔가 이상하다"고 느낀다. 반드시 적어야 한다.

4. 하드코딩된 비즈니스 임계값

VIP status at 400+ points
Posts with 15+ likes are "hot" posts

두 숫자 모두 코드 어딘가(User#vip?, Post#hot? scope)에 있다. 문제는 Claude가 관련 기능을 건드릴 때 — 포인트 보상 조정, "곧 VIP" 알림 추가, "핫 포스트 고정" 크론 작성 — 다른 위치의 임계값을 자발적으로 맞추지 않는다는 점이다.

결과: 어떤 작업 완료에 500점을 주고 카피는 "VIP가 될 수 있다"고 쓴다 (실제 400점이면 충분). 새 기능 시드 데이터에 좋아요를 부족하게 넣어서 15 임계값을 영원히 못 넘긴다.

Claude의 코딩 능력은 강하지만 시스템 전체의 숫자 감각은 없다. 핵심 임계값을 CLAUDE.md에 넣으면 대화 시작부터 "400과 15는 특별한 숫자"임을 알고 출발한다.

5. 인증/인가 스택 표지판

- Devise (authentication) + Pundit (authorization)
- Pundit policies in app/policies/
- Check UserPolicy, PostPolicy, etc. for permission rules

이 줄의 역할은 내비게이션, 설명이 아니다.

이게 없으면 Claude가 새 권한 체크를 추가할 때 세 가지 가능성이 있다:

  • controller에 unless current_user.admin? 박아서 쓰기
  • 더는 쓰이지 않는 CanCan 잔재를 찾아내기
  • 모델에 자체 발명한 authorize? 메서드 추가

"Pundit policies in app/policies/" 한 줄이 있으면 Claude는 매번 app/policies/에 policy 파일을 추가해서 스타일이 통일된다.

한 줄로 Claude의 매번의 "탐정 작업"을 없앤다.

6. 프로젝트 범위의 외부 제약

Supported locales: en, zh-CN, zh-TW
Testing stack: RSpec + FactoryBot + Capybara + Shoulda Matchers

새 기능 추가 시 Claude 기본값은:

  • 영어 문자열만 추가
  • Minitest + fixtures로 테스트 (Rails 기본)

하지만 당신의 프로젝트가 실제로 필요한 건:

  • 3개 로케일 번역
  • RSpec + FactoryBot, fixtures 아님

이런 "프로젝트 범위 외부 제약"을 어기면 번역 보충, 테스트 재작성 같은 자잘한 후처리가 쏟아진다. CLAUDE.md에 적으면 "매번 반드시 해야 하는 것들"을 한 번에 못 박아 둔다.

반대편: 이런 건 쓰면 낭비

"반드시 써야 하는 것"만큼 중요한 게 "쓰지 말 것"이다. 아래 유형은 보이는 족족 지워라:

1. 기능 설명

"배심원 시스템: 사용자는 위반 콘텐츠를 신고할 수 있고, 신고된 콘텐츠는 공개 투표 단계로 진입하며, 배심원은..."

→ Claude가 topic_review_service.rb를 열어서 당신이 쓴 것보다 더 정확히 읽는다. 매번 새 대화에 이걸 밀어넣는 건 순수 낭비.

2. 디렉토리 구조 / Gemfile에서 바로 보이는 것

"app/models/에는 ActiveRecord 모델", "Rails 8 사용", "DB는 PostgreSQL"

→ Claude가 프로젝트 루트와 Gemfile을 한 번 훑으면 안다.

3. 일반적인 프로그래밍 상식

"Controllers should be thin, delegate to services", "N+1 쿼리 피하기", "주요 기능 테스트 커버"

→ 이미 Claude의 훈련 데이터에 있다. 당신 프로젝트가 비정상적일 때만 쓴다 — 예를 들어 "우리는 일부러 service 계층을 안 쓴다, 로직은 controller에 둔다".

4. 현재 작업의 문맥

"지금 결제 시스템을 리팩토링 중, 초점은..."

→ 이건 대화 문맥이지 프로젝트 사실이 아니다. CLAUDE.md에 넣으면 다른 모든 대화를 오염시킨다.

실전 감사: 내 CLAUDE.md도 절반 줄일 수 있다

"나도 할 수 있다"는 증거를 주장보다 앞에 두자. 위 섹션을 다 쓰고 나서 자신의 5가지 질문으로 Pickful의 CLAUDE.md — 238줄 — 를 한 바퀴 돌렸다. 결과: 대략 절반이 낭비였다.

잘라내야 할 부분 (약 120줄):

개발 명령어 섹션의 대부분 (70줄 → 10줄): bin/setup / bin/rails db:migrate / bundle exec rspec / bin/rubocop / bin/brakeman은 전부 표준 Rails 명령어, Claude 훈련 데이터 안의 것. 프로젝트 고유 세 개만 남긴다: bin/jobs (Solid Queue worker), bin/importmap pin (ImportMap 전용), bin/kamal deploy.

Core Domain Models 리스트 (35줄 → 싹 삭제): 20개 모델 이름과 역할을 쭉 나열하는 건 가장 전형적인 "README 스타일 CLAUDE.md" — Claude가 ls app/models/를 돌리거나 모델 파일 하나만 읽어도 안다. 매 대화에 밀어 넣는 건 순수 낭비.

Tech Stack의 표준 항목 (28줄 → 8줄): Rails 8 / Devise / Pundit / Tailwind / pg_search는 전부 Gemfile에서 바로 읽을 수 있다. 반직관적인 것만 남긴다: Propshaft / ImportMap / Lexxy / x402-rails / Grover.

흩어진 일반 프로그래밍 상식 몇 줄: "Controllers should be thin", "Use app/jobs/ for async processing", request specs / model specs가 각각 뭘 테스트하는지 — Claude가 기본적으로 하는 것들. 토큰만 잡아먹는다.

남은 약 100줄: URL 라우팅 관례, 4 DB 분업, VIP 400 / Hot 15 두 개의 단단한 임계값, Lexxy override, 반기본 아키텍처 선택, Pundit 내비, 로케일 리스트, FactoryBot (not fixtures).

하지만 더 중요한 것: 아직 쓰지 않은 invariant는 무엇인가?

감사하면서 깨달은, 추가해야 하는데 한 번도 추가하지 않은 몇 가지:

  • 배심원 시스템 안에 숨어 있는 숫자 (투표 임계값, 배심원 자격 요건) — CLAUDE.md에 전혀 노출되지 않았음
  • x402에 chain id, 컨트랙트 주소, 필요한 env var가 있다면 — 설정 파일을 Claude가 안 뒤지고 알아서 값을 지어낸다
  • 포인트 거래 / Referral 서비스의 특수 제약 규칙

238줄을 100~120줄로 줄이고, 이전에 빠뜨렸던 5~10줄의 핵심 invariant를 추가한다 — 이게 "올바른 밀도"에 가까운 CLAUDE.md다.

이건 튜토리얼이 아니라 내 자신의 프로젝트에 대한 감사다. 나도 제대로 못 썼다. CLAUDE.md의 올바른 형태는 계속 지우고 계속 더하는 것 — 프로젝트가 조금이라도 성숙해지면 CLAUDE.md는 점점 더 짧고, 더 단단해져야 한다.

규칙 하나를 넣기 전에 자신에게 묻는 5가지 질문

CLAUDE.md에 뭘 추가하려 할 때마다 이 체크리스트를 돌린다:

  1. Claude가 파일 3개 읽으면 이 규칙을 유추할 수 있는가? 그렇다면 쓰지 말고, 스스로 읽게 둔다.
  2. 이 규칙이 직관에 반하는가? (비정상적 임계값, 주류에서 벗어난 라이브러리 선택, 상류 기본 동작에 반하는 설정) 반직관이라면 반드시 쓴다.
  3. 이 규칙이 codebase 전체의 invariant인가, 한 파일에만 영향을 주는가? 단일 파일은 코드 주석으로, CLAUDE.md로 올리지 말자.
  4. 이 규칙을 어기면 Claude가 조용히 틀린 일을 하는가? (에러 안 나지만 의미 틀림 — DB 잘못, 번역 누락, policy 검사 누락) 그렇다면 반드시 쓴다.
  5. 3줄로 설명할 수 있는가? 못하면 아직 네 자신이 정리가 안 된 것 — 쓰지 마라.

5가지를 다 통과하는 규칙만 남긴다; 하나라도 답이 안 나오는 건 지우거나 다시 쓴다.

한 줄 요약

CLAUDE.md의 올바른 용법은 "프로젝트 소개"가 아니라, 너와 코드베이스 사이에서 Claude가 아무리 읽어도 채우지 못하는 암묵지를 압축하는 것 — 비정상적인 선택, 보이지 않는 임계값, 기본값에 반하는 관례, 프로젝트 범위의 외부 제약.

추가하는 모든 줄은 이 질문에 답해야 한다: "이게 Claude가 코드에서 못 읽어내는 것인가?" 못 읽어낸다 — 남긴다. 읽어낸다 — 지운다.

이렇게 쓴 CLAUDE.md는 보통 초안보다 절반 이상 짧아진다. 그러나 매 대화에 대한 가치는 수천 자 기능 설명보다 압도적으로 크다. 그리고 그것은 늘 "아직 다 못 썼다" — 새 기능을 낼 때마다 빠뜨린 invariant가 드러나고, 예전에 썼던 문단은 이제 지울 수 있게 된다. 지우고 더하고, 지우고 더한다 — 그게 CLAUDE.md의 일상 유지보수다.