
F5를 눌렀더니 설정 파일을 만들라는 창이 뜨고, 뭘 골라야 할지 몰라 닫아버린 적이 있다면 이 글이 그 지점을 풀어준다. VS Code의 디버거는 launch.json 하나로 동작하고, 그 파일에서 알아야 할 것은 생각보다 적다. 이 글은 launch.json의 구조와 자주 쓰는 속성, 그리고 console.log를 지우러 다니지 않게 해주는 기능 두 가지를 다룬다.
launch.json 은 .vscode 안에 있고 두 가지 방식만 있다
파일 위치는 워크스페이스 루트의 .vscode/launch.json이다. 만드는 방법은 실행 및 디버그 뷰를 열고 "create a launch.json file"을 고른 뒤 디버그 환경을 선택하면 VS Code가 초안을 만들어 준다.
핵심 개념은 request 속성의 두 값이다. 문서의 설명이 명료하다. "launch 구성은 VS Code가 앱에 붙기 전에 디버그 모드로 앱을 시작하는 방법에 대한 레시피이고, attach 구성은 이미 실행 중인 앱이나 프로세스에 VS Code 디버거를 연결하는 방법에 대한 레시피다."
이 구분이 중요한 이유는 실패 양상이 다르기 때문이다. launch가 안 되면 프로그램 경로나 실행 환경이 틀린 것이고, attach가 안 되면 대상 프로세스가 디버그 포트를 안 열었거나 포트 번호가 다른 것이다. 도커 컨테이너 안에서 도는 서버에 붙을 때는 거의 항상 attach를 쓴다.
필수 속성은 세 개다. type(디버거 종류), request(launch 또는 attach), name(드롭다운에 보일 이름)이다.
자주 쓰는 속성은 열 개 남짓이다
문서가 여러 디버거에서 공통으로 지원한다고 밝힌 속성 중 실제로 손이 가는 것들을 정리한다.
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "서버 실행",
"program": "${workspaceFolder}/src/server.js",
"args": ["--port", "3000"],
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/.env.local",
"env": { "NODE_ENV": "development" },
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"]
}
]
}
skipFiles는 넣어두면 체감이 크다. 이게 없으면 한 단계씩 들어가다가 라이브러리 내부나 런타임 코드로 빠져서, 내 코드로 돌아오는 데 한참 걸린다. <node_internals>/**와 node_modules/**만 넣어도 디버깅이 훨씬 덜 답답하다.
console은 기본값이 내부 디버그 콘솔인데, 표준 입력을 받는 프로그램이면 integratedTerminal로 바꿔야 한다. 내부 콘솔은 입력을 못 받는다. 프로그램이 입력을 기다리며 멈춘 것처럼 보이면 이걸 의심하면 된다.
env는 값을 null로 주면 해당 변수를 지운다. 부모 환경에 있는 변수를 이 실행에서만 없애고 싶을 때 쓴다.
${workspaceFolder} 같은 변수를 알아야 경로가 안 깨진다
절대 경로를 박아 넣으면 다른 사람이 그 저장소를 클론했을 때 바로 깨진다. launch.json은 커밋되는 파일이니 변수를 써야 한다.
자주 쓰는 것은 셋이다. ${workspaceFolder}는 워크스페이스 루트, ${file}은 현재 열려 있는 파일, ${env:이름}은 환경 변수다.
${file}을 쓰면 "지금 보고 있는 파일을 그대로 실행" 구성을 만들 수 있다. 파이썬 스크립트를 이것저것 돌려볼 때 편하다.
{
"type": "debugpy",
"request": "launch",
"name": "현재 파일 실행",
"program": "${file}",
"console": "integratedTerminal"
}
운영체제마다 다르게 줘야 하는 값은 windows, linux, osx 블록으로 덮어쓴다. 문서 예시가 이런 형태다.
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/app.js",
"stopOnEntry": true,
"osx": { "stopOnEntry": false }
}
preLaunchTask 와 compound 로 준비 작업까지 묶는다
디버그를 시작하기 전에 빌드를 돌려야 하는 경우가 많다. preLaunchTask에 tasks.json의 태스크 이름을 적으면 자동으로 먼저 실행된다. 끝난 뒤 정리가 필요하면 postDebugTask를 쓴다.
{
"type": "node",
"request": "launch",
"name": "빌드 후 실행",
"preLaunchTask": "tsc: build",
"program": "${workspaceFolder}/dist/index.js"
}
서버와 클라이언트를 동시에 띄워야 한다면 compounds를 쓴다. 문서 예시를 그대로 옮기면 이렇다.
{
"configurations": [
{ "type": "node", "request": "launch", "name": "Server",
"program": "${workspaceFolder}/server.js" },
{ "type": "node", "request": "launch", "name": "Client",
"program": "${workspaceFolder}/client.js" }
],
"compounds": [
{ "name": "Server/Client", "configurations": ["Server", "Client"], "stopAll": true }
]
}
stopAll을 켜두면 한쪽을 멈출 때 나머지도 함께 멈춘다. 안 켜두면 서버만 남아 포트를 물고 있어서 다음 실행이 실패한다. 이것 때문에 "포트가 이미 사용 중"을 몇 번 겪고 나서야 이 옵션을 알게 됐다.
조건부 중단점과 로그포인트가 console.log를 대체한다
디버거를 쓰는데도 console.log를 계속 심는다면, 이 두 기능을 몰라서일 가능성이 높다.
조건부 중단점은 특정 조건일 때만 멈춘다. 중단점을 우클릭해서 조건을 넣으면 된다. 문서 설명은 "표현식이 true로 평가될 때마다 중단점이 걸린다"이다. 반복문이 1,000번 도는데 348번째만 이상할 때, i === 348이나 user.id === "abc" 같은 조건을 걸면 한 번에 그 지점으로 간다.
로그포인트는 더 유용하다. 문서는 이렇게 정의한다. "중단점의 변형으로, 디버거로 멈추지 않고 대신 디버그 콘솔에 메시지를 기록한다." 즉 코드를 건드리지 않고 로그를 찍는다. 중괄호 안에 표현식을 넣으면 값이 찍힌다.
로그포인트 메시지: user={user.id} count={items.length}
이 방식의 장점은 명확하다. 소스에 로그 코드가 들어가지 않으니 지우러 다닐 필요가 없고, 실수로 커밋될 일도 없다. 운영 코드에 디버그 로그가 남아 있는 사고가 꽤 자주 일어나는데, 로그포인트를 쓰면 구조적으로 막힌다.
디버거가 안 붙을 때 확인하는 순서
F5를 눌렀는데 아무 일도 안 일어나거나 곧바로 끝나버리는 경우가 있다. 순서대로 확인하면 대개 금방 찾는다.
먼저 어느 구성이 선택돼 있는지 본다. 실행 및 디버그 뷰 위쪽 드롭다운에 구성 이름이 보인다. 구성이 여러 개인데 엉뚱한 것이 골라져 있는 경우가 의외로 많다.
다음은 경로다. program에 적힌 파일이 실제로 존재하는지 확인한다. 빌드 산출물을 가리키는데 아직 빌드를 안 했다면 없는 게 당연하다. 이런 구성에는 preLaunchTask를 붙여두는 것이 맞다.
그다음은 디버그 콘솔의 첫 줄이다. 여기에 실행된 명령줄이 그대로 찍힌다. 내가 의도한 인자와 다르면 args나 envFile 쪽 문제다.
attach인데 안 붙는다면 대상 프로세스가 디버그 포트를 열었는지 확인한다. Node라면 --inspect나 --inspect-brk 플래그가 필요하고, 컨테이너라면 그 포트가 호스트로 노출돼 있어야 한다. localhost가 아니라 컨테이너 내부 주소를 바라보고 있는 경우도 흔하다.
마지막으로 중단점이 회색 빈 동그라미로 보이는 경우다. 이건 디버거가 그 줄을 소스와 연결하지 못했다는 뜻이다. 트랜스파일된 코드를 실행하면서 소스맵이 없거나 경로가 어긋난 것이다. outFiles나 sourceMapPathOverrides 같은 속성을 디버거 종류에 맞게 손봐야 한다.
그래도 print 가 나은 경우가 있다
디버거를 예찬했지만 반대 경우도 인정해야 공정하다. 나는 다음 세 가지 상황에서는 그냥 출력문을 쓴다.
첫째, 타이밍이 중요한 코드다. 디버거로 멈추면 그 사이에 타임아웃이 나거나 이벤트 순서가 바뀐다. 동시성 문제를 쫓을 때 디버거가 현상을 사라지게 만드는 일이 흔하다.
둘째, 실행이 아주 빠르게 여러 번 반복되는 경우다. 로그포인트도 매번 평가되므로 느려진다. 수만 번 도는 루프라면 출력해서 나중에 한꺼번에 보는 편이 낫다.
셋째, 디버거를 붙이기가 번거로운 환경이다. 원격 서버의 짧은 배치 작업 같은 것에 매번 포트를 열고 붙이는 것보다 로그 한 줄이 빠르다.
기준은 이렇다. 한 번 보고 끝날 값이면 출력문, 여러 번 들여다보며 파고들 문제면 디버거다. 도구는 문제의 성격에 맞춰 고르는 것이지, 어느 쪽이 더 세련되어서 고르는 게 아니다.
참고 자료
- Configure debugging — Visual Studio Code Docs —
launch.json위치, launch/attach 차이, 공통 속성, 변수 치환, compound 구성 - Debugging — Visual Studio Code Docs — 조건부 중단점과 로그포인트의 정의, 디버그 콘솔
- Tasks — Visual Studio Code Docs —
preLaunchTask가 참조하는 태스크를 정의하는 곳
'IT' 카테고리의 다른 글
| 메타 Muse 아마존 차단, AI 에이전트는 이름을 밝혀야 한다 (0) | 2026.09.23 |
|---|---|
| VS Code 스니펫 만들기, 반복 타이핑 줄이는 법 (0) | 2026.09.22 |
| Jev 모델 정리 — 속도보다 과금 구조가 진짜 변화다 (0) | 2026.09.21 |
| VS Code 정규식 검색·치환으로 반복 수정 끝내기 (0) | 2026.09.20 |
| VS Code 설정, 사용자 설정과 워크스페이스 설정 구분 (0) | 2026.09.19 |
