console.log는 그만, VS Code로 자바스크립트를 제대로 디버깅하는 방법

자바스크립트를 배우기 시작하면 디버깅이라는 말을 들어도 실감이 잘 나지 않습니다. 코드가 이상하게 동작하면 console.log를 여기저기 넣어보고, 값을 확인하고, 다시 지우고. 간단한 코드라면 이걸로 충분하긴 합니다. 그런데 코드가 조금만 복잡해지면 상황이 달라집니다.
IT 강사로 27년을 일하면서 자바스크립트를 가르칠 때마다 느끼는 게 있습니다. 대부분의 초보 개발자가 VS Code의 디버거를 안 쓴다는 겁니다. VS Code에는 브라우저를 열지 않아도 Node.js 환경에서 바로 쓸 수 있는 디버거가 들어 있거든요. 코드를 한 줄씩 실행하면서 변수가 어떻게 바뀌는지 눈으로 확인하고, 원하는 지점에서 멈추고, 실행 도중에 변수 값을 바꿀 수도 있습니다.
이 기사에서는 VS Code 내장 디버거로 Node.js 환경에서 자바스크립트를 디버깅하는 방법을 처음부터 끝까지 다룹니다. 브라우저 디버깅은 다루지 않고요. VS Code와 Node.js만으로, 자바스크립트 코드를 제대로 들여다보는 방법을 배워보겠습니다.
Code Runner와 VS Code 디버거는 다릅니다
VS Code에서 자바스크립트를 실행할 때 Code Runner 확장 프로그램을 쓰는 분이 많습니다. Code Runner는 코드를 빠르게 실행해주는 도구입니다. 파일을 열고 재생 버튼을 누르면 OUTPUT 패널에 결과가 바로 나오죠. 코드를 작성하고 결과를 빠르게 확인할 때는 이만한 도구가 없습니다.
하지만 Code Runner는 "실행"만 해줍니다. 코드가 중간에 어떤 상태인지 확인할 수 없고, 특정 줄에서 멈출 수도 없죠. 결과가 예상과 다를 때 원인을 찾으려면 console.log를 여러 군데 넣는 수밖에 없습니다.
VS Code 내장 디버거를 쓰면 이야기가 완전히 달라집니다. 코드를 원하는 줄에서 멈추고, 해당 시점의 모든 변수 값을 확인하고, 한 줄씩 실행하면서 값이 어떻게 바뀌는지 추적할 수 있거든요. 함수 안으로 들어갈 수도 있고, 함수에서 빠져나올 수도 있고, 실행 도중에 변수 값을 바꿀 수도 있습니다.
Code Runner를 이미 설치한 상태라면 그대로 유지하면 됩니다. 간단한 코드 실행에는 Code Runner를 쓰고, 디버깅이 필요할 때는 VS Code 내장 디버거를 쓰면 되니까요. VS Code에 Node.js 디버거가 이미 들어 있어서 별도로 설치할 건 없습니다.
준비물은 딱 두 가지입니다. VS Code와 Node.js만 설치되어 있으면 됩니다. 터미널에서 node --version을 입력했을 때 버전이 표시되면 준비 끝입니다.
브레이크포인트로 코드를 원하는 줄에서 멈추기
디버깅의 첫걸음은 브레이크포인트입니다. "이 줄에서 코드 실행을 멈춰라"라고 표시해두는 지점이죠. 디버거가 코드를 실행하다가 브레이크포인트를 만나면 그 줄에서 일시정지합니다. 영상을 재생하다가 일시정지 버튼을 누른 것과 비슷합니다.
다음 예제 코드를 사용하겠습니다. app2.js라는 이름으로 파일을 만들고 아래 내용을 입력합니다.
let original = { name: "민수", age: 20, city: "서울" }; let copy = Object.assign({}, original); console.log(copy); copy.name = "영희"; console.log("원본:", original); console.log("복사본:", copy);
이 코드는 객체를 하나 만들고, Object.assign으로 복사한 다음, 복사본의 name을 바꿔봅니다. 원본과 복사본을 각각 출력해서 서로 영향을 주는지 확인하는 코드입니다.
브레이크포인트를 설정하는 방법은 간단합니다. 멈추고 싶은 줄의 줄 번호 왼쪽 여백을 클릭하면 됩니다. 빨간 점이 나타나면 설정된 겁니다. 키보드로는 해당 줄에 커서를 놓고 F9를 누르면 되고요. 같은 방법으로 다시 클릭하거나 F9를 누르면 해제됩니다.
아래 화면에서 2번 줄 왼쪽에 빨간 점이 보입니다. 이것이 브레이크포인트입니다.

브레이크포인트는 여러 줄에 동시에 설정할 수 있습니다. 1번 줄, 4번 줄, 8번 줄에 각각 걸면 디버거가 해당 줄을 만날 때마다 멈추죠. 처음에는 하나만 걸어보는 게 좋습니다. 2번 줄에 브레이크포인트를 설정해보겠습니다.
디버거 시작하기
브레이크포인트를 설정했으면 디버거를 시작합니다. 방법이 몇 가지 있는데요.
첫 번째는 메뉴를 쓰는 겁니다. 상단 메뉴에서 Run을 클릭하면 디버깅 관련 메뉴가 펼쳐집니다. 여기서 Start Debugging을 선택하거나, 단축키 F5를 누릅니다.

이 메뉴에서 중요한 단축키를 미리 확인해두면 좋습니다. Step Over는 F10, Step Into는 F11, Step Out은 Shift+F11, Continue는 F5입니다. 이 단축키들은 곧 자세히 설명하겠습니다.
두 번째는 왼쪽 사이드바의 Run and Debug 아이콘을 클릭하는 겁니다. 벌레 모양에 재생 버튼이 붙은 아이콘인데, 클릭하면 아래와 같은 화면이 나타납니다.

여기서 Run and Debug 버튼을 클릭하면 됩니다. 환경을 선택하라는 메시지가 나타나면 Node.js를 선택하고요.
JavaScript Debug Terminal 버튼도 눈여겨보시기 바랍니다. 이걸 클릭하면 디버그 전용 터미널이 열리는데, 이 터미널에서 node app2.js를 실행하면 자동으로 디버거가 붙습니다. launch.json 같은 설정 파일 없이도 바로 디버깅할 수 있어서 간편합니다.
어떤 방법이든 디버거가 시작되면 코드가 실행되다가 브레이크포인트에서 멈춥니다. 2번 줄에 브레이크포인트를 걸었으므로 1번 줄이 실행된 후 2번 줄에서 멈춥니다. 화면이 아래처럼 바뀝니다.

이 화면에서 눈여겨볼 게 몇 가지 있습니다.
먼저 코드 영역을 보면 2번 줄이 노란색으로 강조되어 있습니다. 디버거가 이 줄을 실행하기 직전에 멈춰 있다는 뜻이죠. 아직 2번 줄은 실행되지 않은 상태입니다.
왼쪽 VARIABLES 패널을 보면 현재 시점의 모든 변수를 확인할 수 있습니다. original 변수에는 이미 값이 들어 있는데, 1번 줄이 실행되었기 때문입니다. {name: '민수', age: 2...}라고 표시되죠. 반면 copy 변수는 undefined입니다. 2번 줄이 아직 실행되지 않았으니 copy에는 아무 값도 할당되지 않은 상태인 겁니다.
코드 영역 상단에는 디버그 도구 모음이 작게 나타납니다. 여섯 개의 버튼이 있는 작은 막대인데, 디버거를 조작하는 핵심 도구입니다. 다음 섹션에서 하나씩 알아보겠습니다.
Step Over로 한 줄씩 실행하기
디버거가 멈춘 상태에서 가장 많이 쓰는 기능이 Step Over입니다. 키보드에서 F10을 누르면 현재 줄을 실행하고 다음 줄로 이동합니다. 말 그대로 한 줄을 "넘어가는" 겁니다.
브레이크포인트에서 멈춘 상태, 즉 2번 줄에서 F10을 한 번 누르면 2번 줄이 실행되고 다음 실행할 줄인 4번 줄로 이동합니다. 3번 줄은 빈 줄이므로 건너뜁니다.

2번 줄이 실행되었기 때문에 왼쪽 VARIABLES 패널의 변수 상태가 바뀌었습니다. copy 변수가 더 이상 undefined가 아니고요. {name: '민수', age: 20, c...}라는 값이 들어 있습니다. Object.assign({}, original)이 실행되면서 original 객체의 내용이 복사된 겁니다.
VARIABLES 패널에서 original 변수 옆의 화살표를 클릭하면 객체의 속성을 펼쳐볼 수 있습니다. age: 20, city: '서울', name: '민수' 세 개의 속성이 보이죠. copy도 펼쳐보면 같은 값이 들어 있습니다.
이 상태에서 F10을 계속 누르면 한 줄씩 실행됩니다. 4번 줄 console.log(copy) 실행, 6번 줄 copy.name = "영희" 실행, 8번 줄 console.log("원본:", original) 실행. 매번 한 줄이 실행될 때마다 VARIABLES 패널의 값이 실시간으로 바뀝니다.
아래 화면은 6번 줄까지 실행한 후 8번 줄에 멈춘 모습입니다.

VARIABLES 패널을 자세히 보면 copy 객체의 name이 '영희'로 바뀌어 있습니다. 그런데 original 객체의 name은 여전히 '민수'입니다. Object.assign으로 복사한 객체는 원본과 별개이기 때문에 복사본을 수정해도 원본에 영향을 주지 않는다는 걸 눈으로 직접 확인할 수 있는 겁니다.
이게 디버거의 강점입니다. console.log로는 최종 결과만 볼 수 있지만, 디버거로는 코드가 실행되는 매 순간의 상태를 하나하나 확인할 수 있거든요.
디버그 도구 모음에 있는 여섯 개 버튼의 기능을 정리해보겠습니다.
| 버튼 | 단축키 | 기능 |
|---|---|---|
| Continue | F5 | 다음 브레이크포인트까지 실행 |
| Step Over | F10 | 현재 줄 실행 후 다음 줄로 이동 |
| Step Into | F11 | 함수 안으로 들어가기 |
| Step Out | Shift+F11 | 현재 함수에서 빠져나오기 |
| Restart | Cmd+Shift+F5 / Ctrl+Shift+F5 | 디버깅 처음부터 다시 시작 |
| Stop | Shift+F5 | 디버깅 종료 |
Continue(F5)는 디버거의 일시정지를 푸는 버튼입니다. 다음 브레이크포인트를 만날 때까지 코드를 쭉 실행하고요. 브레이크포인트가 더 없으면 프로그램이 끝까지 실행됩니다.
Step Into와 Step Out은 함수와 관련된 기능인데, 바로 다음 섹션에서 자세히 다루겠습니다.
함수 안으로 들어가고 나오기: Step Into와 Step Out
Step Over(F10)는 함수 호출이 있어도 그 함수를 통째로 실행하고 다음 줄로 넘어갑니다. 함수의 결과만 필요하고 내부 동작은 관심 없을 때 유용하죠. 그런데 함수 안에서 무슨 일이 벌어지는지 확인해야 할 때가 있습니다. 이때 쓰는 게 Step Into입니다.
Step Into와 Step Out을 체험하기 위해 함수가 포함된 예제 코드를 하나 더 만들어보겠습니다. debug_func.js라는 파일을 만듭니다.
function createUser(name, age) { let user = { name: name, age: age, grade: calculateGrade(age) }; return user; } function calculateGrade(age) { if (age >= 20) { return "성인"; } else if (age >= 14) { return "청소년"; } else { return "어린이"; } } let user1 = createUser("민수", 20); console.log(user1); let user2 = createUser("영희", 15); console.log(user2);
이 코드에는 두 개의 함수가 있습니다. createUser 함수는 이름과 나이를 받아서 사용자 객체를 만드는데, 이때 calculateGrade 함수를 호출해서 나이에 따른 등급을 계산합니다.
21번 줄 let user1 = createUser("민수", 20);에 브레이크포인트를 설정하고 F5로 디버거를 시작합니다. 디버거가 21번 줄에서 멈추면, 여기서 F10(Step Over)을 누르면 createUser 함수가 통째로 실행되고 바로 22번 줄로 이동합니다. 함수 안에서 무슨 일이 있었는지는 알 수 없습니다.
반면 F11(Step Into)을 누르면 createUser 함수 안으로 들어갑니다. 코드 실행 위치가 1번 줄로 이동합니다. 함수 내부에서도 F10으로 한 줄씩 실행할 수 있습니다. 5번 줄 grade: calculateGrade(age)에 도착했을 때 다시 F11을 누르면 calculateGrade 함수 안으로 또 들어갑니다. 이렇게 함수가 중첩되어 호출되는 경우에도 한 단계씩 깊이 파고들 수 있습니다.
calculateGrade 함수 안에서 디버깅을 마치고 싶을 때는 Step Out(Shift+F11)을 씁니다. Step Out을 누르면 현재 함수의 나머지 코드를 모두 실행하고, 이 함수를 호출한 곳으로 돌아갑니다. calculateGrade 안에서 Step Out을 누르면 createUser의 5번 줄로 돌아오고요. createUser 안에서 한 번 더 Step Out을 누르면 21번 줄로 돌아옵니다.
세 가지를 정리하면 이렇습니다.
- Step Over (
F10): 함수를 만나면 통째로 실행하고 넘어갑니다. "이 함수는 안 볼래." - Step Into (
F11): 함수를 만나면 안으로 들어갑니다. "이 함수 안에서 무슨 일이 일어나는지 볼래." - Step Out (
Shift+F11): 현재 함수에서 빠져나옵니다. "이 함수 안은 다 봤으니 나갈래."
이 세 가지를 자유자재로 쓸 수 있으면 복잡하게 중첩된 함수 호출에서도 원하는 부분만 골라서 살펴볼 수 있습니다. 실무에서 버그를 추적할 때 가장 많이 쓰는 조합이기도 하고요.
Watch 패널로 원하는 값 감시하기
VARIABLES 패널은 현재 스코프에 있는 모든 변수를 보여줍니다. 변수가 많아지면 찾기가 번거롭죠. 특정 변수나 표현식의 값만 계속 추적하고 싶을 때 쓰는 게 Watch 패널입니다.
Watch 패널은 VARIABLES 패널 아래에 있습니다. 접혀 있으면 WATCH라는 글자를 클릭해서 펼치면 되고요. + 버튼을 클릭하면 입력창이 나타나는데, 여기에 감시하고 싶은 변수 이름이나 표현식을 입력합니다.

copy라고 입력하고 Enter를 누르면 copy 변수가 Watch 목록에 추가됩니다. 이제 코드를 한 줄씩 실행할 때마다 Watch 패널에서 copy의 현재 값을 바로 확인할 수 있습니다.
Watch 패널이 좋은 건 단순한 변수 이름뿐 아니라 표현식도 감시할 수 있다는 겁니다. 예를 들어 다음과 같은 것들을 Watch에 추가할 수 있습니다.
copy.name original.age + copy.age typeof copy JSON.stringify(copy) copy.name === original.name Object.keys(original).length
copy.name === original.name 같은 비교 표현식을 Watch에 넣으면, 두 값이 같은 동안은 true, 달라지면 false로 바뀝니다. 특정 조건이 언제 바뀌는지 추적할 때 쓸 만합니다.
아래 화면은 디버깅 중인 사이드바 전체 모습입니다. VARIABLES, WATCH, CALL STACK, BREAKPOINTS 패널이 모두 보입니다.

CALL STACK 패널에는 현재 실행 중인 함수의 호출 스택이 표시됩니다. 어떤 함수에서 어떤 함수를 호출했는지 경로를 볼 수 있죠. 위 화면에서는 app2.js의 최상위 코드에서 실행이 멈춰 있으므로 <anonymous>라고 표시됩니다. 앞서 만든 debug_func.js 예제에서 calculateGrade 안에 브레이크포인트를 걸면 calculateGrade <- createUser <- <anonymous> 순서로 호출 경로가 나옵니다.
BREAKPOINTS 패널에는 현재 설정된 모든 브레이크포인트 목록이 나옵니다. 체크박스를 해제하면 일시적으로 비활성화하고, 다시 체크하면 활성화할 수 있어서 코드를 편집하지 않고도 브레이크포인트를 켰다 껐다 할 수 있습니다.
Debug Console: 실행 중에 변수를 수정하고 식을 평가하기
디버거에서 꼭 알아야 할 기능이 Debug Console입니다. 코드가 브레이크포인트에서 멈춰 있을 때 Debug Console에서 자바스크립트 코드를 직접 실행할 수 있거든요. 변수 값을 확인하는 건 물론이고, 변수에 새 값을 넣거나 함수를 호출하는 것도 됩니다.
Debug Console은 VS Code 하단 패널에 있습니다. PROBLEMS, OUTPUT, TERMINAL 탭 옆에 DEBUG CONSOLE 탭이 있는데, 디버거가 실행 중일 때만 활성화됩니다.

Debug Console 하단의 입력창에 자바스크립트 표현식을 입력하고 Enter를 누르면 즉시 결과가 나옵니다. 디버거가 브레이크포인트에서 멈춰 있는 상태에서 특히 유용합니다.
변수 값 확인하기
Debug Console에 변수 이름을 입력하면 현재 값이 바로 나옵니다.

> copy { name: '민수', age: 20, city: '서울' } > original.name '민수' > typeof copy 'object'
Watch 패널에서도 비슷한 일을 할 수 있지만, Debug Console은 한 번만 확인하고 싶은 복잡한 표현식을 빠르게 평가할 때 더 편합니다.
변수 값 수정하기
Debug Console에서 변수에 새 값을 할당할 수도 있습니다. 코드를 실행하는 도중에 변수 값을 바꿔볼 수 있다는 뜻인데, 이게 꽤 쓸모 있습니다.
예를 들어 디버거가 4번 줄에서 멈춰 있을 때, Debug Console에 다음과 같이 입력합니다.
> copy.name = "철수" '철수'
이렇게 하면 copy.name이 "철수"로 바뀝니다. 이 상태에서 F10으로 계속 실행하면 코드가 원래의 "영희" 대신 "철수"가 적용된 상태로 진행됩니다.
어떤 함수가 특정 조건에서만 버그가 발생한다고 해보겠습니다. 그 조건을 만들기 위해 코드를 수정하고 다시 실행하는 대신, 디버거에서 변수 값을 직접 바꿔서 그 조건을 만들 수 있습니다. 시간이 많이 줄어듭니다.
VARIABLES 패널에서도 변수 값을 수정할 수 있습니다. 수정하고 싶은 변수를 더블클릭하면 편집 모드로 바뀌는데, 새 값을 입력하고 Enter를 누르면 됩니다.
표현식 평가하기
Debug Console에서는 어떤 자바스크립트 표현식이든 실행할 수 있습니다. 현재 스코프에 있는 변수를 써서 복잡한 계산도 해볼 수 있고요.
> Object.keys(copy) ['name', 'age', 'city'] > copy.age > 18 ? "성인" : "미성년" '성인' > JSON.stringify(copy, null, 2) '{\n "name": "민수",\n "age": 20,\n "city": "서울"\n}' > { ...copy, email: "minsu@example.com" } { name: '민수', age: 20, city: '서울', email: 'minsu@example.com' }
새로운 객체를 만들어보는 것도 되고, 배열 메서드를 실행해보는 것도 됩니다. 코드를 수정하지 않고도 "이렇게 하면 어떻게 될까?"를 바로 시험해볼 수 있는 셈이죠.
Debug Console에서 console.log를 쓰면 결과가 Debug Console에 출력됩니다. console.table(copy) 같은 것도 됩니다.
조건부 브레이크포인트와 로그포인트
일반 브레이크포인트는 해당 줄을 실행할 때마다 멈춥니다. 반복문 안에 브레이크포인트를 걸면 매 반복마다 멈추기 때문에, 100번 반복하는 루프의 50번째에서 확인하고 싶으면 F5를 49번이나 눌러야 하는 셈이죠. 이때 쓰는 게 조건부 브레이크포인트입니다.
다음 코드로 테스트해봅니다.
let total = 0; for (let i = 1; i <= 100; i++) { total += i; console.log(`${i}번째: 합계 = ${total}`); } console.log("최종 합계:", total);
3번 줄에 조건부 브레이크포인트를 설정하려면, 줄 번호 왼쪽 여백에서 마우스 오른쪽 버튼을 클릭하고 Add Conditional Breakpoint를 선택합니다. 입력창이 나타나면 조건식을 입력합니다.
i === 50
이렇게 하면 i가 50일 때만 멈춥니다. 나머지 99번의 반복에서는 그냥 지나가고요.
조건식에는 자바스크립트 표현식을 자유롭게 쓸 수 있습니다.
i % 10 === 0 // 10의 배수일 때마다 멈춤 total > 1000 // 합계가 1000을 넘으면 멈춤 i === 50 && total > 0 // 여러 조건 조합
Hit Count 브레이크포인트
조건부 브레이크포인트 입력창에서 드롭다운을 Hit Count로 바꾸면 "이 줄이 N번 실행되었을 때 멈춰라"라고 지정할 수 있습니다. 10을 입력하면 10번째 실행에서 멈춥니다.
로그포인트
로그포인트는 브레이크포인트처럼 보이지만 코드를 멈추지 않습니다. 대신 Debug Console에 메시지를 출력하죠. 코드에 console.log를 추가하지 않고도 실행 중에 값을 확인할 수 있습니다.
설정 방법은 줄 번호 왼쪽 여백에서 마우스 오른쪽 버튼을 클릭하고 Add Logpoint를 선택하면 됩니다. 메시지 템플릿을 입력하는데, 변수는 중괄호로 감싸면 되고요.
현재 i = {i}, total = {total}
이렇게 하면 해당 줄이 실행될 때마다 현재 i = 1, total = 1, 현재 i = 2, total = 3 같은 메시지가 Debug Console에 출력됩니다. 코드를 한 글자도 수정하지 않고 console.log와 같은 효과를 얻는 셈이죠. 디버깅이 끝나면 로그포인트만 삭제하면 되니까 코드를 원래 상태로 되돌릴 필요가 없습니다.
로그포인트는 빨간 점 대신 빨간 다이아몬드 모양으로 표시되어서 일반 브레이크포인트와 구분이 됩니다.
실전 디버깅 시나리오: 버그를 추적하는 과정
디버거의 기능을 하나씩 알아보았으니, 이제 실제 버그를 추적하는 과정을 경험해보겠습니다. 다음 코드에는 의도적으로 버그를 넣어두었습니다. bug_example.js라는 파일을 만들어봅니다.
function calculateDiscount(price, memberType) { let discountRate = getDiscountRate(memberType); let discountAmount = price * discountRate; let finalPrice = price - discountAmount; return finalPrice; } function getDiscountRate(memberType) { if (memberType === "gold") { return 0.2; } else if (memberType === "silver") { return 0.1; } else if (memberType === "bronze") { return 0.05; } } let items = [ { name: "노트북", price: 1500000, member: "gold" }, { name: "마우스", price: 50000, member: "silver" }, { name: "키보드", price: 80000, member: "normal" }, { name: "모니터", price: 300000, member: "bronze" } ]; let totalPrice = 0; for (let i = 0; i < items.length; i++) { let item = items[i]; let finalPrice = calculateDiscount(item.price, item.member); totalPrice += finalPrice; console.log(`${item.name}: ${item.price}원 -> ${finalPrice}원`); } console.log(`총 결제금액: ${totalPrice}원`);
이 코드를 실행하면 결과가 좀 이상합니다.
노트북: 1500000원 -> 1200000원 마우스: 50000원 -> 45000원 키보드: 80000원 -> NaN원 모니터: 300000원 -> 285000원 총 결제금액: NaN원
키보드의 할인가가 NaN이고, 총 결제금액도 NaN입니다. console.log를 여기저기 넣어볼 수도 있겠지만, 디버거로 훨씬 빠르게 원인을 찾을 수 있습니다.
let finalPrice = calculateDiscount(item.price, item.member); 줄에 조건부 브레이크포인트를 설정합니다. 조건은 item.name === "키보드"로 하고요. 키보드 항목을 처리할 때만 멈추도록 하는 겁니다.
F5로 디버거를 시작하면 키보드 항목을 처리할 때 멈춥니다. 여기서 F11(Step Into)을 누르면 calculateDiscount 함수 안으로 들어갑니다. price는 80000, memberType은 "normal"입니다.
한 줄 더 실행하면 getDiscountRate 호출로 이동합니다. 다시 F11로 getDiscountRate 안으로 들어갑니다. 이 함수 안에서 memberType은 "normal"인데, if-else 조건에 "normal"은 없습니다. "gold", "silver", "bronze"만 처리하고 있습니다. F10으로 한 줄씩 실행하면 모든 조건을 건너뛰고 함수가 끝나는 것을 볼 수 있습니다.
이때 Debug Console에서 discountRate를 입력하면 undefined가 나옵니다. getDiscountRate 함수에 "normal" 타입에 대한 return 문이 없어서 undefined를 반환한 거죠. undefined * 80000은 NaN이 되고, 80000 - NaN도 NaN이 됩니다.
원인을 찾았습니다. getDiscountRate 함수에 기본 반환값을 추가하면 해결됩니다.
function getDiscountRate(memberType) { if (memberType === "gold") { return 0.2; } else if (memberType === "silver") { return 0.1; } else if (memberType === "bronze") { return 0.05; } else { return 0; // 일반 회원은 할인 없음 } }
수정 후 다시 실행하면 정상적으로 나옵니다.
노트북: 1500000원 -> 1200000원 마우스: 50000원 -> 45000원 키보드: 80000원 -> 80000원 모니터: 300000원 -> 285000원 총 결제금액: 1610000원
이 과정을 console.log로 했다면 여러 곳에 출력문을 넣고, 실행하고, 결과를 보고, 출력문을 옮기고, 다시 실행하는 과정을 반복했을 겁니다. 디버거로는 코드를 한 글자도 수정하지 않고 몇 분 만에 원인을 찾았습니다.
launch.json으로 디버깅 환경 설정하기
지금까지 설명한 방법은 launch.json 파일 없이도 작동합니다. VS Code가 .js 파일을 감지하면 자동으로 Node.js 디버거를 연결하거든요. 간단한 파일 하나를 디버깅할 때는 이걸로 충분합니다.
그런데 프로젝트가 커지면 launch.json이 필요한 상황이 생깁니다. 특정 인자를 전달하면서 실행하고 싶거나, 환경 변수를 설정하고 싶거나, 특정 디렉토리에서 실행하고 싶을 때가 그렇습니다.
launch.json은 .vscode 폴더 안에 만듭니다. 가장 간단한 형태는 이렇습니다.
{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "현재 파일 디버그", "program": "${file}", "skipFiles": ["<node_internals>/**"] } ] }
"program": "${file}"은 현재 열려 있는 파일을 실행한다는 뜻입니다. skipFiles는 Node.js 내부 코드를 Step Into하지 않도록 건너뛰는 설정인데, 이게 없으면 console.log 안으로 Step Into했을 때 Node.js 내부 코드로 들어가버립니다. 보통 원하는 동작이 아니죠.
인자를 전달하고 싶으면 args 속성을 추가합니다.
{ "type": "node", "request": "launch", "name": "인자 포함 디버그", "program": "${file}", "args": ["--port", "3000"], "skipFiles": ["<node_internals>/**"] }
환경 변수를 설정하고 싶으면 env 속성을 쓰면 됩니다.
{ "type": "node", "request": "launch", "name": "환경 변수 포함 디버그", "program": "${file}", "env": { "NODE_ENV": "development", "DEBUG": "true" }, "skipFiles": ["<node_internals>/**"] }
처음에는 launch.json 없이 시작하고, 필요한 상황이 생기면 그때 추가하면 됩니다.
디버깅을 편하게 해주는 추가 기능들
인라인 값 표시
디버거로 코드를 실행하면 각 변수의 현재 값이 코드 옆에 작은 글씨로 나타납니다. VARIABLES 패널까지 가지 않아도 코드 바로 옆에서 값을 확인할 수 있어서 편합니다.
마우스 호버로 값 확인
디버거가 멈춰 있을 때 코드 안의 변수에 마우스를 올리면 해당 변수의 현재 값이 툴팁으로 나타납니다. 객체인 경우 펼쳐서 속성까지 확인할 수 있고요. VARIABLES 패널이나 Watch를 쓰지 않아도 빠르게 값을 확인할 수 있는 방법입니다.
예외 발생 시 자동 멈추기
BREAKPOINTS 패널 하단에 Caught Exceptions와 Uncaught Exceptions 체크박스가 있습니다. Uncaught Exceptions를 체크하면 처리되지 않은 예외가 발생했을 때 디버거가 자동으로 멈추는데, 브레이크포인트를 설정하지 않아도 에러가 발생한 바로 그 줄에서 멈추기 때문에 원인을 바로 확인할 수 있습니다.
Caught Exceptions까지 체크하면 try-catch로 잡은 예외에서도 멈춥니다. 예외가 많은 코드에서는 너무 자주 멈출 수 있으니 상황에 따라 쓰면 됩니다.
자주 쓰는 단축키 정리
디버깅 중에 마우스보다 키보드 단축키를 쓰는 게 훨씬 빠릅니다. 처음에는 외우기 어려워도 몇 번 쓰면 손에 익습니다.
| 동작 | macOS | Windows/Linux |
|---|---|---|
| 디버깅 시작/계속 | F5 | F5 |
| 디버깅 중지 | Shift+F5 | Shift+F5 |
| 디버깅 재시작 | Cmd+Shift+F5 | Ctrl+Shift+F5 |
| Step Over | F10 | F10 |
| Step Into | F11 | F11 |
| Step Out | Shift+F11 | Shift+F11 |
| 브레이크포인트 토글 | F9 | F9 |
외워야 할 핵심은 F5, F9, F10, F11, Shift+F11 이 다섯 가지입니다. 이것만 알아도 디버깅의 80%는 할 수 있습니다.
마무리
처음 디버거를 접하면 console.log보다 오히려 복잡하게 느껴질 수 있습니다. 그런데 한 번 익숙해지면 console.log로 돌아가기 어렵습니다. 코드를 실행 도중에 멈추고, 모든 변수의 상태를 한눈에 보고, 함수 안으로 들어갔다 나오고, 조건을 바꿔가며 테스트하는 것. 이게 전부 코드 한 줄 수정 없이 됩니다.
오늘 다룬 내용을 정리하면 이렇습니다. 브레이크포인트로 멈추고(F9), Step Over로 한 줄씩 실행하고(F10), Step Into로 함수 안으로 들어가고(F11), Step Out으로 함수에서 나오고(Shift+F11), Watch로 값을 감시하고, Debug Console에서 변수를 수정하고, 조건부 브레이크포인트로 원하는 조건에서만 멈추고, 로그포인트로 코드 수정 없이 로그를 남깁니다.
가장 빠르게 디버거에 익숙해지는 방법은 간단한 코드를 작성하고 브레이크포인트를 걸어서 한 줄씩 실행해보는 겁니다. 이 기사에서 사용한 예제 코드를 직접 타이핑하고 디버깅해보시기 바랍니다. F10을 누를 때마다 변수가 바뀌는 걸 보는 순간, "이런 게 있었다니" 싶을 겁니다.
이 기사에서는 동기 코드의 디버깅을 다루었습니다. 자바스크립트를 더 배우다 보면 콜백, Promise, async/await 같은 비동기 코드를 만나게 되는데, 비동기 코드의 디버깅은 동기 코드와 조금 다른 접근이 필요합니다. 이 주제는 다음 기사에서 다루겠습니다.
console.log는 손전등이고, VS Code 디버거는 방 전체의 조명을 켜는 겁니다. 어둠 속에서 손전등으로 한 곳씩 비추는 것보다 불을 확 켜고 전체를 보는 게 당연히 빠르겠죠. 오늘 당장 여러분의 코드에 브레이크포인트를 하나 찍어보시기 바랍니다.






댓글
댓글을 작성하려면 이 필요합니다.