
에러 스택 트레이스 읽기
빨간 에러 메시지가 뜨면 당황해서 그냥 구글에 복붙했는데, 스택 트레이스를 읽는 법을 알고 나니 디버깅 속도가 10배 빨라졌다.

빨간 에러 메시지가 뜨면 당황해서 그냥 구글에 복붙했는데, 스택 트레이스를 읽는 법을 알고 나니 디버깅 속도가 10배 빨라졌다.
분명히 클래스를 적었는데 화면은 그대로다? 개발자 도구엔 클래스가 있는데 스타일이 없다? Tailwind 실종 사건 수사 일지.

안드로이드는 Xcode보다 낫다고요? Gradle 지옥에 빠져보면 그 말이 쏙 들어갈 겁니다. minSdkVersion 충돌, Multidex 에러, Namespace 변경(Gradle 8.0), JDK 버전 문제, 그리고 의존성 트리 분석까지 완벽하게 해결해 봅니다.

서버에서 잘 오던 데이터가 갑자기 앱을 죽입니다. 'type Null is not a subtype of type String' 에러의 원인과, 안전한 JSON 파싱을 위한 Null Safety 전략을 정리해봤습니다.

버튼을 눌렀는데 부모 DIV까지 클릭되는 현상. 이벤트는 물방울처럼 위로 올라갑니다(Bubbling). 반대로 내려오는 캡처링(Capturing)도 있죠.

첫 번째 웹 앱을 만들 때였다. 콘솔에 빨간 글씨가 주르륵 뜨면 심장이 쿵 내려앉았다. 뭐가 문제인지도 모른 채 전체 에러 메시지를 복사해서 구글에 붙여넣었다. "아무나 답 좀..." 하는 심정이었다.
Stack Overflow에서 비슷한 에러를 찾으면 다행이었다. 하지만 대부분은 내 상황과 미묘하게 달랐다. 프레임워크 버전이 다르거나, 파일 구조가 다르거나, 아예 다른 맥락이었다. 그래서 답변에 있는 코드를 복붙했다가 더 이상한 에러가 나기도 했다.
그러던 어느 날, 시니어 개발자가 내 화면을 힐끗 보더니 "아, 여기 23번째 줄 보면 되겠네"라고 했다. 나는 50줄짜리 에러 메시지를 읽느라 정신없었는데, 그는 3초 만에 문제를 찾았다. 마술 같았다.
그날 이후 깨달았다. 스택 트레이스는 암호문이 아니라 지도였다. 읽는 법만 알면 에러가 난 위치로 바로 데려다주는 내비게이션이었다.
스택 트레이스는 크게 세 부분으로 나뉜다는 걸 이해하니 모든 게 명확해졌다.
맨 위에 나오는 한 줄이 가장 중요하다. "무슨 일이 벌어졌는가"를 말해준다.
TypeError: Cannot read property 'name' of undefined
이 한 줄에서 두 가지를 알 수 있다:
undefined.name을 읽으려고 했다는 뜻처음엔 이게 무슨 말인지 몰랐다. 근데 몇 번 보다 보니 패턴이 보였다. TypeError는 대부분 "없는 걸 있다고 착각했을 때" 나타난다. ReferenceError는 "변수 이름을 잘못 쓴 것"이고, SyntaxError는 "문법이 틀린 것"이다.
에러 메시지 아래에 at ...으로 시작하는 줄들이 주르륵 나온다. 이게 콜 스택이다.
at getUserName (app.js:23:15)
at renderProfile (components.js:45:8)
at App (index.js:12:3)
중요한 건 위에서 아래로 읽는다는 것이다. 맨 위가 가장 최근에 실행된 함수다.
처음엔 반대로 생각했다. "실행 순서니까 위에서 아래겠지?" 근데 아니다. 스택은 쌓인 순서의 역순이다. 책을 쌓듯이 아래부터 쌓였지만, 에러는 맨 위에서 터진다.
위 예시로 보면:
App 함수가 실행됐고renderProfile을 호출했고renderProfile 안에서 getUserName을 호출했는데getUserName에서 에러가 터졌다
그래서 답은 app.js의 23번째 줄에 있다. 간단하다.
(app.js:23:15) 이 부분이 GPS 좌표다.
대부분 IDE는 이 형식을 인식해서 클릭하면 바로 해당 위치로 이동해준다. VSCode에서는 콘솔의 파일 경로를 Cmd+클릭하면 된다. 이거 알고 나서 디버깅 속도가 3배는 빨라졌다.
처음 React를 배울 때 콘솔에 이런 게 떴다:
at renderWithHooks (react-dom.development.js:14985:18)
at mountIndeterminateComponent (react-dom.development.js:17811:13)
at beginWork (react-dom.development.js:19049:16)
at performUnitOfWork (react-dom.development.js:23864:12)
at workLoopSync (react-dom.development.js:23793:5)
at renderRootSync (react-dom.development.js:23752:7)
at MyComponent (App.js:34:10)
"와, React 내부가 망가진 건가?" 싶었다. 근데 아니었다. 진짜 문제는 App.js:34였다.
스택 트레이스에는 내 코드와 남의 코드(프레임워크/라이브러리)가 섞여 있다. 디버깅할 때는 내 코드 부분만 집중하면 된다.
구분하는 법:
node_modules/... 경로: 라이브러리 코드 → 일단 무시react-dom.js, vue.js 같은 파일: 프레임워크 내부 → 무시src/..., app.js, components/...: 내 코드 → 여기 집중이걸 강물에 비유하면 이해하기 쉽다. 상류(프레임워크)에서 물이 흘러왔지만, 내 코드(하류)에서 막혔다면 하류를 먼저 뜯어봐야 한다. 상류는 수백만 명이 쓰는 검증된 코드다. 내가 잘못 쓴 게 99%다.
몇 달 코딩하면서 에러 타입마다 "성격"이 있다는 걸 깨달았다.
const user = null;
console.log(user.name); // TypeError: Cannot read property 'name' of null
"있을 줄 알았는데 없었다." API에서 데이터를 못 받았거나, 배열이 비어있거나, props가 안 넘어왔을 때 주로 난다.
console.log(userName); // ReferenceError: userName is not defined
// 변수명 오타였다. 진짜 이름은 username이었음
이건 대부분 오타다. 아니면 import를 깜빡했거나.
const data = { name: "John" // SyntaxError: Unexpected end of input
괄호를 안 닫았거나, 쉼표를 빼먹었거나. 이건 코드가 아예 실행 안 된다.
const arr = new Array(-1); // RangeError: Invalid array length
숫자가 말이 안 될 때. 재귀 함수가 무한 루프 돌 때도 이게 난다.
프로덕션 배포하면 코드가 압축(minify)된다. 그럼 스택 트레이스가 이렇게 나온다:
at r.a (bundle.min.js:1:2847)
"r.a가 뭐야?" 이게 원래 내 getUserName 함수였는데, 압축 과정에서 이름이 바뀐 거다.
이럴 때 source map 파일(.map 확장자)이 있으면 브라우저가 자동으로 원본 코드 위치를 알려준다. Vite, Webpack 같은 번들러는 기본으로 source map을 생성한다.
개발자 도구 설정에서 "Enable source maps"를 켜면 압축된 코드에서도 원본 파일명과 줄 번호를 볼 수 있다. 이거 모르고 한참 헤맸다.
React는 일반 스택 트레이스 외에 컴포넌트 스택도 보여준다:
The above error occurred in the <UserProfile> component:
in UserProfile (at App.js:45)
in div (at App.js:40)
in App
이건 "어느 컴포넌트에서 에러가 났는지" 보여주는 족보다. HTML 구조처럼 중첩된 컴포넌트 관계를 표시한다.
일반 스택 트레이스는 "함수 호출 순서"를, 컴포넌트 스택은 "UI 구조"를 보여준다. 둘 다 보면 전체 맥락을 파악하기 쉽다.
Node.js 에러는 파일 경로가 절대 경로로 나온다:
at Object.<anonymous> (/Users/me/project/server.js:15:3)
브라우저는 상대 경로가 많다:
at App (http://localhost:3000/src/App.jsx:23:10)
그리고 Node.js는 비동기 에러가 까다롭다. Promise 체인에서 에러가 나면 스택이 끊긴다:
fetch('/api/users')
.then(res => res.json())
.then(data => {
console.log(data.name); // 여기서 에러나면 위 fetch 맥락이 사라짐
});
그래서 async/await을 쓰는 게 스택 트레이스를 추적하기 더 쉽다:
try {
const res = await fetch('/api/users');
const data = await res.json();
console.log(data.name); // 에러나도 전체 호출 맥락이 남아있음
} catch (error) {
console.error(error); // 훨씬 명확한 스택
}
스택 트레이스 읽는 법을 익힌 후, 이런 습관이 생겼다:
node_modules 무시, src/ 폴더 찾기예전엔 에러가 나면 30분씩 헤맸다. 이제는 5분 안에 원인을 찾는다. 스택 트레이스를 친구처럼 대하게 되니 에러가 무섭지 않다. 오히려 "어디가 문제인지 알려줘서 고맙다"는 생각이 든다.
스택 트레이스는 에러의 "부검 보고서"다. 사인(에러 타입), 사망 시각(어느 줄), 마지막 행적(콜 스택)을 전부 알려준다. 이 정보만 제대로 읽으면 범인(버그)은 금방 잡힌다.
구글에 복붙하는 건 마지막 수단이다. 먼저 스택을 읽어보자. 내 코드 어디가 문제인지 3초 만에 알 수 있다면, 왜 10분씩 헤매겠는가.
디버깅은 추리 게임이 아니다. 지도를 보고 목적지로 가는 내비게이션이다. 스택 트레이스가 바로 그 지도다.