30분 이내에 모든 버그를 찾아내는 디버깅 프레임워크

발행: (2026년 2월 20일 오전 07:16 GMT+9)
11 분 소요
원문: Dev.to

Source: Dev.to

프레임워크: REDUCE

R재현 → E검토 → D분할 → U이해 → C변경 → E평가

Step 1: 재현 (최대 5 분)

재현할 수 없으면, 고칠 수 없습니다. 끝.

목표: 버그를 일으키는 최소한의 단계를 찾는다.
“사용자가 뭔가를 하고 있었고 문제가 발생했다.” 와는 다릅니다.

1. Create a new user with email "test@test.com"
2. Add item to cart
3. Click checkout
4. Enter the same email "test@test.com" in billing
5. BUG: "Account already exists" error on payment page

재현이 로컬에서 안 될 경우

환경 차이를 확인하세요. “재현할 수 없는” 버그의 90 %는 다음 때문에 발생합니다:

  • 데이터 차이 (프로덕션에는 로컬에 없는 엣지 케이스가 존재)
  • 타이밍 차이 (동시 사용자를 필요로 하는 레이스 컨디션)
  • 구성 차이 (환경 변수, 기능 플래그, 패키지 버전)
  • 캐시 (로컬 캐시는 비어 있지만, 프로덕션 캐시는 오래된 데이터를 보유)

이 단계에 시간 제한을 두세요. 5 분 안에 재현하지 못하면, 더 많은 데이터를 수집하세요 (로그, 스크린샷, 사용자 세션) 그리고 다시 시도하십시오.

단계 2: 조사 (3 분)

코드를 건드리기 전에 증거를 모으세요. 알고 있는 것은 무엇인가요? (추측이 아니라)

FACTS:
- 오류: POST /api/checkout에서 "Account already exists" 발생
- 청구 이메일이 기존 사용자와 일치할 때 발생
- v2.3.1 배포 이후부터 시작 (v2.3.0에서는 발생하지 않음)
- 오류는 UserService.findOrCreate()에서 발생
- 사용자 ID가 아닌 이메일 기반 조회에서만 발생
UNKNOWNS:
- 왜 checkout이 findOrCreate를 호출하나요? (사용자를 생성하면 안 됨)
- v2.3.0과 v2.3.1 사이에 무엇이 바뀌었나요?
- 지난 주에 병합된 인증 리팩터링과 관련이 있나요?

이 내용을 적어두세요—종이에, 메모에, 주석에—어디든 물리적으로. 그러면 꼬리를 물고 돌지는 않게 됩니다.

단계 3: 나누기 (핵심 단계)

이 단계가 차이를 만듭니다. 전체 코드를 읽는 대신 버그를 이진 탐색합니다.

요청 흐름은: Router → Middleware → Controller → Service → Repository → Database.

질문: 버그가 앞부분에 있나요, 아니면 뒤부분에 있나요?

// Add a log at the midpoint: the service layer
async checkout(data: CheckoutData) {
  console.log('SERVICE INPUT:', JSON.stringify(data));
  const user = await this.userService.findOrCreate(data.email);
  console.log('SERVICE USER:', JSON.stringify(user));
  // ... rest of checkout
}

재현 단계를 실행하고 로그를 확인하세요.

  • SERVICE INPUT에 잘못된 데이터가 있으면 → 버그는 앞부분에 있습니다 (router/middleware/controller).
  • SERVICE INPUT은 정상인데 SERVICE USER가 잘못되면 → 버그는 findOrCreate에 있습니다.
  • 두 로그 모두 정상이면 → 버그는 뒤부분에 있습니다.

이제 탐색 범위를 절반으로 줄였습니다. 반복하세요. 각 나눔은 1‑2 분 정도 걸립니다. 4‑5번 나누면 전체 코드베이스에서 단일 함수로 좁혀집니다.

이것은 커밋 대신 코드 경로를 위한 git bisect와 같습니다.

Step 4: 이해하기 (증상만 고치지 말 것)

버그가 있는 함수를 찾았습니다. 변경하기 전에 잘못됐는지 이해하세요.

// The bug:
async findOrCreate(email: string): Promise {
  const user = await this.db.users.findOne({ email });
  if (!user) {
    return this.db.users.create({ email });  // BUG is HERE
  }
  return user;
}

이 함수는 기존 사용자를 찾거나 새 사용자를 생성합니다. 결제 흐름은 사용자를 찾기 위해 이 함수를 호출합니다. 청구 이메일이 계정 이메일과 다르면, 청구 이메일로 새 사용자를 만들려고 시도하게 되고—기존 계정과 충돌이 발생합니다.

근본 원인: 결제 흐름이 findByEmail을 호출해야 할 때 findOrCreate를 호출하고 있습니다. findOrCreate 동작은 회원가입에는 올바르지만 결제 흐름에는 잘못되었습니다.

증상만 (예: create 주변에 try‑catch를 두는) 임시로 고쳤다면, 아키텍처상의 실수를 가려두게 되어 나중에 더 많은 버그를 만들게 됩니다.

Step 5: 변경 (수정)

코드의 증상이 아니라 근본 원인을 해결하도록 수정하십시오.

// Fix: checkout should find, not create
async checkout(data: CheckoutData) {
  const user = await this.userService.findByEmail(data.billingEmail);
  if (!user) {
    throw new BadRequestError('No account found with this email');
  }
  // ... proceed with checkout
}

6단계: 평가 (검증)

  1. 재현 단계를 실행 → 버그가 사라짐 ✅
  2. 테스트 스위트를 실행 → 다른 문제가 없음 ✅
  3. 이 특정 버그에 대한 테스트 작성
test('checkout with non‑matching billing email returns 400', async () => {
  const user = await createUser({ email: 'account@test.com' });
  const res = await api.post('/checkout', {
    ...validCheckoutData,
    billingEmail: 'different@test.com',
    userId: user.id,
  });
  expect(res.status).toBe(400);
  // Verify no new user was created
  const users = await db.users.findAll({ email: 'different@test.com' });
  expect(users).toHaveLength(0);
});

이 테스트는 버그가 다시 발생하지 않도록 방지합니다.

고급 기술

기술: Printf 디버거 (부끄러워할 필요 없음)

console.log 디버깅은 평판이 좋지 않지만, 체계적으로 사용하면 가장 실용적인 도구입니다.

하지 말 것:

console.log('here');
console.log('here2');
console.log(thing);

해야 할 일:

console.log('[CHECKOUT] Input:', {
  email: data.email,
  itemsCount: data.items.length,
});

명확하고 상황에 맞는 접두사를 추가하고 필요한 데이터만 로그에 남기세요. 이렇게 하면 출력이 읽기 쉬워지고 이상 현상을 쉽게 발견할 수 있습니다.

디버깅 체크리스트 및 기술

1️⃣ 디버그 로그 제거

console.log('[CHECKOUT] User lookup result:', { found: !!user, userId: user?.id });
console.log('[CHECKOUT] Payment attempt:', { amount, currency, method });

Action: 작업이 끝났다면 다음 명령을 실행해 코드베이스에서 이 로그들을 모두 삭제하세요:

grep -r "console.log.*\[CHECKOUT\]" -l | xargs sed -i '/console\.log.*\[CHECKOUT\]/d'

2️⃣ 기술: 고무오리 설명

목표: 버그를 평범한 영어로 설명하기.

“청구 이메일이 기존 사용자와 일치할 때 결제 페이지에 ‘계정이 이미 존재합니다’ 라는 메시지가 표시됩니다. 결제 흐름이 findOrCreate를 호출하는데, 이 함수는 생성해서는 안 되는 상황에서도 사용자를 만들려고 합니다. 이는 청구 이메일을 별도 필드로 추가한 뒤부터 발생했습니다.”

절반 정도는 문제를 말로 풀어보는 것만으로도 해결책이 떠오릅니다.

3️⃣ 기술: 차이점 조사

# 정상 버전과 문제가 있는 버전 사이에 무엇이 바뀌었나요?
git diff v2.3.0..v2.3.1 -- src/checkout/

# 누가 결제 흐름을 바꿨나요?
git log --oneline v2.3.0..v2.3.1 -- src/checkout/

# 그 특정 커밋은 무엇을 했나요?
git show abc123

Tip: 버그는 특정 변경에 의해 도입되었습니다. 그 변경을 찾으면 근본 원인을 찾은 것입니다.

4️⃣ 기술: 환경 교체

문제: 버그가 프로덕션에서만 나타나요? 프로덕션 환경을 로컬로 가져오세요.

# 프로덕션 데이터를 로컬로 복사 (민감 정보 제외!)
pg_dump production_db --exclude-table=secrets | psql local_db

# 프로덕션 트래픽을 로컬에서 재생
# (툴: GoReplay, mitmproxy)

# 설정 파일 비교
diff config/production.yml config/local.yml

Note: 지식이 풍부한 팀원과 짧게 대화하는 것만으로도 몇 시간의 추측 작업을 절약할 수 있습니다.

5️⃣ 커뮤니티 프롬프트

당신이 가장 즐겨 사용하는 디버깅 기술은 무엇인가요? 저는 “Divide” 단계에서 대부분의 사람들이 코드를 바로 읽기 시작한다는 점을 발견했습니다. 체계적인 접근 방식을 가지고 있나요, 아니면 console.log 전사인가요?

댓글 환영합니다!

6️⃣ 보너스 자료

💡 AI 코딩 프롬프트 팩 – 50개 이상의 검증된 프롬프트, 6개의 Cursor 규칙 파일, 그리고 Claude Code 워크플로 템플릿.
Grab it on Gumroad – 평생 업데이트 포함 $9.

0 조회
Back to Blog

관련 글

더 보기 »

Conductor 업데이트: 자동 리뷰 도입

2026년 2월 13일 — 12월에 우리는 Conductor https://github.com/gemini-cli-extensions/conductor 를 소개했습니다. 이는 Gemini CLI용 확장으로, 컨텍스트를 제공하도록 설계되었습니다.