Skip to Content
WebJavaScriptECMAScript14. 에러 처리와 디버깅

이번 편의 결과물: 잘못된 명령이나 존재하지 않는 id를 입력하면 스택 트레이스 대신 읽을 수 있는 에러 메시지가 출력됩니다. node --inspect로 중단점도 확인합니다. · 다루는 개념: 에러 객체 계층, 커스텀 에러 클래스, console/--inspect 디버깅

이 편에서 만드는 파일

todo-cli/ └── src/ ├── errors.js (+) ├── index.js (~) ├── models/ │ └── TodoStore.js (~) └── commands/ ├── done.js (+) └── remove.js (+)

개념 정리

에러 객체 계층

JavaScript의 모든 에러는 Error를 상속합니다. 내장 서브클래스는 원인별로 나뉩니다.

클래스발생 상황
Error기본 에러, 직접 만드는 커스텀 에러의 부모
TypeError값의 타입이 예상과 다름(undefined.length)
RangeError값이 허용 범위를 벗어남(배열 길이에 음수)
SyntaxErrorJSON.parse에 잘못된 문자열을 넘김

error.name, error.message, error.stack 세 속성은 모든 에러 인스턴스에 공통으로 있습니다. error.cause(ES2022)는 에러를 감쌀 때 원래 원인을 함께 담습니다.

커스텀 에러 클래스

Error를 상속해 todo-cli 전용 에러를 만들면, instanceof로 종류를 구분해 각기 다른 메시지를 보여줄 수 있습니다.

class TodoNotFoundError extends Error { constructor(id) { super(`id ${id}인 할 일을 찾을 수 없습니다.`); this.name = 'TodoNotFoundError'; this.id = id; } }

super(message)Error의 생성자를 호출해 messagestack을 채웁니다. this.name을 지정하지 않으면 console.errorError: ...로만 찍혀 어떤 종류인지 구분하기 어렵습니다.

try/catch/finally와 에러 구분

실행 시점
try정상 흐름을 시도
catch (error)try 블록에서 에러가 던져졌을 때만
finally에러 발생 여부와 무관하게 항상(자원 정리용)

catch 안에서 error instanceof TodoNotFoundError처럼 종류를 나눠 사용자 메시지를 다르게 보여줍니다. 알 수 없는 에러는 원래 스택 트레이스를 그대로 보여줘 디버깅에 활용합니다.

실습

1. src/errors.js 만들기

todo-cli 전용 에러 세 가지를 정의합니다.

2. 코드 작성

// src/errors.js export class TodoNotFoundError extends Error { constructor(id) { super(`id ${id}인 할 일을 찾을 수 없습니다.`); this.name = 'TodoNotFoundError'; this.id = id; } } export class InvalidArgumentError extends Error { constructor(message) { super(message); this.name = 'InvalidArgumentError'; } } export class StorageError extends Error { constructor(message, { cause } = {}) { super(message, { cause }); this.name = 'StorageError'; } }

StorageError는 파일 읽기/쓰기 실패를 감쌀 때 씁니다. { cause }로 원래 파일시스템 에러(ENOENT 등)를 보존해, 디버깅할 때는 error.cause로 원인을 추적할 수 있습니다.

3. TodoStore에 remove 메서드 추가

done·remove 명령은 findById(09편)에 더해 항목을 지우는 기능이 필요합니다.

// src/models/TodoStore.js (remove 메서드만 발췌) // ...기존 fromArray/toArray/add/list/findById/tagCounts/replace/Symbol.iterator 유지 remove(id) { const index = this.#todos.findIndex((todo) => todo.id === id); if (index === -1) return false; this.#todos.splice(index, 1); return true; }

4. commands/done.js, remove.js에서 에러 던지기

// src/commands/done.js import { TodoNotFoundError } from '../errors.js'; export function runDone(store, positional) { const id = Number(positional[0]); const todo = store.findById(id); if (!todo) { throw new TodoNotFoundError(id); } todo.done = true; console.log(`완료 처리됨: #${todo.id} ${todo.title}`); }
// src/commands/remove.js import { TodoNotFoundError } from '../errors.js'; export function runRemove(store, positional) { const id = Number(positional[0]); const removed = store.remove(id); if (!removed) { throw new TodoNotFoundError(id); } console.log(`삭제됨: #${id}`); }

5. src/index.js에서 에러 구분해 출력하고 done·remove 명령 연결

// src/index.js (전체 구조: 기존 if/else if 체인을 async 함수와 try/catch로 감싼다) import { parseArgs } from './cli/parseArgs.js'; import { TodoStore } from './models/TodoStore.js'; import { loadTodos, saveTodos } from './storage/fileStore.js'; import { runAdd } from './commands/add.js'; import { runList } from './commands/list.js'; import { runDone } from './commands/done.js'; import { runRemove } from './commands/remove.js'; import { runTags } from './commands/tags.js'; import { runSearch } from './commands/search.js'; import { runStats } from './commands/stats.js'; import { runExport } from './commands/export.js'; import { runImport } from './commands/import.js'; import { TodoNotFoundError, InvalidArgumentError, StorageError } from './errors.js'; async function main() { const argv = process.argv.slice(2); const { command, positional, flags } = parseArgs(argv); const rawTodos = await loadTodos(); const store = TodoStore.fromArray(rawTodos); try { if (command === 'add') { await runAdd(store, positional, flags); await saveTodos(store.toArray().map((todo) => todo.toJSON())); } else if (command === 'list') { runList(store, flags); } else if (command === 'tags') { runTags(store); } else if (command === 'search') { runSearch(store, positional); } else if (command === 'stats') { runStats(store); } else if (command === 'export') { await runExport(store, positional); } else if (command === 'import') { await runImport(store, positional); await saveTodos(store.toArray().map((todo) => todo.toJSON())); } else if (command === 'done') { runDone(store, positional); await saveTodos(store.toArray().map((todo) => todo.toJSON())); } else if (command === 'remove') { runRemove(store, positional); await saveTodos(store.toArray().map((todo) => todo.toJSON())); } else { console.log('알 수 없는 명령입니다:', command); } } catch (error) { if (error instanceof TodoNotFoundError || error instanceof InvalidArgumentError) { console.error(`오류: ${error.message}`); process.exitCode = 1; return; } if (error instanceof StorageError) { console.error(`저장소 오류: ${error.message}`); if (error.cause) console.error(`원인: ${error.cause.message}`); process.exitCode = 1; return; } console.error('예상하지 못한 오류가 발생했습니다.'); console.error(error.stack); process.exitCode = 1; } } main();

기존 if/else if 체인은 그대로 두고 done·remove 분기만 추가한 뒤, 전체를 async function main()try/catch로 감쌌습니다. 사용자 실수(TodoNotFoundError, InvalidArgumentError)와 저장소 문제(StorageError)는 짧은 한 줄 메시지만 보여주고, 그 외 예상 못 한 에러만 error.stack을 그대로 노출합니다. 이렇게 하면 사용자는 스택 트레이스에 겁먹지 않고, 개발자는 진짜 버그일 때만 스택을 봅니다.

5. 실행(정상 에러 처리 확인)

node src/index.js done 999

6. node —inspect로 중단점 확인

node --inspect-brk src/index.js done 1

확인

  • done 999(존재하지 않는 id) 실행 결과:
오류: id 999인 할 일을 찾을 수 없습니다.
  • 종료 코드가 1인지 echo $?(macOS/Linux) 또는 echo $LASTEXITCODE(PowerShell)로 확인합니다.
  • node --inspect-brk로 실행하면 터미널에 Debugger listening on ws://... 메시지가 나오고, Chrome 주소창에 chrome://inspect를 열어 “Open dedicated DevTools for Node”를 누르면 runDone 첫 줄에서 실행이 멈춥니다.

직접 해보기

  1. add 명령에 제목 없이 실행하면(node src/index.js add) 어떤 에러가 나는지 확인하고, InvalidArgumentError로 감싸 “할 일 제목이 필요합니다.”라는 메시지가 나오게 고쳐봅니다.
  2. error.cause를 이용해 StorageError를 만들 때, 원래 fs 에러의 code('ENOENT' 등)까지 함께 출력하도록 index.js의 catch 블록을 수정해봅니다.

정답 보기(1번)

// src/commands/add.js (일부) import { InvalidArgumentError } from '../errors.js'; export async function runAdd(store, positional, flags) { const [title] = positional; if (!title) { throw new InvalidArgumentError('할 일 제목이 필요합니다: todo add "제목"'); } // ...기존 로직 유지 }

자주 하는 실수

증상원인고치는 법
커스텀 에러인데 Error: ...로만 찍힘this.name을 지정하지 않음생성자에서 this.name = '클래스이름' 지정
catch에서 모든 에러가 같은 메시지로 처리됨instanceof 분기 없이 하나로 처리에러 종류별로 instanceof 분기 추가
--inspect를 켰는데 바로 끝까지 실행됨--inspect만 쓰면 중단 없이 연결만 대기시작하자마자 멈추려면 --inspect-brk 사용

확인 문제

문제 14지선다
커스텀 에러 클래스를 만들 때 super(message)가 하는 일은?
문제 24지선다
try/catch/finally 중 에러 발생 여부와 무관하게 항상 실행되는 절은?
문제 34지선다
error instanceof TodoNotFoundError로 에러 종류를 구분하는 이유는?
문제 44지선다
node --inspect-brk와 node --inspect의 차이는?

참고 자료

Last updated on