본문 바로가기
개발&프로그래밍

[Claude] Claude Code로 코드 리팩토링 & 디버깅 실전 예제

by 재아군 2026. 2. 12.
반응형

레거시 코드 현대화, 버그 추적, 성능 최적화... 개발자의 일상이지만 가장 시간이 많이 걸리는 작업입니다.

Claude Code가 이 모든 것을 자동화합니다.

이번 가이드에서는 실전 예제와 함께 Claude Code로 코드 리팩토링과 디버깅을 마스터하는 방법을 소개합니다.

 

 

왜 Claude Code로 리팩토링&디버깅인가?

전통적인 방법의 한계

수동 리팩토링:

  • 시간 소모적 (콜백 → async/await 변환에 수 시간)
  • 실수 위험 (한 곳만 빼먹어도 버그)
  • 컨텍스트 파악 어려움 (10개 이상 파일에 걸친 변경)

전통적인 디버깅:

  • 스택 트레이스 수동 분석
  • 콘솔 로그 반복 추가
  • 원인 파악에만 수 시간 소요

 

 

Claude Code의 강점

1. 대규모 컨텍스트 이해

  • 200K+ 토큰 윈도우 (최대 1M 토큰)
  • 수백 개 파일 동시 분석
  • 전체 코드베이스의 맥락 파악

2. 지능적 분석

  • 코드의 의도를 이해하고 리팩토링
  • 단순 find-and-replace가 아님
  • 근본 원인까지 추적

3. 안전한 실험

  • 자동 체크포인트로 언제든 되돌리기
  • Plan Mode로 위험 없이 분석
  • 테스트 자동 실행으로 검증

 

 

코드 리팩토링 실전 예제

예제 1: 콜백 지옥 → Async/Await

Before: 콜백 지옥

// user.js
function fetchUserData(userId, callback) {
  getUser(userId, function(err, user) {
    if (err) {
      callback(err);
    } else {
      getProfile(user.id, function(err, profile) {
        if (err) {
          callback(err);
        } else {
          getPosts(user.id, function(err, posts) {
            if (err) {
              callback(err);
            } else {
              callback(null, { user, profile, posts });
            }
          });
        }
      });
    }
  });
}

Claude Code 명령어

user.js의 fetchUserData 함수를 async/await로 리팩토링해줘.
에러 핸들링도 try-catch로 개선하고, 가독성을 높여줘.

After: 깔끔한 Async/Await

// user.js
async function fetchUserData(userId) {
  try {
    const user = await getUser(userId);
    const profile = await getProfile(user.id);
    const posts = await getPosts(user.id);
    return { user, profile, posts };
  } catch (error) {
    console.error('Failed to fetch user data:', error);
    throw error;
  }
}

개선 사항:

  •  중첩 제거 (5단계 → 1단계)
  •  에러 핸들링 통합
  •  가독성 대폭 향상

 

예제 2: React 클래스 → 함수형 컴포넌트 + Hooks

Before: Class Component

// UserProfile.js
import React, { Component } from 'react';

class UserProfile extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user: null,
      loading: true,
      error: null
    };
  }

  componentDidMount() {
    this.fetchUser();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.userId !== this.props.userId) {
      this.fetchUser();
    }
  }

  fetchUser = async () => {
    try {
      this.setState({ loading: true });
      const response = await fetch(`/api/users/${this.props.userId}`);
      const user = await response.json();
      this.setState({ user, loading: false });
    } catch (error) {
      this.setState({ error, loading: false });
    }
  }

  render() {
    const { user, loading, error } = this.state;
    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error.message}</div>;
    return <div>{user.name}</div>;
  }
}

export default UserProfile;

Claude Code 명령어

UserProfile.js를 함수형 컴포넌트와 Hooks로 변환해줘.
useState, useEffect를 사용하고 기존 기능은 모두 유지해줘.

After: Functional Component + Hooks

// UserProfile.js
import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user.name}</div>;
}

export default UserProfile;

개선 사항:

  •  라인 수 감소 (45줄 → 28줄)
  •  현대적 React 패턴
  •  더 간결한 상태 관리

예제 3: 대규모 멀티파일 리팩토링

시나리오: API 엔드포인트 변경

여러 파일에 걸쳐 있는 /api/v1/users/api/v2/users 변경

파일 목록:

  • src/services/userService.js
  • src/components/UserList.js
  • src/components/UserProfile.js
  • src/utils/apiClient.js
  • tests/userService.test.js

 

Claude Code 명령어

프로젝트 전체에서 `/api/v1/users` 엔드포인트를
`/api/v2/users`로 변경해줘.

조건:
1. 모든 파일을 찾아서 일괄 변경
2. 테스트도 함께 업데이트
3. 변경 후 테스트 실행
4. 모든 테스트가 통과하면 커밋

Claude의 작업 프로세스

[1/5] 프로젝트 전체 검색 중...
      - 5개 파일에서 `/api/v1/users` 발견

[2/5] 파일 변경 중...
      ✓ src/services/userService.js
      ✓ src/components/UserList.js
      ✓ src/components/UserProfile.js
      ✓ src/utils/apiClient.js
      ✓ tests/userService.test.js

[3/5] 테스트 실행 중...
      npm test
      ✓ 23 tests passed

[4/5] Git 커밋 생성 중...
      git add .
      git commit -m "Update API endpoints from v1 to v2"

[5/5] 완료!

소요 시간: 2분 (수동 작업 시 30분 이상)

 

디버깅 실전 예제

예제 4: TypeError 추적 및 수정

에러 발생

$ npm test

TypeError: Cannot read property 'name' of undefined
    at getUserInfo (user.js:15:25)
    at processUser (index.js:42:18)
    at Object.<anonymous> (test.js:10:5)

Claude Code 명령어

npm test를 실행했는데 TypeError가 발생해.

에러: "Cannot read property 'name' of undefined"
user.js:15 라인에서 발생.

근본 원인을 찾아서 수정해줘.

Claude의 분석 프로세스

1단계: 스택 트레이스 분석

[분석] 스택 트레이스를 따라가보겠습니다:
- test.js:10 → processUser 호출
- index.js:42 → getUserInfo 호출
- user.js:15 → user.name 접근 시 에러

user 객체가 undefined인 것 같습니다.

2단계: 코드 검토

// user.js:15
function getUserInfo(user) {
  return {
    name: user.name,  // ← 여기서 에러 발생
    email: user.email
  };
}

3단계: 근본 원인 파악

[근본 원인]
processUser 함수가 null/undefined user를 전달하고 있습니다.

index.js:42를 확인하니 데이터베이스 쿼리가 빈 결과를 반환할 때
null을 전달하고 있었습니다.

4단계: 수정 제안

두 가지 수정 방법을 제안합니다:

**방법 1: Null 체크 추가**
function getUserInfo(user) {
  if (!user) {
    throw new Error('User not found');
  }
  return {
    name: user.name,
    email: user.email
  };
}

**방법 2: Optional Chaining**
function getUserInfo(user) {
  return {
    name: user?.name ?? 'Unknown',
    email: user?.email ?? 'N/A'
  };
}

방법 1은 명시적 에러 처리, 방법 2는 기본값 제공입니다.
어떤 방법을 선호하시나요?

사용자 선택 후 적용

방법 1로 해줘. 테스트도 업데이트해서 null user 케이스를 커버해줘.

After: 수정 완료

// user.js
function getUserInfo(user) {
  if (!user) {
    throw new Error('User not found');
  }
  return {
    name: user.name,
    email: user.email
  };
}

// test.js (새로운 테스트 추가)
describe('getUserInfo', () => {
  it('should throw error when user is null', () => {
    expect(() => getUserInfo(null)).toThrow('User not found');
  });

  it('should throw error when user is undefined', () => {
    expect(() => getUserInfo(undefined)).toThrow('User not found');
  });
});

결과:

✓ 25 tests passed (including 2 new tests)

 

예제 5: 메모리 누수 디버깅

증상

프로덕션 서버에서 메모리 사용량이 계속 증가하고 있습니다.
시간당 약 100MB씩 증가 중.

 

Claude Code 명령어

Node.js 서버에서 메모리 누수가 의심됩니다.
메모리 프로파일링을 통해 원인을 찾아줘.

힌트:
- WebSocket 연결 사용 중
- Redis 캐싱 사용 중
- 주기적인 크론 작업 실행 중

 

Claude의 조사 프로세스

1단계: 프로파일링 설정

# 힙 스냅샷 생성
node --inspect --heap-prof app.js

2단계: 코드 분석

[발견된 문제]

1. WebSocket 핸들러에서 리스너 제거 누락:
   server/websocket.js:45

2. Redis 연결 풀 미해제:
   server/cache.js:78

3. 크론 작업에서 누적되는 타이머:
   server/cron.js:23

3단계: 수정 적용

문제 1: WebSocket 리스너 누수

// Before
socket.on('message', handleMessage);
socket.on('disconnect', () => {
  console.log('disconnected');
  // removeListener 호출 없음!
});

// After
socket.on('message', handleMessage);
socket.on('disconnect', () => {
  socket.removeAllListeners('message');
  console.log('disconnected');
});

문제 2: Redis 연결 미해제

// Before
async function getData(key) {
  const client = redis.createClient();
  await client.connect();
  const data = await client.get(key);
  return data;
  // 연결 종료 안 함!
}

// After
async function getData(key) {
  const client = redis.createClient();
  try {
    await client.connect();
    const data = await client.get(key);
    return data;
  } finally {
    await client.disconnect();
  }
}

문제 3: 크론 타이머 누적

// Before
setInterval(() => {
  runCleanupTask();
}, 60000);

// After
let cleanupInterval;

function startCron() {
  if (cleanupInterval) {
    clearInterval(cleanupInterval);
  }
  cleanupInterval = setInterval(() => {
    runCleanupTask();
  }, 60000);
}

process.on('SIGTERM', () => {
  clearInterval(cleanupInterval);
});

결과:

메모리 사용량 안정화:
- 이전: 시간당 +100MB
- 수정 후: 시간당 +5MB (정상 범위)

예제 6: 비동기 경합 조건 (Race Condition) 디버깅

증상

간헐적으로 사용자 프로필 업데이트가 덮어씌워집니다.
재현이 어렵고 로그에도 명확한 에러가 없습니다.

Claude Code 명령어

사용자가 프로필을 빠르게 여러 번 업데이트하면
마지막 변경이 반영되지 않는 경우가 있어.

비동기 경합 조건이 의심되는데 원인을 찾아줘.
src/controllers/profileController.js를 분석해줘.

Claude의 분석

의심 코드:

// profileController.js
async function updateProfile(req, res) {
  const { userId, updates } = req.body;

  // 현재 프로필 가져오기
  const currentProfile = await getProfile(userId);

  // 업데이트 적용
  const updatedProfile = { ...currentProfile, ...updates };

  // 저장
  await saveProfile(userId, updatedProfile);

  res.json(updatedProfile);
}

Claude의 설명:

[경합 조건 발견!]

문제: 동시에 두 요청이 들어오면:

시간 t0: 요청A - getProfile() 호출
시간 t1: 요청B - getProfile() 호출
시간 t2: 요청A - saveProfile() 호출 (name 변경)
시간 t3: 요청B - saveProfile() 호출 (email 변경)

결과: 요청B가 요청A의 name 변경을 덮어씁니다!

해결책: 낙관적 잠금(Optimistic Locking) 또는
        비관적 잠금(Pessimistic Locking) 사용

수정: 낙관적 잠금 구현

// profileController.js
async function updateProfile(req, res) {
  const { userId, updates, version } = req.body;

  try {
    // 트랜잭션 시작
    const result = await db.transaction(async (trx) => {
      // 버전과 함께 프로필 가져오기
      const currentProfile = await trx('profiles')
        .where({ user_id: userId, version })
        .first();

      if (!currentProfile) {
        throw new ConflictError('Profile was updated by another request');
      }

      // 버전 증가하며 업데이트
      const updatedProfile = {
        ...currentProfile,
        ...updates,
        version: currentProfile.version + 1
      };

      await trx('profiles')
        .where({ user_id: userId, version })
        .update(updatedProfile);

      return updatedProfile;
    });

    res.json(result);
  } catch (error) {
    if (error instanceof ConflictError) {
      res.status(409).json({
        error: 'Please refresh and try again'
      });
    } else {
      throw error;
    }
  }
}

프론트엔드 재시도 로직 추가:

// frontend/api.js
async function updateProfile(userId, updates, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const profile = await getProfile(userId);
      const response = await fetch('/api/profile', {
        method: 'PUT',
        body: JSON.stringify({
          userId,
          updates,
          version: profile.version
        })
      });

      if (response.status === 409) {
        // 충돌 발생, 재시도
        continue;
      }

      return await response.json();
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
    }
  }
}

 

고급 기법

체크포인트를 활용한 안전한 리팩토링

시나리오: 대규모 아키텍처 변경

전체 프로젝트를 MVC에서 Hexagonal Architecture로 리팩토링해줘.

단계별로 진행하고, 각 단계마다 테스트를 실행해줘.
문제가 생기면 이전 단계로 되돌릴 수 있게 해줘.

Claude의 작업 방식:

[체크포인트 1] 현재 상태 저장
[단계 1] 도메인 레이어 분리
  ✓ 테스트 통과

[체크포인트 2] 도메인 레이어 분리 완료
[단계 2] 애플리케이션 레이어 추가
  ✗ 2개 테스트 실패

[되돌리기] 체크포인트 2로 복원
[분석] 실패 원인 파악
[재시도] 수정된 방법으로 단계 2 진행
  ✓ 테스트 통과

체크포인트 명령어:

  • /rewind - 이전 체크포인트로 되돌리기
  • Esc 두 번 - 되돌리기 메뉴 열기

Plan Mode로 위험 없이 분석

언제 사용?

  • 대규모 변경 전 영향도 파악
  • 여러 접근 방법 비교
  • 위험 요소 사전 식별

사용 방법:

  1. Plan Mode 활성화
  2. Shift + Tab (Plan Mode로 전환)
  3. 분석 요청
  4. 인증 시스템을 JWT에서 OAuth2로 변경하려고 해. 영향을 받는 파일들과 변경 순서를 분석해줘.
  5. 결과 검토
  6. [영향 분석] 변경 필요 파일: 12개 - 인증 미들웨어 (4개) - 라우터 (3개) - 테스트 (5개) 위험도: 높음 - 세션 저장소 마이그레이션 필요 - 기존 토큰 무효화 전략 필요 권장 순서: 1. OAuth2 클라이언트 설정 2. 병렬 인증 지원 (JWT + OAuth2) 3. 단계적 마이그레이션 4. JWT 폐기
  7. 실행 모드로 전환
  8. Shift + Tab (Normal Mode로 전환) 좋아, 1단계부터 시작해줘.

 

멀티파일 리팩토링 전략

대규모 프로젝트 예시: API 버전 마이그레이션

REST API를 GraphQL로 전환해줘.

작업 범위:
- REST 엔드포인트: 30개
- 컨트롤러: 15개
- 테스트: 45개

조건:
1. 기존 REST API 유지 (하위 호환성)
2. GraphQL 스키마 생성
3. 리졸버 구현
4. 테스트 마이그레이션
5. 문서 업데이트

Claude의 접근 방법:

[Phase 1] 분석
- REST 엔드포인트 매핑
- GraphQL 스키마 설계
- 의존성 그래프 작성

[Phase 2] 인프라 구축
- GraphQL 서버 설정
- 미들웨어 통합
- 인증/인가 레이어

[Phase 3] 점진적 마이그레이션
- 그룹 1: User 관련 API (10개)
- 그룹 2: Product 관련 API (12개)
- 그룹 3: Order 관련 API (8개)

[Phase 4] 검증
- 통합 테스트
- 성능 테스트
- 문서 생성

 

 

 

베스트 프랙티스

리팩토링 체크리스트

단계 내용 완료
1. 준비 테스트 커버리지 확인 (90% 이상 권장)
2. 분석 Plan Mode로 영향도 파악
3. 계획 단계별 작업 순서 수립
4. 실행 작은 단위로 점진적 변경
5. 검증 각 단계마다 테스트 실행
6. 문서화 변경사항 문서 업데이트
7. 리뷰 코드 리뷰 요청

디버깅 체크리스트

단계 내용 완료
1. 재현 버그 재현 단계 작성
2. 수집 에러 메시지, 스택 트레이스 수집
3. 분석 Claude에게 상세한 컨텍스트 제공
4. 가설 원인 가설 수립
5. 검증 가설 테스트
6. 수정 근본 원인 해결
7. 테스트 회귀 테스트 추가

효과적인 Claude 사용법

DO ✅

  • 구체적인 컨텍스트 제공
  • 예상 동작과 실제 동작 명시
  • 테스트 케이스 포함
  • 단계별로 진행

DON'T ❌

  • 막연하게 "고쳐줘"만 요청
  • 에러 메시지 없이 "작동 안 해" 말하기
  • 한 번에 너무 많은 변경 요청
  • 테스트 없이 프로덕션 적용

실전 워크플로우

워크플로우 1: 레거시 코드 현대화

# 1단계: 프로젝트 분석
claude

> 이 프로젝트의 기술 부채를 분석해줘.
  오래된 패턴, 개선이 필요한 부분을 찾아줘.

# 2단계: 우선순위 설정
> 가장 위험한 기술 부채 3가지를 골라서
  각각의 개선 계획을 세워줘.

# 3단계: 단계별 실행
> 첫 번째 항목부터 시작해줘.
  콜백 기반 코드를 async/await로 변환.

# 4단계: 테스트 및 커밋
> 테스트를 실행하고, 통과하면 커밋해줘.

워크플로우 2: 버그 추적 및 수정

# 1단계: 버그 리포트
claude

> npm test를 실행하니 이런 에러가 나와:
  [에러 메시지 붙여넣기]

  재현 방법:
  1. 로그인
  2. 프로필 페이지 방문
  3. 이메일 변경 시도

# 2단계: 근본 원인 분석
> 스택 트레이스를 따라가서 근본 원인을 찾아줘.

# 3단계: 수정 적용
> 근본 원인을 수정하고, 비슷한 버그가 다른 곳에도
  있는지 확인해줘.

# 4단계: 회귀 테스트 추가
> 이 버그를 커버하는 테스트를 추가해줘.

워크플로우 3: 성능 최적화

# 1단계: 프로파일링
claude

> 이 API 엔드포인트가 느려.
  /api/users/search

  평균 응답 시간: 3초
  목표: 300ms 이하

  병목 지점을 찾아줘.

# 2단계: 최적화 제안
> 데이터베이스 쿼리 최적화 방법을 제안해줘.
  인덱스, 쿼리 개선, 캐싱 등.

# 3단계: 적용 및 측정
> 최적화를 적용하고 벤치마크 테스트를 실행해줘.
  개선 전후를 비교해줘.

 

자주 묻는 질문 (FAQ)

Q1. 리팩토링 중에 실수로 중요한 로직을 지웠어요. 되돌릴 수 있나요?

A: 네! Claude Code는 모든 변경마다 자동 체크포인트를 생성합니다. /rewind 명령어 또는 Esc 두 번으로 이전 상태로 되돌릴 수 있습니다.

Q2. 테스트가 없는 레거시 코드를 리팩토링하고 싶어요.

A: 먼저 Claude에게 테스트를 생성하도록 요청하세요:

이 코드에 대한 특성 테스트(characterization test)를 작성해줘.
현재 동작을 있는 그대로 검증하는 테스트야.

테스트 생성 후 안전하게 리팩토링할 수 있습니다.

Q3. Claude가 제안한 리팩토링이 마음에 안 들어요.

A: 체크포인트로 되돌리고 다른 접근을 요청하세요:

/rewind

다른 방법으로 다시 해줘. 이번에는 디자인 패턴 X를 사용해줘.

Q4. 버그를 찾았는데 같은 패턴이 다른 곳에도 있을까 걱정돼요.

A: Claude에게 패턴 검색을 요청하세요:

이 버그와 비슷한 패턴이 프로젝트 다른 곳에도 있는지 찾아줘.
[버그 코드 패턴]

Q5. 대규모 리팩토링은 어떻게 관리하나요?

A: Plan Mode를 사용하여 단계별 계획을 세우고, 각 단계를 독립적으로 실행하세요. 각 단계마다 테스트를 실행하여 안전성을 확보합니다.

Q6. 리팩토링 vs 재작성, 어떻게 판단하나요?

A: Claude에게 분석을 요청하세요:

이 코드를 리팩토링할지 재작성할지 판단해줘.
- 현재 코드 복잡도
- 테스트 커버리지
- 비즈니스 로직 복잡도
- 시간/비용 추정

Q7. 디버깅할 때 Claude에게 어떻게 정보를 제공하나요?

A: 다음 정보를 포함하세요:

  • 에러 메시지 전문
  • 스택 트레이스
  • 재현 단계
  • 예상 동작 vs 실제 동작
  • 관련 코드 파일 (@-멘션 사용)

Q8. 프로덕션 버그를 빠르게 수정하려면?

A:

[긴급] 프로덕션 버그 수정 필요

에러: [에러 메시지]
영향: [사용자 수, 빈도]
재현: [단계]

최소한의 변경으로 빠른 핫픽스를 제공해줘.
근본 해결은 나중에 할게.

다음 단계

더 알아보기

리팩토링과 디버깅을 마스터했다면 다음 글들을 확인하세요:

  • 11편: Claude Code GitHub Actions 자동화 - PR 자동 리뷰 설정 
  • 12편: CLAUDE.md 작성법 - 프로젝트별 최적화 컨텍스트 만들기

 

추천 리소스

 

 

결론 - AI와 함께하는 코드 개선

Claude Code는 리팩토링과 디버깅을 시간 소모적 작업에서 전략적 개선 활동으로 변모시킵니다.

핵심 요약:

  1.  대규모 컨텍스트로 멀티파일 리팩토링 가능
  2.  체크포인트로 안전한 실험과 되돌리기
  3.  Plan Mode로 위험 없이 영향도 분석
  4.  지능적 디버깅으로 근본 원인까지 추적
  5.  자동 테스트 생성으로 안전성 보장

다음 편에서는 GitHub Actions 자동화로 PR 자동 리뷰 시스템을 구축합니다!


📌 관련 글:

 

 

[Claude] Claude Code MCP 서버 설정법 - GitHub, 파일시스템 연동

Claude Code가 단순한 코드 생성을 넘어 GitHub, 데이터베이스, Slack까지 제어할 수 있다면?MCP (Model Context Protocol)가 바로 그 해답입니다.이번 가이드에서는 MCP 서버 설정부터 실전 활용까지,Claude Code를

observerlife.tistory.com

 

 

[Claude] Claude Code VS Code 연동 - 개발환경 세팅 가이드

터미널에서만 사용하던 Claude Code, 이제 VS Code에서도 직접 사용할 수 있습니다.에디터 안에서 코드 작성, 리팩토링, 디버깅을 AI와 협업하세요.이번 가이드에서는 Claude Code VS Code 확장 설치부터 실

observerlife.tistory.com

 

 

 

[Claude] Claude Code GitHub Actions 자동화 - PR 자동 리뷰 설정

PR마다 수동 코드 리뷰, 테스트 실행, 문서 업데이트... 시간이 부족하신가요?Claude Code GitHub Actions로 모든 것을 자동화하세요.이번 가이드에서는 한 줄의 명령어로 PR 자동 리뷰부터 배포까지,완전

observerlife.tistory.com

 

 

🔗 참고 자료:


Sources:

반응형

댓글