Claude Code 디버깅 워크플로: 관찰, 재현, 수정, 회귀 테스트
TypeScript, Node, Vitest로 Claude Code 디버깅을 조사, 재현, 수정, 회귀 테스트까지 진행합니다.
Claude Code는 오류 메시지만으로도 그럴듯한 수정안을 만들 수 있습니다. 하지만 실무 디버깅은 그럴듯한 patch보다 원인 증명과 회귀 테스트가 더 중요합니다. 관찰, 가설, 최소 재현, 수정, 회귀 테스트를 분리해야 같은 bug가 다른 입력, 다른 언어 페이지, 다른 배포 경로에서 돌아오지 않습니다.
이 글은 TypeScript, Node, Vitest를 기준으로 설명합니다. 실패 조건을 보존하고 싶다면 Claude Code and TDD를, 예외 처리 설계가 불명확하다면 error handling patterns를, 검증 command 자체가 실패한다면 build error triage loop를 함께 보세요.
Claude Code에 조사 순서를 준다
“이 오류 고쳐줘”라고만 말하면 Claude Code는 가장 빠른 patch로 이동하기 쉽습니다. 전체 오류, stack trace, 재현 단계, 입력 형태, 기대 결과, 실제 결과, 최근 변경 파일을 먼저 제공합니다. 그리고 코드 수정 전에 세 가지 원인 가설을 쓰게 합니다.
관찰은 사실을 모으고, 가설은 가능한 원인을 설명하고, 최소 재현은 실패 test나 script로 바뀝니다. 수정은 작게 유지하고, 회귀 테스트는 저장소에 남깁니다. 이 순서가 있어야 reviewer가 변경 이유를 다시 추측하지 않습니다.
{
"name": "claude-debug-lab",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.6.0",
"vitest": "^3.0.0"
}
}
예시 1: undefined map 오류를 규칙으로 바꾸기
첫 번째 예시는 데이터가 오기 전에 map을 호출하는 문제입니다. 목표는 crash를 없애는 것뿐 아니라, payload가 없으면 빈 배열, 빈 이름은 제거, 유효한 이름은 trim한다는 규칙을 남기는 것입니다.
export type User = {
id: string;
name?: string | null;
};
export function normalizeUsers(users: User[] | null | undefined): string[] {
if (!Array.isArray(users)) return [];
return users
.filter((user): user is User & { name: string } => typeof user.name === "string")
.map((user) => user.name.trim())
.filter((name) => name.length > 0);
}
import { describe, expect, it } from "vitest";
import { normalizeUsers } from "../src/normalize-users.js";
describe("normalizeUsers", () => {
it("returns an empty list when the API payload is missing", () => {
expect(normalizeUsers(undefined)).toEqual([]);
expect(normalizeUsers(null)).toEqual([]);
});
it("keeps only usable display names", () => {
expect(
normalizeUsers([
{ id: "u1", name: " Masa " },
{ id: "u2", name: "" },
{ id: "u3", name: null },
]),
).toEqual(["Masa"]);
});
});
Claude Code에는 이렇게 요청합니다.
Cannot read properties of undefined (reading 'map') 오류가 있습니다.
normalizeUsers는 API payload가 없어도 실패하지 않고, 빈 display name을 제거해야 합니다.
먼저 실패하는 Vitest를 추가한 뒤 최소 수정만 하세요.
마지막에 npm test와 npm run typecheck를 실행하세요.
흔한 함정은 users ?? []에서 멈추는 것입니다. crash는 사라져도 null name, 빈 문자열, 배열이 아닌 payload의 동작은 남습니다. Claude Code에 exception만 주지 말고 원하는 규칙을 주세요.
예시 2: 마지막 오류를 숨기지 않는 async retry
async retry bug는 간헐적인 문제처럼 보입니다. 좋은 테스트는 호출 횟수, wait 동작, 결국 성공하는 경우, 전부 실패하는 경우를 모두 확인합니다. 마지막 오류가 숨겨지면 incident response에서 원인을 찾기 어렵습니다.
type RetryOptions = {
times: number;
delayMs: number;
sleep?: (ms: number) => Promise<void>;
};
const defaultSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
export async function retry<T>(
task: () => Promise<T>,
{ times, delayMs, sleep = defaultSleep }: RetryOptions,
): Promise<T> {
let lastError: unknown;
for (let attempt = 1; attempt <= times; attempt += 1) {
try {
return await task();
} catch (error) {
lastError = error;
if (attempt < times) await sleep(delayMs);
}
}
throw lastError instanceof Error ? lastError : new Error("Retry failed");
}
테스트를 부탁할 때 mock isolation, 큰 refactor 금지, 마지막 오류 보존을 명시하세요. 조사 중 temporary log는 괜찮지만, 최종 patch에는 지워야 합니다. 제품에 필요한 structured logging이라면 별도 설계로 남깁니다.
예시 3: 날짜 경계를 최소 재현으로 고정하기
세 번째 예시는 날짜 경계 버그입니다. 화면에서는 “3월 내보내기에서 3월 31일 주문이 빠진다”처럼 보이지만, 실제 원인은 버튼이 아니라 선택한 달의 시작과 다음 달 시작을 비교하는 조건인 경우가 많습니다.
export type Order = {
id: string;
createdAt: string;
total: number;
};
export function exportMonthlyOrderIds(orders: Order[], month: string): string[] {
const [yearText, monthText] = month.split("-");
const year = Number(yearText);
const monthIndex = Number(monthText) - 1;
const start = new Date(Date.UTC(year, monthIndex, 1));
const end = new Date(Date.UTC(year, monthIndex + 1, 1));
return orders
.filter((order) => {
const createdAt = new Date(order.createdAt);
return createdAt >= start && createdAt < end;
})
.map((order) => order.id);
}
import { describe, expect, it } from "vitest";
import { exportMonthlyOrderIds } from "../src/export-orders.js";
describe("exportMonthlyOrderIds", () => {
it("includes orders from the first day through the last moment of the month in UTC", () => {
const orders = [
{ id: "feb-end", createdAt: "2026-02-28T23:59:59.999Z", total: 1000 },
{ id: "mar-start", createdAt: "2026-03-01T00:00:00.000Z", total: 2000 },
{ id: "mar-end", createdAt: "2026-03-31T23:59:59.999Z", total: 3000 },
{ id: "apr-start", createdAt: "2026-04-01T00:00:00.000Z", total: 4000 },
];
expect(exportMonthlyOrderIds(orders, "2026-03")).toEqual(["mar-start", "mar-end"]);
});
});
Claude Code에는 로컬 타임존에 의존하지 말고, 선택한 달의 시작은 포함하고 다음 달 시작은 제외하라고 적습니다. 그러면 애매한 날짜 파싱 대신 리뷰 가능한 경계 테스트가 남습니다.
복사 가능한 디버깅 프롬프트
목표:
원인을 찾고, 회귀 테스트를 추가하고, 가장 작은 유효 diff로 수정하세요.
관찰:
- 전체 오류:
- 재현 단계:
- 기대 결과:
- 실제 결과:
- 최근 변경 파일:
제약:
- 수정 전에 가설 3개를 제시
- 큰 refactor 금지
- any로 타입 오류 숨기지 않기
- 임시 로그는 마지막에 제거
- 마지막에 npm test와 npm run typecheck 실행
보고:
- 원인
- 변경 파일
- 추가한 회귀 테스트
- 남은 위험
이 템플릿은 Claude Code를 patch 생성기가 아니라 조사 파트너로 만듭니다. reviewer는 모든 line을 읽기 전에 원인, 증거, 남은 위험을 볼 수 있습니다. 팀에서는 review gate에 같은 항목을 넣어 AI diff와 인간 diff를 같은 기준으로 봅니다.
디버깅하면서 수익 경로를 깨지 않기
공개 사이트 디버깅은 CTA도 봐야 합니다. 본문 수정으로 무료 PDF form이 아래로 밀리거나, Gumroad link가 바뀌거나, 상담 경로가 찾기 어려워질 수 있습니다. 인기 글일수록 revenue routing도 디버깅 surface에 포함해야 합니다.
CTA 분기는 작은 rule로 고정할 수 있습니다.
const debuggingRoutes = {
first_error: "free_pdf",
repeated_bugfixes: "prompt_templates",
setup_or_ci_blocked: "setup_guide",
team_process_blocked: "consultation",
};
export function chooseDebuggingCta(intent) {
return debuggingRoutes[intent] ?? "free_pdf";
}
console.log(chooseDebuggingCta("repeated_bugfixes"));
초보자와 비교 독자에게는 free cheatsheet가 맞습니다. 반복 debugging과 review에는 50 Prompt Templates가 맞습니다. 권한, CI/CD, setup blocker에는 Setup Guide가 맞습니다. 팀 도입과 운영 설계에는 상담이 맞습니다.
이 글에서 확인한 것
이번 rewrite는 깨끗한 UTF-8 본문, 실행 가능한 code block, internal link, external link, 무료 PDF, Gumroad, 상담 경로를 포함합니다. 다음 숫자는 이 debugging slug의 PDF 등록, Prompt Templates 클릭, Setup Guide 클릭, Training 방문입니다.
공개 후 디버깅 확인 메모
디버깅 글을 공개할 때는 code correctness만으로 끝내지 않습니다. 공개 URL에서 h1, canonical, 본문 첫 부분, hero image, 무료 PDF form, Gumroad link, 상담 link를 확인합니다. 인기 글의 debugging workflow 개선은 독자의 다음 행동에 바로 영향을 줍니다. 설명은 좋아졌지만 PDF signup, Gumroad product, consultation route가 틀리면 revenue path는 끝나지 않았습니다.
다국어 버전은 code example을 공유해도 되지만, 설명과 CTA는 대상 언어여야 합니다. 한국어 독자가 관찰, 재현, 수정, 테스트 순서를 한국어로 이해할 수 있어야 합니다. Screenshot review에서는 opening body, prompt template 영역, CTA 영역을 확인합니다.
다음 숫자는 이 slug의 무료 PDF 등록, Prompt Templates 클릭, Setup Guide 클릭, Training 방문입니다. Debugging 독자는 pain이 명확하므로 PV보다 downstream movement가 중요합니다. 다음 rewrite 전에 이 숫자를 Claude Code에 먼저 전달합니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.