빨간 글씨만 보면 당황했던 시절
첫 번째 웹 앱을 만들 때였다. 콘솔에 빨간 글씨가 주르륵 뜨면 심장이 쿵 내려앉았다. 뭐가 문제인지도 모른 채 전체 에러 메시지를 복사해서 구글에 붙여넣었다. "아무나 답 좀..." 하는 심정이었다.
Stack Overflow에서 비슷한 에러를 찾으면 다행이었다. 하지만 대부분은 내 상황과 미묘하게 달랐다. 프레임워크 버전이 다르거나, 파일 구조가 다르거나, 아예 다른 맥락이었다. 그래서 답변에 있는 코드를 복붙했다가 더 이상한 에러가 나기도 했다.
그러던 어느 날, 시니어 개발자가 내 화면을 힐끗 보더니 "아, 여기 23번째 줄 보면 되겠네"라고 했다. 나는 50줄짜리 에러 메시지를 읽느라 정신없었는데, 그는 3초 만에 문제를 찾았다. 마술 같았다.
그날 이후 깨달았다. 스택 트레이스는 암호문이 아니라 지도였다. 읽는 법만 알면 에러가 난 위치로 바로 데려다주는 내비게이션이었다.
에러 메시지의 구조 - 세 가지 정보
스택 트레이스는 크게 세 부분으로 나뉜다는 걸 이해하니 모든 게 명확해졌다.
1. 에러 타입과 메시지 (What happened)
맨 위에 나오는 한 줄이 가장 중요하다. "무슨 일이 벌어졌는가"를 말해준다.
TypeError: Cannot read property 'name' of undefined
이 한 줄에서 두 가지를 알 수 있다:
- TypeError: 타입 관련 문제다 (undefined나 null을 잘못 다뤘을 확률 높음)
- 'name' of undefined:
undefined.name을 읽으려고 했다는 뜻
처음엔 이게 무슨 말인지 몰랐다. 근데 몇 번 보다 보니 패턴이 보였다. TypeError는 대부분 "없는 걸 있다고 착각했을 때" 나타난다. ReferenceError는 "변수 이름을 잘못 쓴 것"이고, SyntaxError는 "문법이 틀린 것"이다.
2. 콜 스택 (Where it happened)
에러 메시지 아래에 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번째 줄에 있다. 간단하다.
3. 파일 위치와 라인, 컬럼 (Exactly where)
(app.js:23:15) 이 부분이 GPS 좌표다.
- app.js: 파일 이름
- 23: 줄 번호
- 15: 그 줄에서 15번째 문자 (컬럼)
대부분 IDE는 이 형식을 인식해서 클릭하면 바로 해당 위치로 이동해준다. VSCode에서는 콘솔의 파일 경로를 Cmd+클릭하면 된다. 이거 알고 나서 디버깅 속도가 3배는 빨라졌다.
내 코드 vs 라이브러리 코드 구분하기
처음 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%다.
자주 보는 에러 타입들
몇 달 코딩하면서 에러 타입마다 "성격"이 있다는 걸 깨달았다.
TypeError: 타입 착각
const user = null;
console.log(user.name); // TypeError: Cannot read property 'name' of null
"있을 줄 알았는데 없었다." API에서 데이터를 못 받았거나, 배열이 비어있거나, props가 안 넘어왔을 때 주로 난다.
ReferenceError: 오타 or 선언 안 함
console.log(userName); // ReferenceError: userName is not defined
// 변수명 오타였다. 진짜 이름은 username이었음
이건 대부분 오타다. 아니면 import를 깜빡했거나.
SyntaxError: 문법 틀림
const data = { name: "John" // SyntaxError: Unexpected end of input
괄호를 안 닫았거나, 쉼표를 빼먹었거나. 이건 코드가 아예 실행 안 된다.
RangeError: 범위 초과
const arr = new Array(-1); // RangeError: Invalid array length
숫자가 말이 안 될 때. 재귀 함수가 무한 루프 돌 때도 이게 난다.
Source Map: 압축된 코드의 지도
프로덕션 배포하면 코드가 압축(minify)된다. 그럼 스택 트레이스가 이렇게 나온다:
at r.a (bundle.min.js:1:2847)
"r.a가 뭐야?" 이게 원래 내 getUserName 함수였는데, 압축 과정에서 이름이 바뀐 거다.
이럴 때 source map 파일(.map 확장자)이 있으면 브라우저가 자동으로 원본 코드 위치를 알려준다. Vite, Webpack 같은 번들러는 기본으로 source map을 생성한다.
개발자 도구 설정에서 "Enable source maps"를 켜면 압축된 코드에서도 원본 파일명과 줄 번호를 볼 수 있다. 이거 모르고 한참 헤맸다.
React의 Component Stack
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 vs 브라우저 스택 트레이스
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); // 훨씬 명확한 스택
}
디버깅 속도를 10배 올린 습관
스택 트레이스 읽는 법을 익힌 후, 이런 습관이 생겼다:
- 위 3줄만 집중한다: 답은 거의 항상 맨 위 3줄 안에 있다
- 내 코드부터 찾는다:
node_modules무시,src/폴더 찾기 - 에러 타입으로 범위를 좁힌다: TypeError면 null/undefined 체크, ReferenceError면 오타 확인
- 파일 경로를 클릭한다: 수동으로 파일 열지 말고 IDE가 자동으로 열게 하기
- async/await을 선호한다: Promise 체인보다 스택이 깔끔함
예전엔 에러가 나면 30분씩 헤맸다. 이제는 5분 안에 원인을 찾는다. 스택 트레이스를 친구처럼 대하게 되니 에러가 무섭지 않다. 오히려 "어디가 문제인지 알려줘서 고맙다"는 생각이 든다.
정리 - 빨간 글씨는 적이 아니다
스택 트레이스는 에러의 "부검 보고서"다. 사인(에러 타입), 사망 시각(어느 줄), 마지막 행적(콜 스택)을 전부 알려준다. 이 정보만 제대로 읽으면 범인(버그)은 금방 잡힌다.
구글에 복붙하는 건 마지막 수단이다. 먼저 스택을 읽어보자. 내 코드 어디가 문제인지 3초 만에 알 수 있다면, 왜 10분씩 헤매겠는가.
디버깅은 추리 게임이 아니다. 지도를 보고 목적지로 가는 내비게이션이다. 스택 트레이스가 바로 그 지도다.