JavaScript 디버깅 마스터 — Chrome DevTools 실전 가이드

Chrome DevTools를 제대로 활용하자

console.log로 디버깅하는 것은 가장 기본적인 방법이지만, 복잡한 버그를 추적할 때는 한계가 있습니다. Chrome DevTools는 브레이크포인트, 네트워크 분석, 메모리 프로파일링, 성능 측정 등 강력한 디버깅 도구를 제공합니다. 이 글에서는 DevTools의 핵심 기능을 실전 예제와 함께 정리합니다.

console API 제대로 사용하기

console.log 외에도 다양한 메서드가 있습니다. 상황에 맞는 메서드를 사용하면 디버깅 효율이 크게 향상됩니다.

// console.table — 배열/객체를 표 형태로 출력
const users = [
  { name: "홍길동", role: "admin", active: true },
  { name: "김영희", role: "editor", active: false },
  { name: "이철수", role: "viewer", active: true },
];
console.table(users);
// ┌─────────┬──────────┬──────────┬────────┐
// │ (index) │  name    │  role    │ active │
// ├─────────┼──────────┼──────────┼────────┤
// │    0    │ "홍길동"  │ "admin"  │  true  │
// │    1    │ "김영희"  │ "editor" │ false  │
// │    2    │ "이철수"  │ "viewer" │  true  │
// └─────────┴──────────┴──────────┴────────┘

// console.group — 관련 로그를 그룹으로 묶기
console.group("사용자 데이터 로딩");
console.log("API 호출 시작");
console.log(`URL: /api/users`);
console.warn("캐시 미스 — 서버에서 가져옴");
console.groupEnd();

// console.time — 실행 시간 측정
console.time("데이터 처리");
const processed = users.filter((u) => u.active);
console.timeEnd("데이터 처리");
// 데이터 처리: 0.012ms

// console.assert — 조건이 false일 때만 출력
console.assert(users.length > 0, "사용자 목록이 비어있습니다");
console.assert(users.length > 10, "10명 미만입니다"); // 출력됨

// console.trace — 호출 스택 추적
function innerFunction() {
  console.trace("여기서 호출됨");
}
function outerFunction() {
  innerFunction();
}
outerFunction();
// Trace: 여기서 호출됨
//     at innerFunction (app.js:35)
//     at outerFunction (app.js:38)
메서드용도
console.table()배열/객체를 표로 출력API 응답 데이터 확인에 유용
console.group()로그 그룹화중첩된 처리 과정 추적
console.time()실행 시간 측정성능 병목 지점 파악
console.assert()조건부 출력불변 조건 검증
console.trace()호출 스택 출력함수 호출 경로 추적
console.count()호출 횟수 카운트이벤트 발생 빈도 확인

브레이크포인트 활용

브레이크포인트는 코드 실행을 특정 지점에서 멈추고 변수 상태를 검사할 수 있는 강력한 도구입니다.

// 1. debugger 키워드 — 코드에서 직접 브레이크포인트 설정
function processOrder(order) {
  const total = order.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  // 총액이 음수인 경우에만 멈춤
  if (total < 0) {
    debugger; // DevTools가 열려 있을 때만 동작
  }

  const tax = total * 0.1;
  const grandTotal = total + tax;
  return { total, tax, grandTotal };
}

// 2. 조건부 브레이크포인트 (DevTools에서 설정)
// Sources 패널 → 줄 번호 우클릭 → "Add conditional breakpoint"
// 조건 예시: order.total > 100000

// 3. DOM 변경 브레이크포인트
// Elements 패널 → 요소 우클릭 → "Break on"
//   - subtree modifications: 자식 요소 변경 시
//   - attribute modifications: 속성 변경 시
//   - node removal: 요소 삭제 시

// 4. XHR/Fetch 브레이크포인트
// Sources 패널 → XHR/fetch Breakpoints → URL 패턴 추가
// 예: "api/users" 포함하는 요청에서 멈춤

// 5. 이벤트 리스너 브레이크포인트
// Sources 패널 → Event Listener Breakpoints
// 예: Mouse → click 체크 → 클릭 이벤트 발생 시 멈춤

네트워크 패널 활용

네트워크 요청의 상세 정보를 확인하고, 느린 요청이나 실패한 요청을 추적합니다.

// fetch 인터셉터 — 모든 네트워크 요청 로깅
const originalFetch = window.fetch;

window.fetch = async function (...args) {
  const url = typeof args[0] === "string" ? args[0] : args[0].url;
  const method =
    args[1]?.method || "GET";

  console.group(`[Fetch] ${method} ${url}`);
  console.time("응답 시간");

  try {
    const response = await originalFetch.apply(this, args);

    console.log("상태:", response.status);
    console.log("헤더:", Object.fromEntries(response.headers));
    console.timeEnd("응답 시간");

    if (!response.ok) {
      console.error(`HTTP 에러: ${response.status} ${response.statusText}`);
    }

    console.groupEnd();
    return response;
  } catch (error) {
    console.error("네트워크 에러:", error.message);
    console.timeEnd("응답 시간");
    console.groupEnd();
    throw error;
  }
};

// 네트워크 패널 주요 기능:
// - 필터: XHR, Fetch, JS, CSS, Img 등 타입별 필터링
// - Throttling: 느린 네트워크 시뮬레이션 (Slow 3G, Offline)
// - Waterfall: 요청 타이밍 시각화
// - Preserve log: 페이지 이동 시에도 로그 유지

성능 프로파일링

Performance 패널로 렌더링 병목을 찾고, 프레임 드롭의 원인을 파악합니다.

// Performance API로 커스텀 마커 설정
function measureRender(componentName) {
  performance.mark(`${componentName}-start`);

  return {
    end() {
      performance.mark(`${componentName}-end`);
      performance.measure(
        componentName,
        `${componentName}-start`,
        `${componentName}-end`
      );

      const entries = performance.getEntriesByName(componentName);
      const duration = entries[entries.length - 1].duration;
      console.log(`[성능] ${componentName}: ${duration.toFixed(2)}ms`);

      // 16ms(60fps) 초과 시 경고
      if (duration > 16) {
        console.warn(
          `[성능 경고] ${componentName}이 ` +
          `${duration.toFixed(0)}ms 소요됨 (16ms 초과)`
        );
      }

      return duration;
    },
  };
}

// 사용 예시
const measure = measureRender("사용자목록렌더링");
// ... 렌더링 로직 ...
const duration = measure.end();
// [성능] 사용자목록렌더링: 23.45ms
// [성능 경고] 사용자목록렌더링이 23ms 소요됨 (16ms 초과)

// Web Vitals 측정
function observeWebVitals() {
  // Largest Contentful Paint (LCP)
  new PerformanceObserver((list) => {
    const entries = list.getEntries();
    const lastEntry = entries[entries.length - 1];
    console.log(`LCP: ${lastEntry.startTime.toFixed(0)}ms`);
  }).observe({ type: "largest-contentful-paint", buffered: true });

  // Cumulative Layout Shift (CLS)
  let clsScore = 0;
  new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (!entry.hadRecentInput) {
        clsScore += entry.value;
      }
    }
    console.log(`CLS: ${clsScore.toFixed(4)}`);
  }).observe({ type: "layout-shift", buffered: true });
}

메모리 디버깅

메모리 누수는 장시간 실행되는 SPA에서 흔히 발생합니다. DevTools의 Memory 패널로 탐지합니다.

// 메모리 누수 일반적인 원인과 해결

// 1. 해제되지 않은 이벤트 리스너
class BadComponent {
  constructor() {
    // 누수: 컴포넌트 제거 후에도 리스너 남음
    window.addEventListener("resize", this.handleResize);
  }
  handleResize = () => {
    console.log("resize");
  };
}

class GoodComponent {
  #controller = new AbortController();

  constructor() {
    // AbortController로 리스너 일괄 해제 가능
    window.addEventListener("resize", this.handleResize, {
      signal: this.#controller.signal,
    });
  }

  handleResize = () => {
    console.log("resize");
  };

  destroy() {
    this.#controller.abort(); // 모든 리스너 한 번에 해제
  }
}

// 2. 클로저에 의한 누수
function createLeakyHandler() {
  const hugeData = new Array(1000000).fill("leak");
  // hugeData가 클로저에 의해 유지됨
  return () => console.log(hugeData.length);
}

// 3. DOM 참조에 의한 누수
const detachedNodes = new Map();
function removeElement(id) {
  const element = document.getElementById(id);
  detachedNodes.set(id, element); // DOM에서 제거해도 참조 유지
  element.remove();
  // 해결: detachedNodes.delete(id);
}

// Memory 패널 사용법:
// 1. Heap snapshot → 메모리 사용 현황 캡처
// 2. 작업 수행 후 두 번째 snapshot
// 3. Comparison 뷰로 증가한 객체 확인
// 4. Retainers 확인 → 어떤 참조가 GC를 막는지 파악

유용한 단축키

// DevTools 열기: F12 또는 Ctrl+Shift+I (Windows)

// Console 패널 단축키
// $_   : 마지막 평가된 표현식의 결과
// $0   : Elements 패널에서 선택한 요소
// $("selector") : document.querySelector 단축
// $$("selector") : document.querySelectorAll 단축
// copy(obj) : 객체를 클립보드에 복사
// monitor(fn) : 함수 호출 시마다 로그 출력
// monitorEvents(element, "click") : 이벤트 모니터링

// 실전 활용 예시
// $0.style.border = "2px solid red"  // 선택한 요소에 빨간 테두리
// copy(JSON.stringify(data, null, 2))  // JSON 데이터 클립보드 복사
// monitor(fetchUser)  // fetchUser 호출 추적

실전 팁

  • console.log 대신 브레이크포인트: 복잡한 버그는 코드 흐름을 한 줄씩 따라가며 변수 상태를 확인하는 것이 훨씬 효율적입니다
  • Logpoints 활용: 브레이크포인트 우클릭에서 “Add logpoint”를 선택하면 코드 수정 없이 로그를 출력할 수 있습니다
  • Blackboxing: Sources 패널에서 라이브러리 파일을 블랙박스 처리하면 내 코드만 디버깅됩니다
  • Network 조건부 필터: status-code:500이나 larger-than:1M로 특정 조건의 요청만 필터링합니다
  • Snippets: Sources 패널의 Snippets에 자주 사용하는 디버깅 코드를 저장해둡니다
  • Performance Monitor: Ctrl+Shift+P에서 “Performance Monitor”를 검색하면 실시간 CPU, 힙, DOM 노드 수를 모니터링할 수 있습니다
  • Coverage: Ctrl+Shift+P에서 “Coverage”를 검색하면 사용되지 않는 CSS/JS 코드를 식별할 수 있습니다

이 글이 도움이 되었나요?