Free

Claude로 조용한 버그 디버깅하기

"클릭해도 반응 없는" 실제 버그 3개 — 프롬프트에 한 문장만 빠져도 Claude는 매번 헛다리를 짚었다.


버그에는 두 종류가 있다. 에러를 던지는 버그는 스택 트레이스를 Claude에 넘기면 30초 안에 답이 나온다. 에러를 던지지 않는 버그——버튼을 눌러도 아무 일도 안 일어남, 페이지가 안 움직임, 폼이 조용히 실패함——이런 건 Claude도 첫 시도에 틀린다. 멍청해서가 아니다. 안 보여서다.

최근 how2claude의 결제 플로우를 만들다 이런 버그를 연속으로 세 개 밟았다. 포스트모템과, 이후로 조용한 버그에 쓰게 된 프롬프트 패턴을 정리한다.

Bug 1: 지갑 주소에 숨어있던 전각 물음표

x402 크립토 결제를 연결했다. 로컬은 됐다. 프로덕션 첫 클릭에서 console에 invalid_string at payTo가 떴다. signing flow에 들어가기도 전에 facilitator의 Zod 스키마가 먼저 거부했다.

지갑은 0x...로 시작하는 42자 주소, 눈으로 보기엔 멀쩡했다. config/credentials/production.yml.enc의 wallet 필드를 Claude에게 확인시켰다.

w = Rails.application.credentials.dig(:x402, :wallet_address).to_s
puts "length: #{w.length}"
# => 43

43자. EVM 주소는 42자여야 한다. 43번째 문자는 중국어 전각 물음표 (U+FF1F). 중국어 입력기에서 복사-붙여넣기할 때 딸려 들어왔다.

Claude가 처음 이 필드를 훑었을 때는 이상을 감지하지 못했다——0x로 시작하는 문자열로 보였고 길이를 스스로 세지 않았다. 프롬프트에 한 줄 추가: "이 주소는 예상보다 1자 길다. 문자별로 codepoint를 출력해." 0xFF1F가 뜨는 순간 원인 확정.

Bug 2: Stripe Checkout 버튼이 눌러도 반응 없음

Pricing 페이지의 Subscribe 버튼. 눌러도 페이지가 안 넘어간다. 에러는 없다. network 탭에선 POST가 나가고 Stripe가 checkout.stripe.com으로 302를 돌려준다——그런데 페이지는 그대로.

먼저 Claude에게 controller를 보게 했다. 로직은 정상: redirect_to session.url, allow_other_host: true. JS를 봤다——관련 listener 없음.

한참 응답을 들여다보다 알아챘다. Content-Type: text/vnd.turbo-stream.html. Turbo가 button_to의 제출을 Turbo Stream 요청으로 가로채고 있었고, Turbo Stream은 교차 출처(302)를 따라가지 않는다——리디렉트가 먹히고 페이지는 조용히 그 자리에 남았다.

수정 한 줄:

<%= button_to "Subscribe", ..., data: { turbo: false } %>

같은 버그가 한 달 뒤 Google OAuth 버튼에서 또 터졌다. 프레임워크 레벨의 인터셉터는 조용한 버그의 온상이다——Claude는 "요청-응답" 선형 추론을 기본으로 하기 때문에, 중간 계층이 의미를 바꿔놓은 건 스스로 찾지 않는다. 프롬프트에 추가: "이 클릭이 통과하는 프레임워크 레벨 인터셉터를 전부 뽑아. 브라우저 → 서버 → 브라우저 전 경로에서 어떤 미들웨어/JS 레이어가 이 요청을 처리하는지 나열해."

Bug 3: Monthly/Yearly 토글이 눌러도 반응 없음

Pricing 페이지의 월간/연간 토글 Stimulus controller. 버튼을 눌러도 전환이 안 된다. Controller 메서드는 발화했다 (console.log로 확인), 근데 this.monthlyTarget이 undefined.

Claude의 첫 추측: target 이름 오타. 아니었다. data-pricing-target="monthly"는 DOM에 있었다.

문제는 scope였다. data-controller="pricing"이 토글 버튼 컨테이너에 붙어 있고, monthly/yearly grid 두 개는 그 컨테이너 바깥에 있었다. Stimulus는 controller 요소의 서브트리 안에서만 target을 찾는다. 바깥 건 존재하지 않는 것과 같다. data-controller를 section 전체를 감싸는 <section>으로 올리니 해결.

이 버그는 딱 "코드는 맞다"라고 외친다——이름은 다 맞고, 속성도 다 있고, 기능만 고장이다. Claude는 코드를 한 줄씩 읽는 게 기본이고, DOM 구조를 스스로 시각화하지 않는다. 프롬프트에 추가: "data-controller='pricing'이 붙은 요소의 조상과 자손을 그려. 어떤 data-pricing-target이 서브트리 안에 있고 어떤 건 밖에 있는지 표시해."

조용한 버그를 위한 3가지 프롬프트 패턴

세 버그 증상은 밖에서 보면 다 똑같았다: 클릭, 반응 없음, 에러 없음. Claude는 매번 처음엔 틀렸지만, 매번 한 문장만 더 붙이면 바로 맞췄다. 공통 패턴:

1. "예상 vs 실제"의 정량적 차이를 말해줘라. "이상하다"만으로는 부족

"지갑 주소에 문제가 있어"가 아니라 "예상보다 1자 길어".
"버튼이 안 먹혀"가 아니라 "응답은 302인데 브라우저가 따라가지 않아".
"토글이 안 돼"가 아니라 "controller 메서드는 발화하는데 target이 undefined".

차이가 좁혀질수록 Claude의 검색 공간이 좁아진다.

2. 보이지 않는 층 — 프레임워크, 브라우저, 인코딩 — 을 가리켜라

조용한 버그는 거의 업무 코드에 살지 않는다. Turbo에, Stimulus scope에, 문자 인코딩에, CSP에, CORS에, service worker에 산다. Claude의 기본값은 업무 코드를 읽는 것이다. 그 층들을 보라고 명시적으로 시켜야 한다.

3. 결론이 아니라 중간 상태를 출력하게 해라

"문자별 codepoint 출력해." "응답 헤더 나열해." "DOM 서브트리 덤프해." Claude가 추론으로 답에 도달하게 하지 말고, 중간 상태를 실체화하게 만든다. 조용한 버그의 "조용"함은 추론 체인 어느 단계에 성립하지 않는 숨은 가정이 있다는 뜻이다. 중간 상태를 실체화하는 것이 그 숨은 가정을 강제로 드러내는 방법이다.


에러 있는 버그는 Claude의 지식량을 시험한다. 에러 없는 버그는 네가 주는 신호의 질을 시험한다. 구체적일수록, 답은 빨리 나온다.