바이브코딩 리팩터링 전략 심층 가이드 — AI가 만든 코드를 유지보수 가능한 구조로 전환하는 5단계
클로드 코드(Claude Code)·커서(Cursor)로 빠르게 만든 코드에는 중복 함수, any 타입 남용, 비대화된 컴포넌트가 쌓입니다. AI 생성 코드의 특성을 이해하고 체계적으로 리팩터링하는 5단계 전략을 다룹니다. 코드베이스 지도 그리기, 테스트 안전망, 중복 제거, TypeScript 타입 정비, 컴포넌트 경계 재설계까지 TanStack Query·Zod·Vitest 실전 코드와 함께 정리합니다.
클로드 코드(Claude Code)로 첫 SaaS를 만들고 3개월이 지났을 때, 저는 코드베이스를 보며 멈췄습니다. 파일은 200개를 넘었고, 같은 API 호출 로직이 7곳에 흩어져 있었습니다. 타입스크립트 파일에 any가 난무했고, 컴포넌트 하나가 400줄을 넘기도 했습니다. 바이브코딩의 속도는 확실했지만, 그 대가가 서서히 나타나고 있었습니다.
이 글은 그 경험을 바탕으로 작성한 리팩터링 전략 가이드입니다. AI가 빠르게 생성한 코드를 팀이 유지보수할 수 있는 구조로 전환하는 데 필요한 5단계를 다룹니다. 단순한 코드 정리 팁이 아니라, AI 생성 코드의 특성을 이해하고 체계적으로 접근하는 방법을 실전 코드와 함께 정리합니다.
바이브코딩 코드에 리팩터링이 더 자주 필요한 이유
바이브코딩과 일반 개발의 가장 큰 차이는 컨텍스트의 단절입니다. 사람이 코드를 작성할 때는 이전 의사결정을 머릿속에 담고 일관성을 유지합니다. AI는 매 세션마다 새로운 컨텍스트에서 시작하고, 이전에 정의한 패턴을 인지하지 못할 수 있습니다.
세 가지 패턴이 반복적으로 나타납니다. 첫째, 중복 유틸리티 함수입니다. API 호출 래퍼, 날짜 포매팅, 문자열 처리가 여러 파일에서 각자 다르게 구현됩니다. 둘째, 타입 희석입니다. 빠른 구현을 위해 any와 Record<string, unknown>이 누적됩니다. 셋째, 컴포넌트 비대화입니다. AI는 요청받은 기능을 기존 컴포넌트에 추가하는 쪽을 선호해, 하나의 파일이 수백 줄로 늘어납니다.
이 세 문제가 동시에 쌓이면 코드베이스의 인지 부하가 기하급수적으로 늘어납니다. 새 기능 하나를 추가할 때 관련 파일을 10개 이상 열어야 하는 상황이 옵니다. 리팩터링은 선택이 아니라 필수가 됩니다.
1단계 — 코드베이스 지도 그리기: 리팩터링 범위 파악
무작정 파일을 수정하기 전에 전체 그림을 파악해야 합니다. 두 가지 도구로 시작합니다.
의존성 분석부터 합니다. 복잡한 의존성 트리는 리팩터링 순서에 직접 영향을 줍니다. 의존하는 쪽보다 의존받는 쪽(유틸리티, 공통 훅, API 클라이언트)을 먼저 정리해야 합니다. 의존성이 하나도 없는 파일은 가장 안전하게 리팩터링할 수 있고, 많은 파일이 의존하는 코어 모듈은 변경 시 파급 범위가 넓으므로 마지막에 다룹니다.
중복 탐지도 같이 진행합니다. 유사 함수가 얼마나 많은지 파악하는 것만으로도 리팩터링의 ROI를 예측할 수 있습니다. 중복이 많을수록 공통 모듈 추출 효과가 큽니다. 이 단계에서 실제로 리팩터링하지 않습니다. 지도만 그립니다.
중복 함수 탐지 및 의존성 분석
# 유사 함수명 패턴 검색 (fetch, get, load로 시작하는 함수들)
grep -rn "const (fetch|get|load)" src/ --include="*.ts" --include="*.tsx"
# any 타입 사용 현황 파악
grep -rn ": any" src/ --include="*.ts" --include="*.tsx" | wc -l
grep -rn "as any" src/ --include="*.ts" --include="*.tsx" | wc -l
# 라인수 200 초과 파일 목록
find src/ \( -name "*.tsx" -o -name "*.ts" \) | xargs wc -l 2>/dev/null | sort -rn | head -20
# 파일 간 순환 의존성 탐지
npx madge --circular src/
# 시각화 (의존성 그래프 이미지 생성)
npx madge --image dependency-graph.svg src/
# 중복 코드 블록 탐지 (5줄 이상)
npx jscpd src/ --threshold 5 --reporters console
2단계 — 테스트 안전망 먼저 깔기
리팩터링 중 가장 흔한 실수는 동작하는 코드를 망가뜨리는 것입니다. 테스트가 없으면 리팩터링이 아니라 재작성입니다. 완벽한 테스트 커버리지가 목표가 아닙니다. 리팩터링할 영역의 핵심 흐름을 먼저 테스트로 고정하는 것이 목표입니다.
바이브코딩 프로젝트에서 테스트 작성의 현실적인 전략은 두 가지입니다. 첫째, 리팩터링 대상 함수만 테스트합니다. 전체 커버리지를 높이려 하면 테스트 작성 자체가 프로젝트가 됩니다. 둘째, AI에게 테스트를 먼저 작성하게 합니다. "이 함수를 리팩터링하기 전에 현재 동작을 검증하는 Vitest 테스트를 작성해줘. 엣지케이스 3개 포함해서"라고 요청하면, AI가 먼저 현재 구현을 이해하고 테스트를 작성합니다. 이후 리팩터링 결과가 동일한지 검증할 수 있습니다.
통합 테스트가 없는 경우, 적어도 API 엔드포인트 계약은 MSW로 고정해둡니다. 프론트엔드 리팩터링 중 API 응답 형식 처리 코드를 건드렸다가 런타임 오류가 나는 경우가 흔합니다.
리팩터링 전 안전망 테스트 예시 (Vitest)
// utils/formatDate.test.ts — 리팩터링 전 현재 동작 고정
import { describe, it, expect } from 'vitest'
import { formatDate } from './formatDate'
describe('formatDate — 현재 동작 고정 테스트', () => {
it('ISO 문자열을 한국어 날짜로 변환한다', () => {
expect(formatDate('2026-07-12T09:00:00Z')).toBe('2026년 7월 12일')
})
it('null 입력에 빈 문자열을 반환한다', () => {
expect(formatDate(null)).toBe('')
})
it('잘못된 날짜 형식에 원본 문자열을 반환한다', () => {
expect(formatDate('not-a-date')).toBe('not-a-date')
})
})
// 테스트 통과 확인 후 리팩터링 시작
// npx vitest run utils/formatDate.test.ts
리팩터링 전 테스트 안전망 구축이 성공적인 코드 개선의 핵심이다
3단계 — 중복 제거와 공통 모듈 추출
1단계에서 파악한 중복 목록을 바탕으로 실제 리팩터링을 시작합니다. 바이브코딩 프로젝트에서 가장 자주 발견되는 중복 세 가지와 해결책입니다.
① API 호출 로직 중복입니다. 컴포넌트마다 fetch 또는 axios 호출이 직접 작성된 경우, 에러 처리 방식이 제각각이고 로딩 상태 관리 패턴도 다릅니다. 해결책은 API 클라이언트 클래스 또는 TanStack Query로 중앙화합니다. 모든 API 호출은 이 단일 경로를 통과해야 합니다.
② 유틸리티 함수 중복입니다. 날짜 포매팅, 숫자 포매팅, 문자열 처리가 여러 파일에 흩어져 있습니다. src/utils/ 디렉토리에 표준 구현 하나를 두고, 나머지는 임포트로 교체합니다. AI에게 "이 중복들을 하나의 유틸리티 함수로 통합해줘. 가장 완성도 높은 구현을 기준으로 나머지를 흡수해줘"라고 요청하면 효과적입니다.
③ 타입 정의 중복입니다. 같은 사용자 객체를 다루는데 파일마다 인터페이스가 조금씩 다릅니다. src/types/ 디렉토리에 도메인 타입을 중앙화하고, 모든 파일에서 임포트합니다. 이 과정에서 any도 함께 정리됩니다.
타입스크립트 프로젝트에서 any가 많다는 것은 컴파일러의 보호막이 뚫린 상태입니다. 런타임 오류가 개발 단계에서 발견되지 않고 프로덕션에서 터집니다. 바이브코딩 프로젝트에서 any 제거는 한 번에 전부 하려 하면 작업이 너무 커집니다. 경계 지점(Boundary Point)부터 시작하는 것이 현실적입니다.
경계 지점은 외부 데이터가 앱으로 들어오는 곳입니다. API 응답, 로컬스토리지, URL 파라미터가 대표적입니다. 이 지점에서 Zod나 수동 타입 가드를 적용하면, 이후 코드는 타입이 보장된 상태에서 흐릅니다. 안쪽에서 any를 제거하는 것보다 훨씬 효율적입니다.
AI에게 타입 정리를 맡길 때 효과적인 접근은 "이 API 응답 response.json()에서 시작해서, 이 데이터를 사용하는 모든 컴포넌트까지 타입을 추적해줘. any를 제거하고 Zod 스키마를 추가해줘"입니다. 파일 하나씩, 데이터 흐름을 따라가며 처리하는 방식이 일괄 처리보다 안전합니다.
타입 정비 중 흔히 발견되는 문제는 unknown을 남용하는 경우입니다. any를 unknown으로 바꿨지만 타입 내로잉 없이 그냥 사용하는 패턴은 any와 거의 동일한 위험을 가집니다. 반드시 typeof 체크 또는 Zod 파싱으로 구체 타입으로 좁혀야 합니다.
Zod로 API 응답 타입 경계 설정
import { z } from 'zod'
// API 응답 스키마 정의
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'viewer']),
createdAt: z.string().datetime(),
metadata: z.record(z.string()).optional()
})
type User = z.infer<typeof UserSchema>
// API 호출 지점에서 파싱 — 이후 코드는 타입 보장
async function fetchUser(id: string): Promise<User> {
const response = await fetch('/api/users/' + id)
if (!response.ok) throw new Error('HTTP ' + response.status)
const raw = await response.json()
const parsed = UserSchema.safeParse(raw)
if (!parsed.success) {
console.error('API 응답 형식 불일치:', parsed.error.format())
throw new Error('Invalid user data from API')
}
return parsed.data // User 타입 완전 보장
}
// ❌ 기존 any 패턴
function displayUser(user: any) {
return user.name.toUpperCase() // 런타임 에러 가능성
}
// ✅ 개선 후
function displayUser(user: User) {
return user.name.toUpperCase() // 컴파일 단계에서 보장
}
타입 경계 설정 후 컴파일 단계에서 오류를 잡을 수 있다
5단계 — 컴포넌트 경계 재설계와 관심사 분리
바이브코딩으로 만든 컴포넌트의 전형적인 문제는 데이터 페칭, 비즈니스 로직, UI 렌더링이 하나의 파일에 혼재한다는 점입니다. 300줄짜리 컴포넌트는 보통 세 가지 역할을 동시에 하고 있습니다. 이 세 가지를 분리하면 각각 50~100줄짜리 파일이 됩니다.
컨테이너-프레젠터 패턴이 바이브코딩 리팩터링에 잘 맞습니다. 컨테이너는 데이터 페칭과 상태 관리를 담당하고(로딩, 에러 처리 포함), 프레젠터는 순수하게 props를 받아 UI를 그립니다. 프레젠터는 테스트와 스토리북 작성이 쉬워지고, 컨테이너는 로직 테스트에 집중할 수 있습니다.
AI에게 컴포넌트 분리를 맡길 때는 "이 컴포넌트를 컨테이너-프레젠터 패턴으로 분리해줘. 컨테이너는 데이터 페칭과 상태, 프레젠터는 순수 UI만 담당하게 해줘. props 인터페이스를 명시적으로 정의해줘"라고 하면 됩니다. 단, AI가 한 번에 너무 큰 컴포넌트를 다시 작성하면 실수가 생깁니다. 파일을 반으로 쪼갠 뒤, 각각 따로 다듬는 방식이 안전합니다.
컴포넌트 경계 재설계에서 놓치기 쉬운 것은 커스텀 훅의 역할입니다. 컴포넌트에서 상태 로직만 추출해도 즉시 재사용성이 높아집니다. useUserProfile, usePostList 같은 훅은 서로 다른 컴포넌트에서 동일한 로직을 공유할 수 있게 합니다. AI는 이 분리를 제안하지 않는 경우가 많습니다. 직접 요청해야 합니다.
리팩터링 자체도 AI의 도움을 받을 수 있습니다. 단, 리팩터링 전용 프롬프트 전략이 필요합니다. 새 기능 추가 프롬프트와 리팩터링 프롬프트는 달라야 합니다.
컨텍스트 설정을 철저히 합니다. "리팩터링만 해줘. 새 기능 추가 금지. 동작은 완전히 동일해야 해. 바꾸는 것은 구조와 가독성뿐이야"라고 명시합니다. 이 지시가 없으면 AI가 더 나은 기능을 추가하거나 API를 변경해버리는 경우가 있습니다.
한 번에 하나의 변경만 요청합니다. 중복 제거 + 타입 정리 + 컴포넌트 분리를 한 번에 요청하면 AI가 너무 많은 것을 바꿉니다. 검토하기 어렵고 실수도 많습니다. "1단계: 중복만 제거. 2단계: 타입 정리"처럼 순차 요청합니다.
변경 범위를 파일 단위로 제한합니다. 클로드 코드에서 세션 시작 시 컨텍스트 파일을 명시하면, AI가 다른 파일을 건드리는 것을 방지할 수 있습니다. 리팩터링 중 예상치 못한 파일이 변경되면 추적이 어려워집니다.
리팩터링 후에는 AI에게 "이 변경이 실제로 의미가 있는지, 놓친 중복은 없는지, 더 단순화할 수 있는 부분은 없는지"를 2차 검토 요청하는 것도 효과적입니다.
리팩터링 전후 품질 지표 측정법
리팩터링이 실제로 효과가 있었는지 측정하는 지표 세 가지입니다. 직관에 의존하지 말고 수치로 확인합니다.
① 순환 복잡도(Cyclomatic Complexity)입니다. 함수 하나에 분기(if, switch, ?:)가 얼마나 있는지를 나타냅니다. 높을수록 테스트하기 어렵고 버그 가능성이 높습니다. ESLint의 complexity 규칙으로 기준을 설정합니다. 바이브코딩 초기 코드에서 10을 넘는 함수가 자주 발견됩니다. 6 이하를 목표로 합니다.
② 타입 커버리지입니다. typescript-coverage-report로 any 비율을 수치화합니다. 90% 이상을 목표로 합니다. 바이브코딩 초기에는 60~70% 수준에서 시작하는 경우가 많습니다.
③ 번들 사이즈입니다. 중복 코드 제거 후 번들 크기가 줄어야 합니다. next build의 First Load JS 수치로 변화를 추적합니다. 불필요한 라이브러리가 여러 번 임포트되던 것이 정리되면 눈에 띄는 개선이 있습니다.
품질 지표 측정 명령어
# TypeScript any 커버리지 측정
npx typescript-coverage-report --threshold 90
# ESLint 순환 복잡도 + 라인 수 제한 설정 (.eslintrc.js)
module.exports = {
rules: {
'complexity': ['warn', { max: 6 }],
'max-lines-per-function': ['warn', { max: 50 }],
'max-depth': ['warn', { max: 3 }],
}
}
# ESLint 실행 — 복잡한 함수 목록 확인
npx eslint src/ --rule '{"complexity": ["warn", 6]}'
# Next.js 번들 사이즈 분석
npx @next/bundle-analyzer
# 중복 코드 탐지 (jscpd)
npx jscpd src/ --threshold 5 --reporters console
번들 분석 도구로 리팩터링 전후 크기 변화를 추적한다
리팩터링 함정 3가지 1. 완벽주의 함정: 리팩터링을 시작했다가 연관 코드가 끝없이 나와 수주로 늘어나는 경우입니다. 해결책은 타임박스를 정하고(예: 3일), 그 범위만 처리합니다. 2. 기능 추가 유혹: 리팩터링 중 "이 부분도 개선하면 좋겠는데"라는 생각이 듭니다. 해결책은 개선 아이디어를 따로 기록해두고, 현재 PR에는 리팩터링만 포함합니다. 3. 테스트 없는 리팩터링: "구조만 바꾸는 거니까 테스트 없어도 될 것 같다"는 생각은 금물입니다. 최소한 주요 함수의 입출력 테스트만이라도 작성 후 시작합니다.
실전 사례 — 1인 개발 Next.js 프로젝트 3주 리팩터링 기록
저는 6개월간 바이브코딩으로 개발한 Next.js 사이트 12개를 운영하고 있습니다. 각 사이트는 2~3만 줄 규모입니다. 가장 먼저 리팩터링을 진행한 사이트에서 얻은 기록을 공유합니다.
리팩터링 전 상태입니다. any 타입 사용 비율 41%, 200줄 초과 컴포넌트 28개, 중복 함수 탐지 결과 34개 블록, useEffect 내 fetch 직접 호출 67곳, 빌드 경고 129개였습니다.
진행 방식입니다. 하루 2~3시간씩 3주간 진행했습니다. 1주차는 지도 그리기와 테스트 안전망 구축이었습니다. 가장 많이 사용하는 유틸리티 10개에 대한 테스트를 작성했습니다. 2주차는 중복 제거와 API 클라이언트 중앙화였습니다. 직접 fetch 호출 67곳을 TanStack Query로 교체했습니다. 3주차는 타입 정비와 컴포넌트 분리였습니다.
리팩터링 후 결과입니다. any 타입 비율 12%로 감소, 200줄 초과 컴포넌트 6개로 감소, 빌드 경고 23개로 감소, 번들 사이즈 First Load JS 기준 8% 감소, 신규 기능 추가 시 연관 파일 파악 시간 평균 70% 단축이었습니다. 가장 큰 부수 효과는 신규 기능 추가 속도가 높아진 것입니다. 리팩터링 후에는 어디를 수정해야 하는지 명확해지고, AI에게 컨텍스트를 제공하기도 훨씬 쉬워졌습니다.
처음부터 완벽한 구조를 요구하면 바이브코딩의 속도 이점이 줄어듭니다. 실무에서는 빠른 초안 생성 → 동작 확인 → 리팩터링의 사이클을 반복하는 것이 더 효율적입니다. 다만 any 남발과 API 직접 호출만큼은 처음부터 CLAUDE.md에 금지 규칙으로 명시하는 것이 장기적으로 유리합니다.
리팩터링 중 기능이 망가지는 것을 어떻게 방지하나요?
리팩터링 전에 주요 사용자 흐름(로그인, 데이터 조회, 수정)을 직접 수동 테스트하고 결과를 기록해둡니다. 리팩터링 완료 후 동일한 흐름을 다시 테스트합니다. 변경을 작은 단위로 나누고 자주 커밋하는 것도 중요합니다. 하루 리팩터링 후 반드시 빌드와 수동 테스트를 완료하고 커밋합니다.
리팩터링을 언제 시작해야 하나요?
새 기능 추가 시 기존 코드를 이해하는 데 걸리는 시간이 구현 시간보다 길어질 때, 같은 버그가 여러 곳에서 반복 발생할 때, 코드 리뷰에서 "이게 무슨 코드냐"는 피드백이 늘어날 때가 신호입니다. 바이브코딩 초반 2~3개월 지점에서 한 번 집중 리팩터링 주간을 갖는 것이 효과적입니다.
AI에게 리팩터링을 맡기면 안 되는 상황은 언제인가요?
결제, 인증, 데이터 마이그레이션처럼 핵심 비즈니스 로직을 리팩터링할 때는 반드시 개발자가 모든 변경사항을 직접 검토해야 합니다. AI는 명세에 없는 엣지케이스를 빠뜨릴 수 있습니다. 이런 코드에는 리팩터링 전 포괄적인 테스트를 먼저 작성하고, AI 변경사항을 한 줄씩 검토합니다.
리팩터링과 재작성의 차이는 무엇인가요?
리팩터링은 외부 동작(입출력, API 계약)을 유지하면서 내부 구조를 개선하는 것입니다. 재작성은 처음부터 다시 만드는 것입니다. 바이브코딩 프로젝트에서 전체 재작성은 거의 항상 과도한 선택입니다. 부분 리팩터링을 반복하는 것이 리스크가 낮고 효과도 검증하기 쉽습니다. 재작성을 고려한다면 반드시 "동작 명세"를 먼저 문서화해두세요.