고급 디버깅 기법
콘솔 API·중단점·단계 실행·Watch·Network·Memory 패널로 실행 흐름과 데이터·요청·누수 문제를 추적합니다.
앞선 절들에서는 단위/통합/E2E 테스트와 크로스 브라우저 테스팅으로 품질을 높이는 방법을 학습했습니다.
테스트는 많은 버그를 사전에 걸러주지만, 모든 시나리오와 환경을 완벽히 커버할 수는 없습니다.
예측하기 어려운 조건이나 특정 사용자 환경에서만 드러나는 버그는 여전히 발생할 수 있습니다.
이러한 상황에서 개발자는 문제의 원인을 파악하고 해결하는 능력이 필요합니다.
바로 디버깅(Debugging)입니다.
디버깅은 코드의 실행 흐름을 추적하고, 변수의 값을 검사하며, 오류가 발생하는 지점을 정확히 찾아내어 수정하는 과정입니다.
이번 장에서는 console.log()를 넘어서, 브라우저 개발자 도구의 고급 디버깅 기능으로 복잡한 문제를 효율적으로 진단하고 해결하는 방법을 정리합니다.
EVIDENCE MAP
증상에 맞는 개발자 도구 증거를 고른다
문제를 같은 URL·입력·환경에서 재현한 뒤 아래 패널을 조합해 가설을 확인한다. 한 증상이 여러 원인 경계에 걸칠 수 있다.
| 증상 | 패널 | 확인할 증거 |
|---|---|---|
| 요청·응답 이상 | Network | URL·status·headers·응답 본문·waterfall을 같은 캐시 조건에서 비교한다. |
| 예외·상태 이상 | Sources + Console | 중단점의 Scope·Watch·호출 스택으로 기대값과 실제값을 비교한다. |
| DOM·스타일 이상 | Elements | 계산된 스타일·box model·DOM 변경 중단점으로 화면과 코드의 관계를 확인한다. |
| 느린 로딩·반응 | Performance | 같은 상호작용을 기록하고 main thread·long task·layout·paint 비용을 분리한다. |
| 반복 후 메모리 증가 | Memory | 반복 전후 heap snapshot과 보유 경로를 비교한다. 살아 있는 객체가 모두 누수인 것은 아니다. |
요청·응답 이상 · Network
URL·status·headers·응답 본문·waterfall을 같은 캐시 조건에서 비교한다.
예외·상태 이상 · Sources + Console
중단점의 Scope·Watch·호출 스택으로 기대값과 실제값을 비교한다.
DOM·스타일 이상 · Elements
계산된 스타일·box model·DOM 변경 중단점으로 화면과 코드의 관계를 확인한다.
느린 로딩·반응 · Performance
같은 상호작용을 기록하고 main thread·long task·layout·paint 비용을 분리한다.
반복 후 메모리 증가 · Memory
반복 전후 heap snapshot과 보유 경로를 비교한다. 살아 있는 객체가 모두 누수인 것은 아니다.
특정 기기에서만 나타나면 해당 기기·브라우저 버전에서 원격 디버깅으로 같은 증거를 수집한다. 사용할 수 있는 패널은 브라우저와 연결 환경에 따라 다르다.
가설 하나 수정 → 같은 조건으로 재검증
증거를 바탕으로 원인을 좁히고 최소 변경을 적용한다. 같은 조건에서 전후 결과를 비교하고 주변 동작의 회귀도 확인한다.
console.log()를 넘어서는 콘솔 API
console.log()는 가장 흔하고 직관적인 디버깅 도구이지만, 콘솔 객체는 훨씬 더 많은 유용한 메서드를 제공합니다.
console.warn(message): 경고 수준의 메시지를 출력합니다. 아이콘·색상은 브라우저와 테마에 따라 다르며, 로그 수준 필터에서 숨겨질 수도 있습니다.console.error(message): 오류 수준의 메시지를 출력합니다. 예외를 던지거나 실행을 중단하는 메서드는 아닙니다. 호출 스택을 명시적으로 확인하려면console.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보다 구조적으로 정보를 얻을 수 있습니다. 위 총 처리 시간 타이머는 map 이후의 반복문만 감싸므로 processData 전체 실행 시간을 측정한 것은 아닙니다.
이 예제는 숫자로 이루어진 배열을 가정합니다. 첫 호출의 반환값은 [2, 4, 6, 8, 10], 두 번째 호출은 null입니다. Array.isArray()는 원소가 유한한 숫자인지까지 검사하지 않습니다. 또 console.count('아이템 처리')의 카운터는 호출이 끝나도 유지되므로 같은 예제를 다시 실행하면 6부터 이어집니다. 독립된 실행 횟수를 비교할 때는 먼저 console.countReset('아이템 처리')로 초기화합니다. 콘솔 메서드의 역할은 Chrome Console API 문서에서 확인할 수 있습니다.
브레이크포인트 (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, Attribute 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 패널은 특정 변수나 표현식의 값을 지속적으로 모니터링할 수 있도록 해줍니다.
디버깅 중 중요하게 봐야 할 변수들을 추가해두면 편리합니다.
Watch에는 item.quantity처럼 값을 읽는 표현식부터 넣습니다. 상태를 바꾸는 함수 호출을 넣으면 관찰 자체가 실행 결과를 바꿀 수 있습니다. 중단점 종류와 실행 직전 중단 규칙은 Chrome 중단점 문서를 참고합니다.
네트워크 (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, 오프라인 등 다양한 네트워크 환경을 시뮬레이션하여 실제 사용자 환경에서의 동작을 테스트할 수 있습니다.
HTTP 오류와 통신 실패도 나누어 확인합니다. fetch()는 서버가 404나 500 응답을 보냈다는 이유만으로 거부되지 않습니다. 응답을 받은 경우에는 response.ok·response.status·본문을 확인하고, 연결 실패나 요청 취소로 거부된 경우에는 catch의 오류를 확인합니다. 따라서 “catch가 실행되지 않았으니 성공했다”는 판정은 맞지 않습니다. 이 구분은 MDN Fetch 사용 문서에 설명되어 있습니다.
성능 탭 및 메모리 탭
복잡한 웹 애플리케이션에서 성능 저하나 메모리 누수는 사용자 경험에 치명적입니다.
-
성능 (Performance) 탭: 웹 페이지의 런타임 성능을 분석합니다. CPU 사용량, 네트워크 활동, 레이아웃 계산, 페인팅 등 모든 활동을 기록하고 시각화하여 어떤 작업이 웹 페이지를 느리게 만드는지 파악할 수 있습니다.
- 병목 지점 찾기: 특정 시간 동안의 활동을 기록하고, 프레임 드롭이나 긴 작업 시간을 유발하는 JavaScript 함수 호출, 렌더링 작업 등을 찾아냅니다.
- 렌더링 이슈 진단:
Layout,Recalculate Style,Paint등의 이벤트를 통해 렌더링 파이프라인의 비효율성을 식별합니다.
-
메모리 (Memory) 탭: 웹 페이지의 메모리 사용량을 분석하여 메모리 누수(Memory Leaks)나 비효율적인 메모리 사용을 진단합니다.
- 힙 스냅샷(Heap snapshot): 특정 시점의 자바스크립트 힙(Heap) 메모리 사용 현황을 기록하여 어떤 객체가 얼마나 많은 메모리를 차지하고 있는지, 그리고 가비지 컬렉션(GC) 이후에도 남는 객체와 그 보유 경로를 분석합니다. 살아 있는 객체가 모두 누수인 것은 아니며, 더 이상 필요하지 않은 객체가 참조 때문에 남는지 확인합니다.
- 할당 타임라인(Allocation timeline): 시간에 따른 메모리 할당 및 해제 패턴을 기록하여 메모리 누수가 발생하는 지점을 시각적으로 파악할 수 있습니다.
예를 들어 모달을 열었다가 닫는 동작에서 누수를 의심한다면, 안정된 초기 상태의 스냅샷 A를 만든 뒤 같은 열기·닫기를 10회 반복하고 스냅샷 B를 만듭니다. 다시 10회 반복한 C와 비교하여 제거한 DOM 객체가 계속 증가하는지, 어떤 이벤트 리스너나 컬렉션이 참조를 유지하는지 확인합니다. 정상적으로 유지되는 캐시도 있으므로 총 메모리 증가만으로 누수라고 결론 내리지 않습니다. 객체의 보유 경로를 보는 방법은 Chrome Heap snapshot 문서를 참고합니다.
기타 디버깅 팁
- 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): Android의 Chrome은 USB 디버깅을 허용한 기기를 데스크톱 Chrome에 연결하여 검사할 수 있습니다(Chrome 안내). iPhone·iPad와 시뮬레이터의 Safari 검사는 Mac의 Safari Web Inspector 환경을 사용합니다(Apple 안내, WebKit 연결 절차). 데스크톱의 화면 크기 에뮬레이션만으로 실제 모바일 브라우저의 성능·입력 문제를 모두 재현할 수는 없습니다.
PROCESS · B99 WEB FUNDAMENTALS
가설 기반 디버깅 검증 루프
같은 증상을 재현하고 반증 가능한 예측을 한 문장으로 세운다. 중단점에서 기대값과 실제값을 측정한 뒤 변수 하나만 바꾸고, 예측이 맞지 않으면 가설로 되돌아간다.
계산 오류를 입력 경계까지 추적하기
앞의 calculateTotal()을 그대로 사용하되 다음 입력을 넣어 봅니다. price와 quantity는 숫자여야 한다는 조건이며, 금액 단위는 원입니다. 이 함수와 입력은 브라우저 Console 또는 Node.js에서 실행할 수 있습니다.
const responseItems = [
{ price: 1000, quantity: 2 },
{ price: 500, qty: 3 }, // API 응답이 예상한 필드명과 다름
];
console.log(calculateTotal(responseItems)); // NaN“두 번째 항목의 quantity가 없어서 합계가 NaN이 된다”는 가설을 세우고, total += item.price * item.quantity;에 조건부 중단점 item.quantity === undefined를 설정합니다. 이 줄을 실행하기 전에 멈췄다면 total은 첫 항목의 합인 2000, item.price는 500, item.quantity는 undefined여야 합니다. 한 번 Step over 하면 500 * undefined가 NaN이 되어 total도 NaN이 됩니다.
Network의 응답 본문에서 실제 필드가 qty인지 확인한 다음, 입력을 만드는 곳에서 계약을 맞춥니다. 이 예제의 계약이 quantity라면 두 번째 항목을 { price: 500, quantity: 3 }으로 고쳤을 때 합계는 3500입니다. 합계가 NaN일 때 무조건 0으로 바꾸면 잘못된 입력을 숨길 뿐입니다. 정상 입력·빈 배열·문제가 있던 입력을 다시 실행하여 결과와 실패 처리 방침을 함께 확인합니다.
직접 확인할 문제: [{ price: 1200, quantity: 2 }, { price: 300, quantity: 4 }]에서 두 번째 반복의 합계 대입 줄에 멈췄습니다. 실행 전 total, 해당 줄을 한 번 실행한 뒤 total, 최종 반환값은 각각 얼마인가요? console.error('잘못된 수량')만 호출하면 계산이 자동으로 중단될까요?
풀이 확인
두 번째 반복 직전에는 1200 * 2 = 2400이 들어 있습니다. 한 줄 실행 후에는 2400 + 300 * 4 = 3600이고, 반복이 끝나면 3600을 반환합니다. console.error()는 메시지만 기록하므로 계산을 멈추려면 조건에 맞게 return하거나 예외를 던지는 제어 흐름이 별도로 필요합니다. 빈 배열의 반환값은 초기 total인 0입니다.
고급 디버깅 기법 정리
이번 장에서는 웹 개발 과정에서 발생하는 복잡한 문제들을 효율적으로 진단하고 해결하기 위한 고급 디버깅 기법들을 심층적으로 학습했습니다.
이번 절에서는 console API, 브레이크포인트, 스텝 실행, Scope 및 Watch 패널을 이용해 실행 흐름과 변수 값을 추적하는 방법을 정리했습니다.
Network, Performance, Memory 탭은 서버 통신, 렌더링 병목, 메모리 증가 원인을 분리해서 확인할 때 사용합니다.
debugger, Source Map, 원격 디버깅은 재현 환경이 다른 문제를 좁히는 보조 수단입니다.
디버깅은 단순히 버그를 찾는 기술이 아니라, 코드의 작동 방식을 깊이 이해하고 문제 해결 능력을 키우는 중요한 학습 과정입니다.
이 장에서 배운 다양한 도구와 기법들을 꾸준히 연습하여, 어떤 복잡한 문제에 직면하더라도 침착하게 원인을 분석하고 해결하는 역량을 체계적으로 강화할 수 있습니다.
고급 디버깅은 도구를 많이 쓰는 것이 아니라 같은 문제가 다시 나타나는 조건을 짧게 재현하고 가설을 검증하는 일입니다.