브라우저에서 사용하는 디버깅 기술
(alan.norbauer.com)- 브라우저 개발자 도구를 중급 이상으로 다룰 수 있다면 조건부 중단점을 단순 정지 조건이 아니라 로그, 추적, 상태 변경, 시간 측정 도구로 확장할 수 있음
console.log,console.count,console.trace,console.time같은 콘솔 API를 중단점이나 Watch Pane에 넣으면 실행을 멈추지 않고 흐름과 상태를 기록할 수 있음- 함수 인자 수, 시간, CSS 계산값, 실행 횟수, 무작위 샘플링, 전역 불리언을 조합하면 멈추는 시점을 훨씬 정교하게 제어 가능함
- Chrome의
monitor,debug,copy,$0,getEventListeners,monitorEvents는 함수 호출, DOM, 이벤트, 클립보드 작업을 콘솔에서 빠르게 다루게 해줌 - DOM이 JS나 사용자 이벤트로 계속 바뀌는 화면에서는 JS 일시정지,
setTimeout(debugger), DOM 스냅샷, 포커스 감시가 HTML/CSS 디버깅에 특히 유용함
조건부 중단점을 디버깅 도구로 확장하기
-
조건부 중단점에는 참/거짓 조건뿐 아니라 부수 효과가 있는 JavaScript 표현식도 넣을 수 있음
-
이를 활용하면 기본 중단점을 로그, 추적, 상태 변경, 시간 측정 도구처럼 사용할 수 있음
-
로그포인트와 트레이스포인트
- 조건부 중단점에서
console.log를 호출하면 실행을 멈추지 않고 콘솔에 값을 남기는 로그포인트처럼 동작함 console.count를 쓰면 해당 라인이 몇 번 실행됐는지도 함께 셀 수 있음- 2020년 5월 업데이트 기준 주요 브라우저는 로그포인트/트레이스포인트를 직접 지원함
- 조건부 중단점에서
-
Watch Pane에서 콘솔 API 사용
- Watch Pane에도
console.log같은 표현식을 넣을 수 있음 - 애플리케이션이 디버거에서 멈출 때마다
console.table(localStorage)로 localStorage 스냅샷을 찍을 수 있음 - DOM 변경 중단점과 Watch 표현식을 함께 쓰면 DOM 서브트리 변경 후 스냅샷을 배열에 저장 가능함
(window.doms = window.doms || []).push(document.documentElement.outerHTML)- DOM 변경 중단점은 실행을 멈추지 않도록 만들 수 없음
- Watch Pane에도
-
호출 스택 추적
- 로딩 스피너를 표시하는 함수와 숨기는 함수가 있고 표시 호출만 남는 문제를 찾을 때
console.trace를 조건부 중단점에 넣을 수 있음 - 표시 함수의 마지막 스택 트레이스에서 호출자를 클릭하면 해당 호출 위치로 이동 가능함
- 로딩 스피너를 표시하는 함수와 숨기는 함수가 있고 표시 호출만 남는 문제를 찾을 때
-
실행 중 프로그램 동작 바꾸기
- 조건부 중단점 표현식에서 함수 인자를 덮어쓰면 브라우저 안에서 프로그램 동작을 즉석으로 바꿀 수 있음
- 예를 들어
getPerson함수의id값을 바꿀 수 있음 id=1처럼 참으로 평가되는 표현식은 디버거를 멈추므로, 멈추지 않으려면, false를 덧붙일 수 있음
-
빠른 성능 측정
- 조건부 중단점 평가 시간이 섞이므로 정밀한 성능 프로파일링에는 적합하지 않음
- 빠른 측정이 필요할 때는 시작 지점 조건에
console.time('label'), 끝 지점 조건에console.timeEnd('label')을 넣어 실행 시간을 콘솔에 남길 수 있음
중단 조건을 더 정교하게 만들기
-
함수 인자 수 활용
- 현재 함수가 3개 인자로 호출될 때만 멈추려면 다음 조건을 사용할 수 있음
arguments.callee.length === 3- 선택 인자가 있는 오버로드 함수에서 유용함
- 정의된 인자 수와 실제 전달 인자 수가 다를 때만 멈추려면 다음 조건을 사용할 수 있음
(arguments.callee.length) != arguments.length- 함수 호출 위치의 버그를 찾는 데 도움이 됨
-
시간 기반 조건
- 페이지 로드 후 5초가 지나기 전에는 멈추지 않으려면 다음 조건을 사용할 수 있음
performance.now() > 5000- 초기 페이지 로드 이후에만 중단점에 관심이 있을 때 적합함
- 중단점이 설정된 뒤 다음 5초 동안은 멈추지 않고 이후에는 멈추게 하려면 기준 시간을 전역 변수에 저장할 수 있음
window.baseline = window.baseline || Date.now(), (Date.now() - window.baseline) > 5000- 콘솔에서
window.baseline = Date.now()로 카운터를 다시 시작할 수 있음
-
CSS 상태 기반 조건
- 계산된 CSS 값을 조건으로 삼아 멈출 수 있음
- 예를 들어
document.body의 배경색이 빨간색일 때만 멈추려면 다음 조건을 사용할 수 있음
window.getComputedStyle(document.body).backgroundColor === "rgb(255,0,0)" -
실행 횟수와 샘플링
- 라인이 실행될 때마다 카운터를 증가시켜 두 번에 한 번만 멈출 수 있음
window.counter = (window.counter || 0) + 1, window.counter % 2 === 0- 무작위 샘플링으로 10번 중 1번만 멈추려면 다음 조건을 사용할 수 있음
Math.random() < 0.1 -
특정 위치에서는 절대 멈추지 않기
- Chrome에서 gutter를 우클릭하고 “Never Pause Here”를 선택하면 항상
false인 조건부 중단점이 만들어져 해당 라인에서 멈추지 않음 - XHR 중단점에서 특정 라인을 제외하거나 특정 예외를 무시할 때 활용 가능함
- Chrome에서 gutter를 우클릭하고 “Never Pause Here”를 선택하면 항상
-
인스턴스 ID 자동 부여
- 클래스 생성자에 조건부 중단점을 넣어 각 인스턴스를 배열에 저장할 수 있음
(window.instances = window.instances || []).push(this)- 특정 인스턴스의 고유 ID는 배열 인덱스로 확인 가능함
window.instances.indexOf(instance) -
중단점 전역 토글
- 하나 이상의 조건부 중단점을 전역 불리언으로 제어할 수 있음
- 콘솔에서 직접 바꾸거나, 다른 중단점에서 바꾸거나, 타이머로 바꾸는 방식이 가능함
window.enableBreakpoints = true setTimeout(() => (window.enableBreakpoints = true), 5000)
함수와 클래스 호출 추적
-
클래스 메서드 호출 감시
- Chrome의
monitor명령줄 메서드를 사용하면 클래스 메서드 호출을 쉽게 추적할 수 있음 Dog클래스의 모든 인스턴스에 대한 메서드 호출을 보고 싶다면 prototype의 모든 프로퍼티에monitor를 적용할 수 있음
var p = Dog.prototype; Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k]));- 콘솔에는
function bark called with arguments: 2같은 호출 정보가 출력됨 - 로그만 남기는 대신 호출 시 실행을 멈추고 싶다면
monitor대신debug를 사용할 수 있음
- Chrome의
-
특정 인스턴스에서 클래스 추적
- 클래스 이름을 모르지만 인스턴스가 있다면 생성자의 prototype을 통해 같은 방식으로 감시할 수 있음
var p = instance.constructor.prototype; Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k]));- 특정 클래스에 고정하지 않고 임의 인스턴스에 대해 동작하는 함수를 만들 때 유용함
-
콘솔에서 함수 호출 직전 디버깅
- 디버깅하려는 함수를 콘솔에서 호출하기 전에
debugger를 실행하고, 다음 함수 호출로 Step into 하면 함수 구현으로 들어갈 수 있음
debugger; fn(1);- 함수 정의 위치를 찾기 귀찮거나
fn이 동적으로 바인딩되어 소스 위치를 모를 때 유용함 - Chrome에서는
debug(fn)을 호출해fn이 호출될 때마다 함수 내부에서 멈추게 할 수도 있음
- 디버깅하려는 함수를 콘솔에서 호출하기 전에
URL 변경과 속성 읽기 디버깅
-
SPA 라우팅 전 멈추기
- 단일 페이지 애플리케이션이 URL을 바꾸기 직전에 멈추려면
history.pushState,history.replaceState,window.onhashchange,window.onpopstate에debugger를 넣은 함수를 연결할 수 있음
const dbg = () => { debugger; }; history.pushState = dbg; history.replaceState = dbg; window.onhashchange = dbg; window.onpopstate = dbg;- 이 버전의
dbg는 내비게이션을 깨지 않으면서 멈추는 구현까지 포함하지 않음 window.location.replace나window.location.assign을 직접 호출하는 경우에는 할당 직후 페이지가 언로드되므로 이 방식으로 처리하지 못함- Chrome에서는 관련 메서드에
debug를 걸어 리다이렉트 소스와 해당 시점의 상태를 볼 수 있음
debug(window.location.replace); debug(window.location.assign); - 단일 페이지 애플리케이션이 URL을 바꾸기 직전에 멈추려면
-
속성 읽기 지점 찾기
- 객체의 특정 프로퍼티가 읽히는 순간을 알고 싶다면 getter 안에
debugger를 넣을 수 있음
{ get configOption() { debugger; return true; } }- 설정 옵션을 어떤 코드가 어떻게 사용하는지 확인할 때 유용함
- 원본 소스에서 바꾸거나 조건부 중단점을 이용해 적용할 수 있음
- 객체의 특정 프로퍼티가 읽히는 순간을 알고 싶다면 getter 안에
콘솔에서 데이터 복사하기
copy()콘솔 API를 사용하면 문자열 잘림 없이 브라우저의 정보를 클립보드로 복사할 수 있음- 현재 DOM 스냅샷, 이미지 같은 리소스 메타데이터, 큰 JSON blob, localStorage 덤프를 복사할 때 사용할 수 있음
copy(document.documentElement.outerHTML)
copy(performance.getEntriesByType("resource"))
copy(JSON.parse(blob))
copy(localStorage)
HTML/CSS 디버깅
-
JS 실행을 멈춘 상태에서 DOM 검사
- DOM inspector에서 Chrome/Windows 기준
ctrl+\를 누르면 언제든지 JS 실행을 멈출 수 있음 - JS가 DOM을 바꾸거나 mouseover 같은 이벤트가 DOM을 바꾸는 상황에서도 현재 DOM 스냅샷을 안정적으로 검사 가능함
- DOM inspector에서 Chrome/Windows 기준
-
사라지는 요소 검사
- 마우스를 옮기면 사라지는 조건부 DOM 요소는 콘솔에 5초 타이머를 걸어 디버거를 실행하면 검사할 수 있음
setTimeout(function() { debugger; }, 5000);- 5초 동안 UI를 띄운 뒤 JS 실행이 멈추면 요소가 사라지지 않으므로 개발자 도구로 이동해 검사할 수 있음
- JS 실행이 멈춘 동안에는 요소 검사, CSS 편집, JS 콘솔 명령 실행이 가능함
- 커서 위치나 포커스에 의존하는 DOM을 검사할 때 효과적임
-
DOM 스냅샷 기록
- 현재 DOM을 복사하려면 다음 명령을 사용할 수 있음
copy(document.documentElement.outerHTML);- 1초마다 DOM 문자열을 배열에 저장하려면 다음처럼
setInterval을 사용할 수 있음
doms = []; setInterval(() => { const domStr = document.documentElement.outerHTML; doms.push(domStr); }, 1000);- 배열에 저장하지 않고 콘솔에 계속 출력할 수도 있음
-
포커스된 요소 감시
document.activeElement를 주기적으로 비교해 포커스 변경을 콘솔에 출력할 수 있음
(function () { let last = document.activeElement; setInterval(() => { if (document.activeElement !== last) { last = document.activeElement; console.log("Focus changed to: ", last); } }, 100); })(); -
굵게 표시된 요소 찾기
- 모든 요소에서 계산된
fontWeight가"bold"또는"700"인 요소를 찾을 수 있음
const isBold = (e) => { let w = window.getComputedStyle(e).fontWeight; return w === "bold" || w === "700"; }; Array.from(document.querySelectorAll("*")).filter(isBold);- 현재 inspector에서 선택한 요소의 자손만 찾으려면
$0를 기준으로 검색할 수 있음
Array.from($0.querySelectorAll("*")).filter(isBold); - 모든 요소에서 계산된
-
선택된 요소와 이벤트 다루기
- 콘솔의
$0는 Element Inspector에서 현재 선택된 요소를 가리키는 자동 참조임 - Chrome과 Edge에서는 이전에 검사한 요소를
$1, 그 이전 요소를$2처럼 접근할 수 있음 - Chrome에서는 현재 선택 요소의 이벤트 리스너를 확인할 수 있음
getEventListeners($0)- 선택된 요소의 모든 이벤트를 디버깅하려면
monitorEvents($0)를 사용할 수 있음 - 특정 이벤트 그룹만 보고 싶다면 배열을 넘길 수 있음
monitorEvents($0, ["control", "key"]) - 콘솔의
댓글과 토론
Hacker News 의견들
-
지난 수십 년 동안 브라우저에 내장된 디버깅 도구가 정말 많이 발전했음
JS를 오래 써온 입장에서, 브라우저 안에서 코드 디버깅을 이렇게 직관적으로 만들어 준 사람들에게 깊이 고마움
백엔드나 다른 언어 같은 다른 개발 영역으로 가면, 현대 브라우저가 기본으로 제공하는 이 디버깅 도구 생태계가 그리워짐- 브라우저 개발자 도구를 본격적으로 끌어올린 Firebug 확장을 만든 사람들에게도 박수를 보내고 싶음
- JS 디버깅 경험에서 특히 뭐가 그렇게 특별하다고 느끼는지 궁금함
Python, Java, C++에서 수년간 많이 디버깅했지만 도구가 부족하다고 느낀 적은 없었음
물론 이 분야에서 쓸 수 있는 선택지를 모르는 사람들은 꽤 많이 봤음 - 완벽하진 않지만, 동료 개발자가 밀던 VS Code로 Node.js 디버깅하는 경험보다는 훨씬 나음
그 지옥보다는 콘솔을 택하겠음
-
setTimeout(function() { debugger; }, 5000);는 영리함
결국 Chrome 디버거가 자기 자신을 디버깅하는 재귀 거북이 스택을 이기는 유일한 방법은debugger문뿐임
악명 높은 Myspace Sammy 웜의 sam.pl도 방문자가 난독화된 HTML 홈페이지를 파헤치지 못하게 하려고 디버깅 함정을 썼음- 자주 쓰는 북마클릿 중 하나임: https://darekkay.com/blog/debugging-dynamic-content/
- 웹사이트가 열린 Console/DevTools 사이드 패널을 막을 때 이런 일이 생김
메인 루프를 돌리면서 여기저기에 성가신debugger;문을 삽입하고, 30FPS / 32ms 간격으로 실행해서 DevTools를 쓸모없게 만듦
debugger문을 “무시”할 방법이 없기 때문임 - 5초 기다리고 싶지 않다면 스크립트 실행 일시정지 단축키
cmd-\를 쓰면 됨
현재 실행 중이던 JS 줄에서 페이지가 멈추고, 문제 요소를 평소처럼 검사할 수 있음 - 브라우저 제작자들은 DevTools에서 명시적으로 켜는 옵션으로 두고, 기본값으로는
debugger문을 무시해 줬으면 함
합법적인debugger문 사용 사례 대부분은 자기 코드에서 작업 중이라면 단순 중단점으로 대체 가능함 - 잠깐, 이게 전부라고? 너무 단순한데 너무 좋음
-
queryObjects가 빠져 있는 게 눈에 띔
특정 생성자로 만들어진 모든 객체 목록을 반환하는 꽤 미친 API임
예를 들어queryObjects(Function)으로 힙에 있는 모든 함수 목록을 얻을 수 있음
어떤 모듈 안에 들어 있는 “private” 함수까지 반환함- 솔직히 이건 알리고 싶지 않아도 정당화될 만함
정말 미친 기능이고, 아마 Chromium 전용일 것 같음
- 솔직히 이건 알리고 싶지 않아도 정당화될 만함
-
감시 변수는 도무지 제대로 쓰지 못하겠음
스코프와 갱신 규칙이 너무 알 수 없고, 전역 변수만 감시 가능한가 싶지만 그마저도 기대대로 동작하지 않아서 테스트할 때 결국 로그를 값으로 도배하게 됨
콘솔에 Data.gui [1] 스타일의 UI를 넣어서 변수와 설정 값을 보고 테스트할 수 있게 해야 한다고 몇 년째 생각해 왔음
이 CodePen [2]에서 실제 동작을 볼 수 있음- 같은 좌절감을 느꼈음
브라우저에는 이론상 훌륭한 디버깅 기능이 많지만, 실제로는 안정적으로 동작하는 것 같지 않음
중단점조차 전부 안정적으로 걸리게 만들지 못하겠음
코드가 풀려 있을 때는 괜찮은데, 번들링되기만 하면 최소화하지 않았더라도 많은 디버거 기능이 깨지는 것 같았음
프런트엔드 엔지니어가 아니라서 아마 뭔가를 잘못하고 있을 수도 있음 - 디버거에서 소스 맵을 꺼보는 게 좋음
좌절의 원인일 가능성이 높고, 켜진 상태로 능동적으로 디버깅하는 경험이 이렇게 나쁜데 왜 기본값으로 켜져 있는지 아직도 이해가 안 됨 - Chrome 사이드바 패널에서는 최소화된 변수가 올바른 이름으로 보이는데, watchpoint에서는 정의되지 않았다는 오류가 나서 짜증남
- 같은 좌절감을 느꼈음
-
IIFE 스코프 안에서 코드 실행을 멈추지 않고 IIFE의 지역 변수에 접근하는 방법이 있으면 좋겠음
디버거를 설득해서 그렇게 하게 만드는 방법이 있을까?- IIFE는 즉시 실행 함수 표현식임
https://en.m.wikipedia.org/wiki/Immediately_invoked_function... - IIFE가 호출한 함수에서 중단점에 진입했다면 그냥 스택을 거슬러 올라가면 됨
IIFE 바깥에 완전히 있다면 불가능하다고 봄
찾는 변수는 실행 전후에는 메모리에 존재하지 않을 수도 있음
JavaScript에는 다른 언어처럼 정적 변수가 없어서 IIFE가 호출될 때마다 내부 변수는 버려짐
JavaScript는 웹 워커 같은 것을 쓰지 않는 한 꽤 강한 단일 스레드 모델이라,async/await호출로 직접 경쟁 상태를 만들려는 게 아니라면 IIFE 스코프 밖에 그런 변수가 메모리에 남아 있다는 개념 자체가 성립하기 어려움
var를 남용해서 IIFE 스코프 밖에서 변수를 초기화하면 마지막 IIFE 호출이 만든 값을 볼 수는 있음 - 12년쯤 전에 그런 일을 하는 도구를 만들었음
조금 수정은 필요하지만 들이는 노력 대비 효과가 큼
https://github.com/kristopolous/_inject
임의의 시점에 거의 아무 함수 컨텍스트나 돌아다니며 무슨 일이 있었는지 볼 수 있음
참조 카운터를 악용해서 컨텍스트가 파괴되지 않게 만드는 방식임
클라이언트 측 JS를 많이 하던 때에는 정말 유용했음
이걸 제대로 만든 킬러 앱 버전은 어떤 컨텍스트에서든 REPL을 여는 형태일 것임
지금 상태로도 잘 쓰려면 꽤 숙련도가 필요함 - 찾는 게 로그포인트인가?
https://firefox-source-docs.mozilla.org/devtools-user/debugg...
수정: 링크된 글에서 말 그대로 첫 번째로 다루는 내용이라는 걸 방금 깨달음 - Chrome에서는 https://news.ycombinator.com/item?id=38226743#38231705에서 설명한 클로저를 Watch 패널로 검사한 뒤
[[Scopes]]의사 속성을 보면 됨
Firefox에는 방법이 없는 것 같음
- IIFE는 즉시 실행 함수 표현식임
-
완전성을 위해 Werkzeug를 추천할 수 있음
Django 백엔드 개발에 쓰고 있는데 엄청 유용함
예외가 발생하는 곳이면 언제 어디서든 브라우저 안에서 “PDB” 셸을 쓸 수 있게 해줌- 브라우저에 Werkzeug를 띄우고 싶은 곳에
1/0을 넣기만 하면 되어서 쉬움 - nose.js와 브라우저를 포함해 어디서나 그런 경험을 갖고 싶음
아주 단순하고 직관적임 - 아직도
django-extension/runserver_plus가 필요한가?
- 브라우저에 Werkzeug를 띄우고 싶은 곳에
-
항상 쓰는 요령 하나는 UI 문자열로 로드된 스크립트를 검색해서 디버깅하는 것임
Network 패널로 가서 네트워크 요청 기록을 시작하고, 왼쪽 사이드바를 연 뒤 찾고 싶은 코드나 UI 문자열을 검색함
보통 이상한 번들 JS 청크 파일 안에서 발견되고, 결과를 클릭하면 해당 파일의 네트워크 요청이 열림
그 파일 안 아무 곳에서 우클릭해 “Open in Sources” 비슷한 항목을 고르면 디버거로 이동함
이제debugger문을 놓으면 되고, 이 과정에서 소스 맵도 아마 로드됨 -
Python/Elixir 프로그래머로 오래 일하면서
pdb.set_trace()와IEx.pry()를 많이 써왔음
최근 아주 지저분한 NodeJS 백엔드를 물려받았는데, 제대로 된 디버깅 도구 없이 남은 머리카락을 쥐어뜯고 있음
console.log디버깅으로 돌아갔지만 원시인이 된 기분임
이렇게 인기 있는 생태계에 제대로 된 디버깅 REPL이 없다는 게 믿기지 않는데, 어디로 가야 할지 알려줄 수 있을까?- 있음
node --inspect-brk를 쓰면 되고, VS Code나 Chrome DevTools로 연결할 수 있음
까다로운 부분은 TypeScript 변환 같은 빌드 도구가nodejs명령 앞에 있는지 여부임
그냥node를 실행한다면 꽤 쉬움
디버깅 REPL은 JavaScript 콘솔 자체임 - VSCode 개발자 도구를 쓰면 NodeJS에서도 꽤 탄탄하게 디버깅할 수 있음
디버깅 옵션으로 추가하면 중단점을 찍고 함수 안으로 단계 실행할 수 있음
대부분이 객체 안에 캡슐화되어 있어서 JavaScript는 디버깅하기 꽤 좋음 - 다른 사람이 말했듯이 원하는 건
--inspect또는--inspect-brk임
VSCode에서는 “open JavaScript debug terminal” 명령도 쓸 수 있음
이 명령은 어떤node명령이든 자동으로 디버거가 붙은 상태에서 시작되는 터미널을 열어줌 - 충분하지 않을 수도 있지만, 방금 python snoop을 알게 됨
https://pypi.org/project/snoop/
깊은 추적용 데코레이터임 - Elixir REPL에서 좋은 점은 Erlang REPL과 비슷하다고 보이는데, 실행 중인 프로세스에 연결할 수 있다는 점임
Node JS 생태계에서는 이런 기능을 보지 못했음
- 있음
-
“Debugging Property Reads” 섹션에서 조건부 중단점을 사용해
{configOption: true}를{get configOption() { debugger; return true; }}로 어떻게 바꾸는지 궁금함- 그건 조건부 중단점 제목 아래의 내용이 아님
콘솔에서 값을 getter로 덮어쓰면 되고, 쓰기 권한이 있다면 소스 코드에서 직접 바꿔도 됨
- 그건 조건부 중단점 제목 아래의 내용이 아님
-
이건 꼭 배워야 할 것 같은데, 전용 책이나 학습 자료가 있는지 궁금함
아니면 그냥 웹 개발과 프런트엔드를 끝없이 파고드는 수밖에 없는 건가?- Chrome 팀의 “What’s new in DevTools” 시리즈를 따라가는 걸 정말 추천함
한창 작업 중일 때 릴리스 노트를 읽으려고 링크를 누르는 게 끌리지 않을 수 있지만, 새 버전이 나올 때 5분만 훑어봐도 충분히 가치가 있음
몇 분짜리로 소화하기 쉬운 영상도 있어서 간단한 개요를 얻을 수 있음
목적은 새 기능 소개지만, 경험상 도구의 현재 한계도 자주 이해하게 됨
- Chrome 팀의 “What’s new in DevTools” 시리즈를 따라가는 걸 정말 추천함