Claude Code로 안전한 리팩터링을 자동화하는 실무 절차
Claude Code refactoring workflow: 전후 비교, 테스트, git diff, 리뷰 체크리스트, 위험 사례와 바로 쓰는 프롬프트.
먼저 안전 경계를 정한다
Claude Code로 refactoring을 자동화할 때 가장 위험한 요청은 “이 코드베이스를 깔끔하게 정리해줘”입니다. 생산적으로 들리지만, 실제로는 리뷰하기 어려운 큰 diff가 생기기 쉽습니다. 리팩터링은 사용자가 보는 동작, API, 테스트, 외부 시스템이 기대하는 결과를 바꾸지 않고 내부 구조만 개선하는 작업입니다. 이 경계가 흐리면 자동화가 기능 변경으로 변합니다.
이 글에서는 Claude Code를 마법 버튼이 아니라 실무 파트너로 다룹니다. 먼저 조사하고, 작은 계획을 만들고, 하나의 주제만 수정하고, 검증 명령을 실행하고, 마지막에 git diff를 설명하게 합니다. 공식 문서의 common workflows는 이 방식의 좋은 참고 자료입니다. 명령 권한과 프로젝트 설정은 settings를 함께 확인하세요.
팀에서 아직 운영 규칙을 만드는 중이라면 Claude Code 권한 설정과 Claude Code 컨텍스트 관리도 같이 읽어보면 좋습니다. 이 글의 초점은 매일 사용할 수 있는 workflow입니다. 무엇을 먼저 물어볼지, 초보자가 어떤 변경부터 시도할지, 어떻게 테스트하고 어떻게 리뷰할지를 정리합니다.
Masa의 검증 메모: 작은 실험에서는 변수명 정리, pure function 추출, TypeScript 타입 명확화, 회귀 테스트 추가에서 Claude Code가 안정적이었습니다. 반대로 “이 service를 모던하게 바꿔줘”처럼 넓은 prompt는 diff가 커졌습니다. 실무에서는 지루한 방식이 이깁니다. 좁은 범위, 명확한 테스트, 읽을 수 있는 diff가 핵심입니다.
안전 workflow: 조사, 계획, 하나의 diff, 검증
팀이 충분히 익숙해질 때까지 이 순서를 고정합니다.
| 단계 | Claude Code가 하는 일 | 사람이 확인할 것 |
|---|---|---|
| 1. 조사 | 대상 파일, 의존성, 테스트를 읽음 | 범위가 너무 넓지 않은가 |
| 2. 계획 | 세 단계 이하의 작은 계획 제안 | 숨은 동작 변경이 없는가 |
| 3. 수정 | 하나의 주제만 변경 | diff를 읽을 수 있는가 |
| 4. 검증 | test, typecheck, lint 실행 | 실패 원인이 설명되는가 |
| 5. 리뷰 | git diff와 risk 요약 | before/after 동작이 같은가 |
처음에는 편집 금지 prompt로 시작합니다.
이 저장소에서 안전하게 refactoring할 수 있는 후보를 조사하세요.
아직 파일은 수정하지 마세요.
조건:
- 외부 동작을 변경하지 않는다
- 한 diff는 최대 3개 파일
- 기존 테스트가 있는 영역을 우선한다
- 후보, 이유, 검증 명령, 위험을 표로 출력한다
“아직 파일은 수정하지 마세요”가 중요합니다. Claude Code는 요청이 실행 가능해 보이면 빠르게 편집으로 넘어갈 수 있습니다. 조사와 구현을 나누면 사고가 줄어듭니다.
작업 전에는 branch를 만들고 baseline을 확인합니다.
git status --short
git checkout -b refactor/safe-extract-order-total
npm test
npm run typecheck
npm run lint
명령은 프로젝트의 package.json에 맞게 바꾸세요. 수정 전부터 테스트가 실패한다면 먼저 기록해야 합니다. 그렇지 않으면 나중에 실패 원인이 기존 문제인지 Claude Code 변경인지 구분하기 어렵습니다.
Use case 1: 이름 변경과 작은 pure function 추출
가장 안전한 첫 연습은 이름 개선과 pure function 추출입니다. pure function은 같은 입력에 같은 출력을 반환하고, DB 업데이트, 이메일 전송, API 호출, 전역 상태 변경 같은 부작용이 없습니다. 성공 조건이 명확해서 Claude Code가 잘 처리하는 영역입니다.
// before: src/domain/order.ts
export function calc(o: { items: { p: number; q: number }[]; d?: number }) {
let t = 0;
for (const i of o.items) {
t += i.p * i.q;
}
if (o.d) {
t = t - o.d;
}
return Math.max(t, 0);
}
코드는 짧지만 p, q, d가 의미를 숨깁니다. Claude Code에는 먼저 테스트를 추가하고 나서 이름을 개선하라고 요청합니다.
src/domain/order.ts의 calc 함수를 안전하게 refactoring하세요.
요구사항:
- 구현 변경 전에 현재 동작을 고정하는 unit test를 추가
- 이번 diff에서는 export 이름 calc를 유지
- 변수명과 타입명을 읽기 쉽게 개선
- total이 음수가 되지 않는 규칙 유지
- 변경 후 npm test -- order 실행
좋은 after는 작습니다.
// after: src/domain/order.ts
type OrderLine = {
price: number;
quantity: number;
};
type OrderInput = {
items: OrderLine[];
discount?: number;
};
export function calc(order: OrderInput): number {
const subtotal = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return Math.max(subtotal - (order.discount ?? 0), 0);
}
복사해서 쓸 수 있는 테스트:
// src/domain/order.test.ts
import { describe, expect, it } from "vitest";
import { calc } from "./order";
describe("calc", () => {
it("multiplies price and quantity", () => {
expect(calc({ items: [{ price: 1200, quantity: 2 }] })).toBe(2400);
});
it("applies discount without returning a negative total", () => {
expect(calc({ items: [{ price: 500, quantity: 1 }], discount: 800 })).toBe(0);
});
});
리뷰는 변경 파일만 봅니다.
git diff -- src/domain/order.ts src/domain/order.test.ts
npm test -- order
npm run typecheck
리뷰 질문은 “코드가 멋져 보이는가”가 아닙니다. “같은 입력이 같은 비즈니스 의미를 유지하는가”입니다. 계산식, export 이름, 테스트 설명을 확인합니다.
Use case 2: any 제거는 경계 타입부터
any를 줄이는 것은 좋지만 프로젝트 전체를 한 번에 처리하는 것은 mistake입니다. API 응답, 폼 입력, 설정 파일, webhook, import row처럼 외부 데이터가 들어오는 경계부터 시작하세요.
// before: src/lib/user-api.ts
export async function fetchUser(id: string): Promise<any> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
export function getDisplayName(user: any): string {
return user.profile.displayName || user.name;
}
좁은 목표와 누락 데이터 처리 방식을 함께 적습니다.
src/lib/user-api.ts에서 any 사용을 줄이세요.
요구사항:
- API 응답 타입 추가
- fetch URL과 반환 의미는 변경하지 않음
- profile이 없어도 getDisplayName이 깨지지 않게 함
- 현재 display name 동작을 테스트로 고정
- npm test -- user-api 및 npm run typecheck 실행
첫 diff는 이 정도면 충분합니다.
// after: src/lib/user-api.ts
export type UserResponse = {
id: string;
name: string;
profile?: {
displayName?: string;
};
};
export async function fetchUser(id: string): Promise<UserResponse> {
const response = await fetch(`/api/users/${id}`);
return response.json() as Promise<UserResponse>;
}
export function getDisplayName(user: UserResponse): string {
return user.profile?.displayName ?? user.name;
}
이 cast는 runtime 데이터를 검증하지 않습니다. 입력 검증이 필요하면 두 번째 diff에서 zod나 기존 parser를 추가하세요. 초보자 diff에서 “any 제거”와 “검증 라이브러리 도입”을 섞으면 리뷰 부담이 커집니다.
// src/lib/user-api.test.ts
import { describe, expect, it } from "vitest";
import { getDisplayName, type UserResponse } from "./user-api";
describe("getDisplayName", () => {
it("uses profile displayName when present", () => {
const user: UserResponse = {
id: "u1",
name: "Masa",
profile: { displayName: "Masa I." },
};
expect(getDisplayName(user)).toBe("Masa I.");
});
it("falls back to name when profile is missing", () => {
expect(getDisplayName({ id: "u2", name: "Guest" })).toBe("Guest");
});
});
리뷰에서는 위험한 shortcut을 찾습니다. as any, 삼킨 error, 빈 문자열 fallback, optional field 의미 변경은 모두 확인 대상입니다.
Use case 3: 큰 함수는 test harness 후에 나눈다
큰 service 함수는 개선하고 싶어지지만, 동작이 숨어 있는 곳이기도 합니다. 주문, 결제, 권한, 알림, import job은 검증, 계산, 저장, 부작용이 섞입니다. Claude Code에는 먼저 순수 계산 한 조각만 추출하게 합니다.
// before: src/services/order-service.ts
export async function createOrder(input: CreateOrderInput) {
if (input.items.length === 0) {
throw new Error("items required");
}
const subtotal = input.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const shippingFee = subtotal >= 10000 ? 0 : 800;
const total = subtotal + shippingFee;
const order = await db.order.create({
data: { userId: input.userId, subtotal, shippingFee, total },
});
await mailer.sendOrderCreated(order.id);
return order;
}
prompt에는 제외할 작업도 적습니다.
src/services/order-service.ts의 createOrder를 작게 만드세요.
이번 diff에서 할 일:
- 배송비와 총액 계산만 pure function으로 추출
- 함수명은 calculateOrderTotals
- calculateOrderTotals unit test 추가
- DB 저장과 이메일 전송 순서는 유지
이번 diff에서 하지 말 것:
- DB schema 변경
- error message 변경
- API response 형태 변경
- unrelated 함수 이동
- 전체 파일 formatting
after는 단순해야 합니다.
// after: src/services/order-service.ts
export function calculateOrderTotals(items: OrderItem[]) {
const subtotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const shippingFee = subtotal >= 10000 ? 0 : 800;
return {
subtotal,
shippingFee,
total: subtotal + shippingFee,
};
}
검증 명령:
git diff --stat
git diff -- src/services/order-service.ts
git diff -- src/services/order-service.test.ts
npm test -- order-service
Claude Code가 관련 없는 formatting까지 바꾸면 이렇게 줄입니다.
이 diff는 너무 큽니다.
formatting-only 변경을 되돌리고 calculateOrderTotals 추출과 테스트만 남기세요.
외부 동작, error text, DB write, email 순서는 바꾸지 마세요.
git diff로 리뷰하고 느낌으로 판단하지 않는다
Claude Code의 설명은 도움이 되지만 진실은 diff입니다.
git diff --check
git diff --stat
git diff --name-only
git diff --word-diff -- src/domain/order.ts
| 항목 | 확인 내용 |
|---|---|
| Behavior | 입력, 출력, 예외, HTTP status, 저장 순서가 그대로인지 |
| Diff 크기 | 사람이 한 번에 리뷰 가능한지 |
| Test | 기존 동작이 테스트로 보호되는지 |
| Type | 새 as any, 위험한 cast, 무시된 error가 없는지 |
| Side effect | API, email, billing, delete, permission 순서가 유지되는지 |
| Summary | Claude Code 요약이 실제 diff와 맞는지 |
리뷰 prompt:
이 git diff를 review하세요.
확인:
- refactoring 범위를 넘는 변경이 있는가
- 테스트로 보호되지 않은 behavior는 무엇인가
- 위험한 cast나 삼킨 error가 있는가
- 사람이 특히 봐야 할 파일은 무엇인가
다음 세 분류로 출력:
- 안전해 보임
- 사람 확인 필요
- 수정 필수
파일명과 이유 포함.
Pitfall: 흔한 failure와 risk
첫 번째 failure는 너무 넓은 prompt입니다.
이 service layer를 더 깔끔하게 만들어줘.
이 요청은 함수 추출, 이름 변경, error 설계, 파일 이동, formatting을 섞습니다. 더 안전한 prompt는 다음입니다.
createOrder의 배송비 계산만 pure function으로 추출하세요.
처리 순서, error message, return value는 바꾸지 마세요.
두 번째 risk는 테스트 없이 예뻐 보이는 diff를 받아들이는 것입니다. 가독성이 좋아져도 할인, 무료배송, 권한 거부, retry, null 처리 같은 edge case가 바뀔 수 있습니다. 세 번째 mistake는 formatter와 구조 refactoring을 섞는 것입니다. Prettier가 수백 줄을 바꾸면 실제 변경이 숨습니다. 네 번째 risk는 command permission을 처음부터 넓게 주는 것입니다. 읽기, test, typecheck, lint부터 시작하고 workflow가 안정되면 넓히세요.
Checklist와 CTA
## Refactoring checklist
- [ ] 변경 목적은 하나뿐인가
- [ ] 편집 전 baseline test를 실행했는가
- [ ] before/after behavior가 같은가
- [ ] 새 테스트나 기존 테스트가 behavior를 보호하는가
- [ ] git diff --stat이 리뷰 가능한 크기인가
- [ ] git diff --check가 통과하는가
- [ ] any, 위험한 cast, 삼킨 error가 늘지 않았는가
- [ ] DB, email, billing, delete, permission 순서가 유지되는가
최종 prompt:
안전한 refactoring diff 하나만 실행하세요.
대상:
- src/services/order-service.ts
- src/services/order-service.test.ts
성공 조건:
- 외부 동작은 바뀌지 않음
- calculateOrderTotals 추출
- 기존 테스트와 새 테스트 통과
- git diff --stat과 실행한 command 보고
금지:
- DB schema 변경
- API response 변경
- error message 변경
- unrelated file 편집
제 검증에서는 먼저 “계획만, 편집 금지”를 요청하고 마지막에 git diff 요약을 강제하는 습관이 가장 효과적이었습니다. 이 workflow를 Claude Code review checklist와 CLAUDE.md best practices와 함께 쓰면 팀에서도 반복하기 쉽습니다.
팀 차원에서 안전한 운영 모델을 만들고 싶다면 Claude Code training에서 권한, prompt, 리뷰 기준, workflow 설계를 함께 정리할 수 있습니다. 리팩터링 자동화의 가치는 화려한 diff가 아니라 유지보수 위험을 꾸준히 줄이는 데 있습니다.
무료 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, 상담 경로 체크리스트.