icon
10장 : 테스팅과 디버깅

고급 디버깅 기법

지난 장에서는 단위 테스트, 통합 테스트, E2E 테스트, 그리고 크로스 브라우저 테스팅을 통해 코드의 품질을 높이고 버그를 사전에 방지하는 방법을 학습했습니다. 잘 작성된 테스트 코드는 많은 버그를 걸러내 주지만, 아무리 테스트를 꼼꼼히 해도 모든 시나리오와 모든 환경을 완벽하게 커버할 수는 없습니다. 때로는 예측 불가능한 상황에서, 혹은 특정 사용자 환경에서만 발생하는 미묘한 버그들이 존재하기 마련입니다.

이러한 상황에서 개발자는 문제의 원인을 파악하고 해결하기 위한 강력한 능력이 필요합니다. 바로 디버깅(Debugging) 입니다. 디버깅은 코드의 실행 흐름을 추적하고, 변수의 값을 검사하며, 오류가 발생하는 지점을 정확히 찾아내어 수정하는 과정입니다.

이번 장에서는 단순히 console.log()를 사용하는 것을 넘어, 웹 브라우저의 개발자 도구가 제공하는 고급 디버깅 기능들을 활용하여 복잡한 자바스크립트 코드와 웹 애플리케이션의 문제를 효율적으로 진단하고 해결하는 방법을 심층적으로 알아보겠습니다.


console.log()를 넘어서는 콘솔 API

console.log()는 가장 흔하고 직관적인 디버깅 도구이지만, 콘솔 객체는 훨씬 더 많은 유용한 메서드를 제공합니다.

  • console.warn(message): 경고 메시지를 출력합니다. 경고 아이콘과 노란색 배경으로 표시되어 눈에 잘 띕니다.
  • console.error(message): 에러 메시지를 출력합니다. 에러 아이콘과 빨간색 배경으로 표시되며, 스택 트레이스(Stack Trace)도 함께 출력되어 오류 발생 위치를 쉽게 파악할 수 있습니다.
  • console.info(message): 정보성 메시지를 출력합니다. 일반적으로 console.log와 비슷하게 동작합니다.
  • console.dir(object): 객체의 속성들을 트리 형태로 자세히 보여줍니다. DOM 요소를 검사할 때 특히 유용합니다. console.log()가 DOM 요소를 HTML처럼 보여주는 반면, console.dir()은 JavaScript 객체처럼 보여줍니다.
  • console.table(data): 배열 또는 객체 배열을 테이블 형태로 보기 좋게 출력합니다. 데이터 구조를 파악하는 데 매우 효과적입니다.
  • console.count(label): 특정 레이블로 호출된 횟수를 기록합니다. 반복문이나 함수 호출 횟수를 확인할 때 유용합니다.
  • console.time(label) / console.timeEnd(label): 특정 작업의 실행 시간을 측정합니다.
  • console.trace(message): 현재 코드 실행 위치까지의 호출 스택(Stack Trace)을 출력합니다.
  • console.group(label) / console.groupEnd(): 콘솔 메시지를 그룹화하여 계층적으로 표시합니다. 복잡한 로깅에 유용합니다.

예시

function processData(data) {
    console.group('데이터 처리 과정'); // 그룹 시작

    console.log('원본 데이터:', data);

    if (!Array.isArray(data)) {
        console.error('입력 데이터는 배열이어야 합니다.', data);
        console.trace('여기서 오류가 발생했습니다.'); // 호출 스택 추적
        console.groupEnd(); // 그룹 종료
        return null;
    }

    const processed = data.map((item, index) => {
        console.count('아이템 처리'); // 각 아이템 처리 횟수 카운트
        return item * 2;
    });

    console.table(processed); // 처리된 데이터를 테이블로 출력

    console.time('총 처리 시간'); // 시간 측정 시작
    // ... 실제 데이터 처리 로직 ...
    for (let i = 0; i < 100000; i++) { /* 복잡한 연산 시뮬레이션 */ }
    console.timeEnd('총 처리 시간'); // 시간 측정 종료

    console.groupEnd(); // 그룹 종료
    return processed;
}

const myNumbers = [1, 2, 3, 4, 5];
processData(myNumbers);
processData("Not an array");

콘솔 API를 적절히 활용하면 console.log의 난잡함을 피하고 훨씬 체계적으로 정보를 얻을 수 있습니다.


브레이크포인트 (Breakpoints)와 스텝 실행

브라우저의 개발자 도구(Elements, Console, Sources 등의 탭)에서 가장 강력한 디버깅 도구는 바로 소스(Sources) 탭입니다. 여기서는 자바스크립트 코드의 실행을 일시 중지시키고, 단계별로 코드를 실행하며, 변수 값을 실시간으로 검사할 수 있습니다.

브레이크포인트 설정

  • 기본 브레이크포인트: 소스 탭에서 디버깅하고 싶은 코드 줄 번호를 클릭하면 브레이크포인트가 설정됩니다. 코드가 해당 줄에 도달하면 실행이 일시 중지됩니다.
  • 조건부 브레이크포인트 (Conditional Breakpoints): 브레이크포인트를 우클릭하고 'Add conditional breakpoint...'를 선택합니다. 특정 조건(예: i === 5 또는 user.id === 123)이 true일 때만 실행이 중지되도록 설정할 수 있습니다. 이는 반복문이나 이벤트 핸들러처럼 여러 번 호출되는 코드에서 특정 상황에서만 멈추고 싶을 때 매우 유용합니다.
  • 로그포인트 (Logpoints): 브레이크포인트를 우클릭하고 'Add logpoint...'를 선택합니다. 코드를 멈추지 않고, 해당 지점에 도달했을 때 특정 메시지나 변수 값을 콘솔에 출력합니다. console.log()를 삽입하는 것과 비슷하지만, 코드를 수정할 필요가 없어서 편리합니다.
  • DOM 변경 브레이크포인트: Elements 탭에서 특정 DOM 요소를 우클릭하고 'Break on' -> 'Subtree modifications', 'Attributes modifications', 'Node removal' 중 하나를 선택하면, 해당 요소의 DOM 구조, 속성, 삭제 등의 변경이 발생할 때마다 실행이 중지됩니다.
  • 이벤트 리스너 브레이크포인트: Sources 탭의 'Event Listener Breakpoints' 패널에서 특정 이벤트(예: 'Click', 'Mouseover')를 선택하면, 해당 이벤트가 발생할 때마다 실행이 중지됩니다. 이는 어떤 이벤트 리스너가 호출되는지 파악할 때 유용합니다.
  • XHR/Fetch 브레이크포인트: 'XHR/Fetch Breakpoints' 패널에서 특정 URL 또는 모든 XHR/Fetch 요청에 대해 브레이크포인트를 설정하여 네트워크 요청이 발생할 때 실행을 중지시킬 수 있습니다.

스텝 실행 제어

코드가 브레이크포인트에서 멈추면, 다음과 같은 스텝 실행 버튼을 사용하여 코드 실행을 제어할 수 있습니다.

  • Resume script execution (F8): 다음 브레이크포인트까지 또는 스크립트가 끝날 때까지 실행을 계속합니다.
  • Step over next function call (F10): 현재 줄을 실행하고 다음 줄로 이동합니다. 함수 호출이 있을 경우 함수 내부로 들어가지 않고 함수 전체를 실행합니다.
  • Step into next function call (F11): 현재 줄을 실행하고 다음 줄로 이동합니다. 함수 호출이 있을 경우 함수 내부로 들어가서 실행합니다.
  • Step out of current function (Shift + F11): 현재 함수 실행을 완료하고, 현재 함수를 호출했던 곳으로 돌아갑니다.
  • Step (F9): 다음 실행 가능한 코드로 이동합니다.

스코프 (Scope) 및 워치 (Watch) 패널

코드가 일시 중지된 상태에서 'Scope' 패널은 현재 실행 컨텍스트(스코프)의 모든 변수(로컬, 클로저, 전역)의 값을 보여줍니다. 'Watch' 패널은 특정 변수나 표현식의 값을 지속적으로 모니터링할 수 있도록 해줍니다. 디버깅 중 중요하게 봐야 할 변수들을 추가해두면 편리합니다.


네트워크 (Network) 탭 활용

웹 애플리케이션의 많은 문제는 서버와의 통신에서 발생합니다. Network 탭은 모든 HTTP 요청 및 응답에 대한 상세 정보를 제공하여 이러한 문제를 진단하는 데 필수적입니다.

  • 요청/응답 확인: 특정 요청을 클릭하면 헤더(Headers), 미리보기(Preview), 응답(Response), 타이밍(Timing) 등 상세 정보를 확인할 수 있습니다.
  • 상태 코드 확인: HTTP 상태 코드(200 OK, 404 Not Found, 500 Internal Server Error 등)를 통해 요청의 성공/실패 여부를 빠르게 파악할 수 있습니다.
  • 페이로드(Payload) 확인: POST/PUT 요청 시 전송된 데이터(Request Payload)를 확인하여, 클라이언트가 서버로 올바른 데이터를 보내는지 검사할 수 있습니다.
  • 타이밍 분석: 요청이 DNS 조회, 연결, 응답까지 얼마나 걸렸는지 시각적으로 보여주어 성능 병목 지점을 파악할 수 있습니다.
  • 필터링 및 검색: 특정 유형(XHR, JS, CSS, Img 등)의 요청만 보거나, 특정 URL 또는 내용으로 검색할 수 있습니다.
  • 느린 네트워크 시뮬레이션: 'Throttling' 드롭다운을 사용하여 3G, 4G, 오프라인 등 다양한 네트워크 환경을 시뮬레이션하여 실제 사용자 환경에서의 동작을 테스트할 수 있습니다.

성능 탭 및 메모리 탭

복잡한 웹 애플리케이션에서 성능 저하나 메모리 누수는 사용자 경험에 치명적입니다.

  • 성능 (Performance) 탭: 웹 페이지의 런타임 성능을 분석합니다. CPU 사용량, 네트워크 활동, 레이아웃 계산, 페인팅 등 모든 활동을 기록하고 시각화하여 어떤 작업이 웹 페이지를 느리게 만드는지 파악할 수 있습니다.

    • 병목 지점 찾기: 특정 시간 동안의 활동을 기록하고, 프레임 드롭이나 긴 작업 시간을 유발하는 JavaScript 함수 호출, 렌더링 작업 등을 찾아냅니다.
    • 렌더링 이슈 진단: Layout, Recalculate Style, Paint 등의 이벤트를 통해 렌더링 파이프라인의 비효율성을 식별합니다.
  • 메모리 (Memory) 탭: 웹 페이지의 메모리 사용량을 분석하여 메모리 누수(Memory Leaks)나 비효율적인 메모리 사용을 진단합니다.

    • 힙 스냅샷(Heap snapshot): 특정 시점의 자바스크립트 힙(Heap) 메모리 사용 현황을 기록하여 어떤 객체가 얼마나 많은 메모리를 차지하고 있는지, 그리고 가비지 컬렉션(GC) 이후에도 남아있는 객체(누수)가 있는지 분석합니다.
    • 할당 타임라인(Allocation timeline): 시간에 따른 메모리 할당 및 해제 패턴을 기록하여 메모리 누수가 발생하는 지점을 시각적으로 파악할 수 있습니다.

기타 디버깅 팁

  • Source Map: 압축되고 난독화된 프로덕션 코드에서 원본 개발 코드를 디버깅할 수 있도록 도와줍니다. 빌드 도구(Webpack, Rollup 등)가 생성합니다.
  • 코드 스니펫(Snippets): Sources 탭에서 직접 JavaScript 코드를 작성하고 실행할 수 있습니다. 빠르게 함수를 테스트하거나 특정 변수 값을 변경하여 실험해볼 때 유용합니다.
  • debugger 키워드: 코드에 debugger;를 삽입하면, 브라우저 개발자 도구가 열려 있을 때 해당 지점에서 자동으로 브레이크포인트가 걸립니다. 임시적인 디버깅에 편리합니다.
    function calculateTotal(items) {
        // debugger; // 여기에 브레이크포인트가 걸림
        let total = 0;
        for (const item of items) {
            total += item.price * item.quantity;
        }
        return total;
    }
  • 원격 디버깅 (Mobile Device): USB 케이블을 통해 실제 안드로이드 기기를 연결하거나, iOS 시뮬레이터를 사용하여 데스크톱의 개발자 도구로 모바일 웹 애플리케이션을 디버깅할 수 있습니다. 이는 모바일 환경 특유의 문제(터치 이벤트, 성능 등)를 해결하는 데 필수적입니다.

마무리하며

이번 장에서는 웹 개발 과정에서 발생하는 복잡한 문제들을 효율적으로 진단하고 해결하기 위한 고급 디버깅 기법들을 심층적으로 학습했습니다.

여러분은 단순히 console.log()를 넘어 console.warn(), console.error(), console.table(), console.time() 등 다양한 콘솔 API를 활용하는 방법을 익혔습니다. 또한, 브라우저 개발자 도구의 Sources 탭에서 브레이크포인트(조건부, 로그포인트, DOM/이벤트/XHR 브레이크포인트 포함) 를 설정하고, 스텝 실행 제어 버튼을 사용하여 코드의 실행 흐름을 정밀하게 추적하는 방법을 배웠습니다. ScopeWatch 패널을 통해 변수 값을 실시간으로 모니터링하는 능력도 향상시켰습니다.

나아가 Network 탭을 활용하여 서버 통신 문제를 진단하고, PerformanceMemory 탭을 통해 애플리케이션의 성능 병목과 메모리 누수를 찾아내는 방법까지 살펴보았습니다. debugger 키워드, Source Map, 원격 디버깅과 같은 추가적인 팁들도 여러분의 디버깅 역량을 강화하는 데 큰 도움이 될 것입니다.

디버깅은 단순히 버그를 찾는 기술이 아니라, 코드의 작동 방식을 깊이 이해하고 문제 해결 능력을 키우는 중요한 학습 과정입니다. 이 장에서 배운 다양한 도구와 기법들을 꾸준히 연습하여, 어떤 복잡한 문제에 직면하더라도 침착하게 원인을 분석하고 해결할 수 있는 능력을 갖춘 숙련된 개발자로 성장하시길 바랍니다.