본문으로 건너뛰기

20~23장: 품질 · 협력 구현 · 개발자 테스트 · 디버깅

한눈에

품질·협력·테스트·디버깅을 한 호흡으로 묶으면, "결함을 어느 시점에 잡을 것인가"라는 한 축이 드러나요.

📝 묶음 요약

20~23장은 "결함을 언제, 어디서, 어떻게 잡을 것인가"를 네 각도에서 다루는 묶음이에요. 품질의 큰 그림(20장)에서 출발해 협력 프로세스(21장), 개발자 테스트(22장), 디버깅(23장)으로 점점 손에 잡히는 단위로 좁혀 들어가요.

  • 20장 — 단일 기법으로 결함의 60%도 못 잡는다는 전제는 타입·린터·테스트·리뷰가 일상이 된 2026년 FE에서 오히려 더 잘 들어맞아요.
  • 21장 — 코드 리뷰·페어·CI·QA를 한 파이프라인으로 보면 "결함 발견 시점을 얼마나 앞당기느냐"가 팀 비용을 가르는 축이 돼요.
  • 22장 — 단위 테스트는 여전히 기본기지만, 컴포넌트와 Playwright가 섞인 FE에서는 "테스트 피라미드 vs 트로피"의 선택이 함께 따라와요.
  • 23장 — 디버깅은 가설 수립과 이분 탐색 같은 개인 사고력 + Sentry/Session Replay 같은 팀 인프라가 양쪽 다 필요한 영역이에요.

큰 그림을 훑었으니, 각 장의 판정과 체크, 그리고 미션 산출물로 들어가 볼게요.

Chapter 20. The Software-Quality Landscape

📖 원문 핵심

품질 특성 간 트레이드오프: 특정 특성을 최대화하려 하면 다른 특성과 충돌이 생기는데, 정확성을 강조하면 견고성이 낮아지고 적응성을 강조하면 견고성이 높아지는 식의 비대칭 관계가 존재해요.

명시적 품질 목표 설정: Weinberg와 Schulman의 1974년 실험에서 팀에게 최적화할 목표를 명시적으로 알려줬더니 5개 팀 중 4개 팀이 해당 목표에서 1위를 차지했고, 이는 프로그래머가 목표를 알면 그대로 달성한다는 것을 보여줘요.

결함 탐지율 비교: 단일 기법의 결함 탐지율은 최대 75%를 넘지 않으며 평균은 약 40%에 불과해서, 높은 탐지율을 달성하려면 여러 기법을 조합해서 사용해야 해요.

검사(Inspection)의 비용 효율성: 코드 검사는 테스팅보다 결함을 더 많이, 더 저렴하게 발견하며 IBM 연구에서는 검사로 결함 1건 찾는 데 3.5 스태프-시간이 소요된 반면 테스팅은 15~25 스태프-시간이 필요했어요.

소프트웨어 품질의 일반 원칙: 품질을 높이면 개발 비용이 낮아지는데, 디버깅과 재작업이 일반 개발 사이클에서 약 50%를 차지하므로 결함을 줄이는 것이 일정을 단축하는 가장 직접적인 방법이에요.

✅ 체크리스트

  • 타입 체커·린터·테스트·코드 리뷰 중 최소 3가지를 조합하고 있나요? (단일 기법으로는 결함의 60%도 못 잡아요)
  • 프로젝트의 품질 우선순위(정확성 vs 속도 vs 유지보수성)가 팀 안에서 합의되어 있나요?
  • 코딩 전에 불확실한 부분을 먼저 검증(스파이크, 프로토타입, 디자인 리뷰)하고 있나요?
  • 결함이 발견됐을 때 "어느 단계에서 잡혔어야 하는가?"를 회고하는 습관이 있나요?

결제 흐름을 결과 모델로 다시 그려보면, 같은 함수가 얼마나 다른 디버깅 난이도를 갖게 되는지 보여요.

💻 React/TS 코드 예제

심화 미션 1 — 버그가 생기기 어려운 구조로 리팩터링하기

Before ❌
export async function pay(order, user) {
if (!user) return;
if (order.status === 'paid') return;

const res = await fetch('/pay', { method: 'POST' });
if (res.ok) {
order.status = 'paid';
}
}
After ✅
// ━━ Types ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

type OrderStatus = 'pending' | 'paid' | 'failed';

interface Order {
id: string;
status: OrderStatus;
amount: number;
}

interface User {
id: string;
}

// ━━ Result Model ━━━━━━━━━━━━━━━━━━━━━━━━━━━

type PayResult =
| { ok: true; order: Order }
| { ok: false; reason: 'no_user' | 'already_paid' | 'payment_failed'; statusCode?: number };

// ━━ 상태 전이 정책 ━━━━━━━━━━━━━━━━━━━━━━━━━

function canPay(order: Order): order is Order & { status: 'pending' } {
return order.status !== 'paid';
}

function markAsPaid(order: Order): Order {
return { ...order, status: 'paid' };
}

// ━━ 외부 호출 경계 ━━━━━━━━━━━━━━━━━━━━━━━━━

interface PaymentGateway {
charge(orderId: string, amount: number): Promise<{ ok: boolean; statusCode: number }>;
}

export const defaultGateway: PaymentGateway = {
async charge(orderId, amount) {
const res = await fetch('/pay', {
method: 'POST',
body: JSON.stringify({ orderId, amount }),
});
return { ok: res.ok, statusCode: res.status };
},
};

// ━━ 핵심 로직 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

export async function pay(
order: Order,
user: User | null,
gateway: PaymentGateway = defaultGateway,
): Promise<PayResult> {
if (!user) {
return { ok: false, reason: 'no_user' };
}

if (!canPay(order)) {
return { ok: false, reason: 'already_paid' };
}

const result = await gateway.charge(order.id, order.amount);

if (!result.ok) {
return { ok: false, reason: 'payment_failed', statusCode: result.statusCode };
}

const paidOrder = markAsPaid(order);
return { ok: true, order: paidOrder };
}
테스트 코드
import { describe, it, expect, vi } from 'vitest';
import { pay } from './pay';

const baseOrder = { id: 'o1', status: 'pending' as const, amount: 10000 };
const user = { id: 'u1' };

describe('pay', () => {
it('유저 없으면 no_user를 반환한다', async () => {
const result = await pay(baseOrder, null);
expect(result).toEqual({ ok: false, reason: 'no_user' });
});

it('이미 결제된 주문이면 already_paid를 반환한다', async () => {
const result = await pay({ ...baseOrder, status: 'paid' }, user);
expect(result).toEqual({ ok: false, reason: 'already_paid' });
});

it('PG 실패 시 payment_failed와 statusCode를 반환한다', async () => {
const gateway = { charge: vi.fn().mockResolvedValue({ ok: false, statusCode: 502 }) };
const result = await pay(baseOrder, user, gateway);

expect(result).toEqual({ ok: false, reason: 'payment_failed', statusCode: 502 });
});

it('성공 시 불변의 paid 주문을 반환한다', async () => {
const gateway = { charge: vi.fn().mockResolvedValue({ ok: true, statusCode: 200 }) };
const result = await pay(baseOrder, user, gateway);

expect(result).toEqual({ ok: true, order: { ...baseOrder, status: 'paid' } });
// 원본 주문은 변하지 않음
expect(baseOrder.status).toBe('pending');
});
});

개선 포인트

  • Before: pay가 undefined를 반환하면 "유저 문제? 이미 결제? PG 에러?" 중 뭔지 알 수 없다. 브레이크포인트를 걸어 한 줄씩 따라가야 한다.
  • After: 반환값의 reason 필드 하나만 보면 실패 원인이 확정된다. 로그에 reason을 찍으면 프로덕션에서도 원인 파악이 즉시 가능하다. 각 정책 함수(canPay, markAsPaid)에 브레이크포인트를 걸면 상태 전이 단계별 관찰도 쉽다.

부연 질문

  • 어떤 종류의 버그를 예방할 수 있게 되었는가
  • 디버깅 난이도가 어떻게 달라졌는가

😈 Devil's Advocate

Chapter 21. Collaborative Construction

📖 원문 핵심

협업 구현(Collaborative Construction)의 가치: 코드 리뷰, 인스펙션, 페어 프로그래밍 같은 협업 기법은 테스트보다 더 높은 비율의 결함을 더 낮은 비용으로 발견해요.

페어 프로그래밍 효과: 비용이 인스펙션과 거의 동일한 수준이면서 결함 검출률은 40~60%에 달하며, 두 사람이 함께 코드를 작성하면 서로의 실수를 즉각 잡아낼 수 있어요.

공식 인스펙션(Formal Inspection): 사전 준비, 명확한 역할 분담, 결함 탐지에만 집중하는 원칙을 따르며, 모든 협업 기법 중 결함 검출률이 45~70%로 가장 높아요.

결함 탐지와 수정의 분리: 인스펙션 회의 중에는 결함을 찾는 데만 집중하고 수정은 하지 않아요. 수정까지 같이 하면 회의가 길어지고 핵심 목적이 흐려지기 때문이에요.

협업 기법과 테스트는 상호 보완적이에요: 협업 기법은 테스트가 잘 잡지 못하는 종류의 오류를 발견하기 때문에, 두 가지를 함께 사용해야 소프트웨어 품질을 충분히 보장할 수 있어요.

✅ 체크리스트

  • 모든 코드가 머지 전에 최소 1명의 리뷰를 거치나요?
  • 리뷰어가 스타일이 아니라 결함·설계·의도에 집중하고 있나요? (스타일은 도구에 위임)
  • 리뷰 코멘트에 severity 구분이 있나요? (must-fix / suggestion / nit)
  • 특정 모듈을 한 사람만 이해하는 "버스 팩터 1" 상황이 없나요?

리뷰·CI·QA를 한 줄에 세워두면, 어느 단계에서 결함을 가장 싸게 잡을 수 있는지 정리돼요.

🛠 심화 미션 2 — 팀의 품질 프로세스 설계하기

파이프라인 타임라인

코딩 ──→ 셀프 체크 ──→ 코드 리뷰 ──→ CI ──→ QA ──→ 릴리스
① ② ③ ④ ⑤

설계 노트

  • 핵심 문제는 결함 발견 시점이 너무 늦다는 것이다. QA에서 버그를 잡으면 수정 비용이 개발 단계 대비 5~10배 높아진다. 아래 프로세스는 결함 발견 시점을 가능한 한 앞당기는 것을 목표로 한다.
  • 목표: ①~④에서 결함의 80% 이상을 제거하여 QA는 "확인"이지 "발견"이 아닌 단계로 만든다.
  • 스타일은 자동화한다. ESLint + Prettier로 포맷·네이밍·import 순서를 CI에서 강제하면, 리뷰어가 스타일에 시간을 쓸 이유가 사라진다.
  • 리뷰어는 설계를 본다. 리뷰 체크리스트를 운영한다.
  • 리뷰 코멘트에 [설계], [nit] 접두어를 붙여 무게를 구분한다. [설계] 코멘트는 반드시 해소해야 머지 가능.
  • 모든 코드에 100% 커버리지를 요구하면 팀이 지친다. 대신 테스트 필수 영역을 정한다.

CI 게이트 규칙

  • 순수 함수 + 비즈니스 로직 폴더: 커버리지 80% 미만이면 머지 차단
  • 나머지: 커버리지 리포트만 표시, 차단 없음
  • 새로 추가된 함수에 테스트가 없으면 CI 경고 (점진적으로 차단으로 전환)

😈 Devil's Advocate

Chapter 22. Developer Testing

📖 원문 핵심

개발자 테스팅의 한계: 테스팅은 오류를 감지하는 수단이지 소프트웨어 품질을 직접 개선하는 수단이 아니며, 개발자 테스트 한 단계가 발견하는 오류는 전체의 50% 미만에 그치는 경우가 많아요.

테스트 우선(Test-First) 작성: 코드를 작성하기 전에 테스트 케이스를 먼저 작성하면 요건 문제를 조기에 발견하고 결함 수정 비용을 낮출 수 있어요.

경계값 분석(Boundary Analysis): 오류는 경계에 집중되므로 최솟값, 최댓값, 경계의 ±1 값(off-by-one)을 반드시 테스트해야 해요.

오류 집중 법칙: 프로젝트 결함의 80%는 전체 클래스·루틴의 20%에 집중되며, 50%는 단 5%의 클래스에서 발생해요.

커버리지 모니터: 커버리지 측정 없이 테스팅하면 실제로는 50~60%의 코드만 실행되므로, 커버리지 도구를 사용해 미실행 코드를 확인해야 해요.

✅ 체크리스트

  • 테스트가 경계값·에러 케이스·비정상 입력도 커버하나요? "happy path만"이지는 않나요?
  • CI에서 매 PR마다 테스트가 자동 실행되고, 실패 시 머지가 차단되나요?
  • 결함이 집중되는 모듈(error-prone)에 테스트를 더 두텁게 작성하고 있나요?
  • 테스트 이름이 "무엇을 검증하는지" 설명하나요? (test1이 아니라 should show error when email is invalid)
  • 커버리지 숫자를 목표로 삼지 않고, "이 테스트가 실제로 무엇을 보호하는가?"를 묻고 있나요?

주문 등록 함수를 순수 함수와 의존성 경계로 풀어내면, 테스트 코드가 자연스럽게 따라와요.

💻 React/TS 코드 예제

기본 미션 1 — 테스트 가능하도록 리팩터링하기

Before ❌
export async function registerOrder(order) {
if (!order.user) throw new Error('no user');

const price = order.items.reduce((sum, i) => sum + i.price, 0);

await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify({ ...order, price }),
});

console.log('order saved');
}
After ✅
// ━━ Types ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

interface OrderItem {
id: string;
price: number;
}

interface Order {
user: string;
items: OrderItem[];
}

interface OrderApi {
save(payload: Order & { price: number }): Promise<void>;
}

interface Logger {
info(message: string): void;
}

// ━━ 순수 함수: 테스트 가능한 핵심 로직 ━━━━━

export function validateOrder(order: Order): void {
if (!order.user) throw new Error('no user');
}

export function calcTotalPrice(items: OrderItem[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}

// ━━ 외부 의존성: 기본 구현 ━━━━━━━━━━━━━━━━━

export const defaultOrderApi: OrderApi = {
async save(payload) {
await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(payload),
});
},
};

export const defaultLogger: Logger = {
info(message) {
console.log(message);
},
};

// ━━ 조합: 의존성 주입 가능한 진입점 ━━━━━━━━

export async function registerOrder(
order: Order,
api: OrderApi = defaultOrderApi,
logger: Logger = defaultLogger,
): Promise<void> {
validateOrder(order);

const price = calcTotalPrice(order.items);

await api.save({ ...order, price });

logger.info('order saved');
}
테스트 코드
import { describe, it, expect, vi } from 'vitest';
import { validateOrder, calcTotalPrice, registerOrder } from './registerOrder';

// 1) 순수 함수 — 외부 의존 없이 바로 테스트
describe('validateOrder', () => {
it('user가 없으면 에러를 던진다', () => {
expect(() => validateOrder({ user: '', items: [] })).toThrow('no user');
});

it('user가 있으면 통과한다', () => {
expect(() => validateOrder({ user: 'u1', items: [] })).not.toThrow();
});
});

describe('calcTotalPrice', () => {
it('아이템 가격의 합을 반환한다', () => {
const items = [
{ id: 'a', price: 1000 },
{ id: 'b', price: 2000 },
];
expect(calcTotalPrice(items)).toBe(3000);
});

it('빈 배열이면 0을 반환한다', () => {
expect(calcTotalPrice([])).toBe(0);
});
});

// 2) 통합 — 가짜 의존성을 주입해서 fetch/console 없이 테스트
describe('registerOrder', () => {
it('계산된 price와 함께 API를 호출한다', async () => {
const fakeApi = { save: vi.fn() };
const fakeLogger = { info: vi.fn() };

await registerOrder({ user: 'u1', items: [{ id: 'a', price: 500 }] }, fakeApi, fakeLogger);

expect(fakeApi.save).toHaveBeenCalledWith({
user: 'u1',
items: [{ id: 'a', price: 500 }],
price: 500,
});
expect(fakeLogger.info).toHaveBeenCalledWith('order saved');
});
});

개선 포인트

  • 순수 함수 추출: validateOrder와 calcTotalPrice는 외부 I/O가 없어서 mock 없이 즉시 단위 테스트가 가능하다.
  • 외부 의존성 분리: fetch와 console.log를 OrderApi/Logger 인터페이스 뒤로 숨겼다. 테스트에서는 vi.fn()으로 대체하면 된다.
  • 기본값으로 기존 동작 유지: 호출부가 의존성을 넘기지 않으면 defaultOrderApi/defaultLogger가 사용되어 기존 동작이 그대로 보존된다.

😈 Devil's Advocate

Chapter 23. Debugging

📖 원문 핵심

디버깅 vs 테스팅: 테스팅이 오류를 탐지하는 과정이라면, 디버깅은 이미 탐지된 오류의 근본 원인을 식별하고 수정하는 과정이에요.

결함을 학습 기회로: 결함은 프로그램을 이해하고, 자신의 실수 패턴을 파악하고, 코드 품질을 점검하고, 문제 해결 방식을 개선할 수 있는 드문 기회예요.

의심 영역 좁히기: 이진 탐색(binary search) 방식으로 프로그램의 절반씩 제거해 가며 결함 위치를 체계적으로 추적하면, 전체 코드를 훑는 것보다 훨씬 빠르게 결함을 찾을 수 있어요.

심리적 집합(Psychological Set): 개발자는 자신이 작성한 코드에서 보고 싶은 것만 보는 경향이 있어, 결함이 있는 구역을 무의식 중에 건너뛰는 맹점이 생겨요.

증상이 아닌 문제를 수정: 특정 값에 대한 특수 케이스 처리처럼 증상만 덮는 수정은 코드를 점점 더 취약하게 만들므로, 항상 근본 원인을 이해한 후 수정해야 해요.

✅ 체크리스트

  • 버그를 만나면 바로 코드를 수정하지 않고, 먼저 가설을 세우고 재현 조건을 최소화하나요?
  • console.log에만 의존하지 않고 breakpoint·DevTools·Network 탭을 활용하나요?
  • 에러 트래킹(Sentry 등)으로 프로덕션 에러를 자동 수집·분류하고 있나요?
  • 수정 후 "이 수정이 다른 곳을 깨뜨리지 않는가?"를 확인하나요?
  • 반복되는 버그 패턴이 있다면 린트 룰이나 테스트로 자동화했나요?

변수명과 조건 분리만 손봐도, 같은 로직이 디버거 위에서 얼마나 친절해지는지 보여줘요.

💻 React/TS 코드 예제

기본 미션 2 — 디버깅하기 쉬운 코드로 만들기

Before ❌
export function process(items) {
let result = 0;
for (const i of items) {
if (i.a) {
if (i.b) {
result += i.v * 0.9;
} else {
result += i.v;
}
}
}
return result;
}
After ✅
interface PricedItem {
active: boolean;
discountEligible: boolean;
value: number;
}

const DISCOUNT_RATE = 0.9;

function itemAmount(item: PricedItem): number {
if (!item.active) return 0;

return item.discountEligible ? item.value * DISCOUNT_RATE : item.value;
}

export function calcActiveTotal(items: PricedItem[]): number {
let total = 0;

for (const item of items) {
const amount = itemAmount(item);
// 디버깅 포인트: 각 아이템이 얼마를 기여하는지 여기서 관찰 가능
total += amount;
}

return total;
}

개선 포인트

  • 의미 있는 변수명: i.a → item.active, i.b → item.discountEligible, i.v → item.value, result → total. 브레이크포인트를 걸었을 때 변수 패널만 봐도 상태가 읽힌다.
  • 중간 상태 관찰 가능: itemAmount를 별도 함수로 분리해 amount 변수에 담았다. 루프 안에서 amount 하나만 watch하면 "이 아이템이 얼마를 기여하는지"를 바로 볼 수 있다. Before 코드는 result += 안에 계산이 인라인되어 있어 중간값을 관찰할 수 없었다.
  • 조건 분리: 중첩 if (i.a) { if (i.b) ... } 를 itemAmount 안의 가드 절 + 삼항으로 풀어서, 조건 하나하나에 브레이크포인트를 걸기 쉬워졌다.
  • 매직 넘버 제거: 0.9를 DISCOUNT_RATE 상수로 추출해 "왜 0.9인가"가 이름에 드러난다.
  • 타입 도입: PricedItem 인터페이스가 "이 함수가 어떤 모양의 데이터를 기대하는지"를 명시하여, 잘못된 데이터가 들어왔을 때 타입 레벨에서 미리 잡힌다.

😈 Devil's Advocate

원칙과 미션은 출발점일 뿐이에요. 같은 주제를 7명이 어떻게 겪었는지 이어 들어 볼게요.

💬 토론 질문 & 멤버 의견

질문 1. 두 문장 중 어디에 더 공감해? (a) 우리는 테스트를 열심히 해서 품질을 만든다 vs (b) 우리는 결함이 생기지 않는 구조를 만들어서 품질을 만든다

Alice2년차 프론트엔드 개발자 (F-pretence)
(b) — 결함이 생기지 않는 구조가 더 중요하다고 생각함. 테스트만으로는 품질을 만들 수 없고, 잘 짜여진 구조일수록 테스트도 용이해지기 때문에 짜임새 있는 구조가 먼저고 테스트는 그 뒤에 따라온다고 봄.
Crong토큰 없으면 퇴근하는 1년차 프론트엔드 개발자
(b) — 테스트는 근시안적인 장치임. 단위 테스트가 통과했다고 좋은 코드는 아니듯이, 결함이 생기지 않는 구조를 만드는 게 훨씬 중요함.
Leo아침 밥 안먹는 4년차 프론트엔드 개발자
사실 두 문장 다 완전히는 불가능하다고 생각하지만, 굳이 고르면 (b). 시스템 차원에서의 무언가가 매우 중요하다고 느낌. 현업 경험: 패키지 매니저에서 yarn의 catalogs 기능을 이용해서 패키지 버전을 일관되게 관리하고, 린트에 워닝이 있으면 push가 되지 않게끔 막아두는 식으로 시스템 차원의 장치를 두고 있음.

질문 2. "초기에 결함을 제거하는 것이 가장 싸다"는 주장, 실제 경험 기반 찬반은?

Alice2년차 프론트엔드 개발자 (F-pretence)
찬성. 모든 가능성을 초기에 고려하는 게 과한 설계가 될 수 있지만, 실제 발생 가능성이 있는 변화에는 여지를 남겨두는 것이 중요함. 현업 경험: 회사 전체에서 공통으로 쓰는 Table 컴포넌트를 props 기반으로 설계했다가, 서비스가 분리되며 props가 4~5개 이상 추가되고 조건 분기가 복잡해졌음. 결국 컴파운드 컴포넌트로 재설계했지만 이미 구현된 모든 곳을 리팩토링해야 해서 더 많은 비용이 들었음.
Crong토큰 없으면 퇴근하는 1년차 프론트엔드 개발자
찬성 — 아토믹 디자인 패턴(atom/molecule/organism)도 결국 같은 맥락이라고 봄.
Leo아침 밥 안먹는 4년차 프론트엔드 개발자
매우 찬성. 다만 현실적으로 빠르게 개발해야 할 상황이 많고, 그런 상황에서는 저점을 최대한 높이는 게 가장 중요한 전략이라고 봄. 현업 경험: 기술적 복잡도가 높아져서 해결해야 했던 문제들 대부분이, 그 당시에 해소하는 게 가장 비용이 저렴했을 것으로 예상되는 케이스였음. 시간이 많이 지난 뒤에 해소하려니 맥락이 유지되지 않는 게 가장 어려운 부분.

질문 3. 운영에서만 간헐적으로 결제가 실패하고, 로컬에서는 재현이 안 된다. 각자의 디버깅 순서는?

Alice2년차 프론트엔드 개발자 (F-pretence)
운영에만 있고 로컬에서 재현 안 되는 건 보통 환경/데이터/타이밍 셋 중 하나라고 생각해서, 그 순서대로 좁혀가는 편. 현업 경험: 광고 작업할 때가 딱 이런 케이스였음. 광고 SDK는 로컬에서 호출 자체가 허용이 안 돼서 재현 자체가 불가능했고, 결국 개발 환경에 배포해가면서 확인하는 식으로 진행했음. 이때 배운 건 "재현이 안 되는 환경에서는 한 번 배포할 때 가설을 최대한 많이 끼워넣어야 한다"는 것. 그 뒤로는 디버깅 시에는 로그를 평소보다 훨씬 촘촘하게 박아두고 배포하는 습관이 생김.
zinii클로드에게 직장을 빼앗기게 생긴 고꼬마 개발자
디버깅을 잘한다는 건 문제 본질을 명확히 파악하고 정확히 해결하는 것. 문제의 본질을 모르고 추측성 시도부터 하면 하루 종일 삽질하게 됨. 현상을 덮기 전에 각 아키텍처 계층 간의 동작 규칙을 완벽히 이해하는 것이 가장 빠른 디버깅의 지름길. 현업 경험: Next.js에서 백엔드 API 호출 시 401 Unauthorized가 계속 나는 문제. CORS/쿠키 도메인부터 의심하며 next.config의 rewrites/middleware 설정을 산발적으로 바꿨다가 시간만 날림. 근본 원인은 브라우저에서는 자동으로 첨부되는 쿠키가 서버 사이드 fetch에서는 자동 릴레이되지 않는 것이었음. Route Handler + next/headers의 cookies()로 명시적으로 Authorization: Bearer 헤더를 세팅하는 구조로 바꿈.
Amber5년차 프론트엔드 개발자, 지금은 취준생
분할정복으로 코드 영역을 좁힐 수 있어야 하고, 여러 테스트 케이스를 수집해서 가설을 세우고 해당하지 않는 영역을 지워가며 원인에 접근하는 것이 디버깅을 잘하는 것이라고 봄. 현업 경험: Next.js App Router 처음 쓸 때, 페이지 이동 시마다 최상단 로딩 스피너가 계속 보이는 이슈. Suspense 원리 이해가 부족해서 loading.tsx를 이곳저곳 넣어봄. 진짜 원인은 페이지 컴포넌트 함수에 오타로 async가 붙어있어서 내부 코드가 비동기로 취급되며 루트 loading.tsx가 보였던 것. "어떤 경우에 loading.tsx가 보이는가?"를 케이스로 먼저 정리했다면 더 빨리 찾았을 것.

의견은 저마다 다르지만, 네 장이 한 흐름으로 이어진다는 감각은 공유돼요.