출시·고도화 중
TypeScript 안내서 · 5/6
TypeScript의 오류 처리는 JavaScript의 throw와 try...catch를 그대로 씁니다. 다만 타입 시스템 덕분에 오류를 더 안전하게 다룰 수 있고, 실패할 수 있는 작업을 타입으로 드러내는 방법도 선택할 수 있습니다. 이 장에서는 unknown 타입의 catch 변수, 사용자 정의 오류 클래스, 결과 타입 패턴을 살펴본 뒤, Vitest와 Node.js 내장 테스트 러너로 테스트를 작성하는 방법을 알아봅니다.
JavaScript에서는 throw "문자열"처럼 Error가 아닌 값도 던질 수 있습니다. 그래서 strict 모드에서 catch 변수의 타입은 unknown입니다(useUnknownInCatchVariables 옵션). 속성을 쓰기 전에 무엇인지 먼저 확인해야 합니다.
function parseConfig(text: string): Record<string, unknown> {
try {
return JSON.parse(text);
} catch (err) {
// err: unknown
if (err instanceof SyntaxError) {
console.error("JSON 형식 오류:", err.message);
} else {
console.error("알 수 없는 오류:", err);
}
return {};
} finally {
console.log("설정 읽기 시도 끝");
}
}
function errorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}errorMessage 같은 작은 도우미를 하나 두면 로그를 남길 때마다 같은 확인을 반복하지 않아도 됩니다. 비동기 함수에서도 await를 try 안에 두면 거부된 Promise를 같은 방식으로 잡을 수 있습니다.
Error를 상속하면 오류 종류를 클래스로 구분하고, 필요한 정보를 속성으로 함께 담을 수 있습니다. name을 지정해 두면 로그에서 오류 종류가 잘 보입니다. 원인이 된 오류는 cause 옵션으로 이어 둡니다.
class NotFoundError extends Error {
constructor(
public readonly resource: string,
options?: ErrorOptions,
) {
super(`찾을 수 없습니다: ${resource}`, options);
this.name = "NotFoundError";
}
}
async function loadUser(id: string): Promise<{ id: string; name: string }> {
try {
const res = await fetch(`https://api.example.com/users/${id}`);
if (res.status === 404) throw new NotFoundError(`user ${id}`);
return await res.json();
} catch (err) {
if (err instanceof NotFoundError) throw err;
throw new Error("사용자 정보를 불러오지 못했습니다", { cause: err });
}
}instanceof로 사용자 정의 오류를 정확히 가려내려면 target이 ES2015 이상이어야 합니다. ErrorOptions와 cause는 ES2022 라이브러리 타입에 들어 있으므로 target이나 lib을 그에 맞게 설정합니다.
예외는 함수 시그니처에 드러나지 않습니다. 호출하는 쪽은 어떤 함수가 무엇을 던지는지 타입만 보고 알 수 없습니다. 실패가 예상되는 정상적인 경우(입력 검증 등)라면 성공과 실패를 담는 결과 타입을 반환하는 방법이 있습니다.
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseAge(input: string): Result<number, string> {
const age = Number(input);
if (!Number.isInteger(age) || age < 0) {
return { ok: false, error: `올바른 나이가 아닙니다: ${input}` };
}
return { ok: true, value: age };
}
const result = parseAge("42");
if (result.ok) {
console.log(result.value + 1); // 여기서는 value가 있습니다
} else {
console.error(result.error); // 여기서는 error가 있습니다
}ok 속성으로 두 경우가 구분되므로 실패를 확인하지 않고 value를 쓰려 하면 컴파일 오류가 납니다. 예상하지 못한 프로그래밍 오류는 예외로, 예상된 실패는 결과 타입으로 나누어 쓰면 균형이 맞습니다.
Vitest는 TypeScript 파일을 별도 설정 없이 바로 실행하는 테스트 러너입니다. 먼저 테스트할 함수를 준비합니다.
// src/math.ts
export function divide(a: number, b: number): number {
if (b === 0) throw new RangeError("0으로 나눌 수 없습니다");
return a / b;
}// src/math.test.ts
import { describe, it, expect } from "vitest";
import { divide } from "./math.js";
describe("divide", () => {
it("나눗셈 결과를 돌려줍니다", () => {
expect(divide(10, 4)).toBe(2.5);
});
it("0으로 나누면 RangeError를 던집니다", () => {
expect(() => divide(1, 0)).toThrow(RangeError);
});
it("비동기 결과도 검사할 수 있습니다", async () => {
await expect(Promise.resolve(divide(9, 3))).resolves.toBe(3);
});
});npm install --save-dev vitest
npx vitest # 파일이 바뀔 때마다 다시 실행
npx vitest run # 한 번만 실행(CI용)Vitest도 타입은 검사하지 않고 지우기만 합니다. 테스트와 별도로 tsc --noEmit을 실행해야 합니다.
추가 의존성을 줄이고 싶다면 Node.js에 내장된 node:test와 node:assert를 쓸 수 있습니다. TypeScript 파일은 tsx를 로더로 붙여 실행합니다.
// src/math.node.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { divide } from "./math.js";
test("divide는 몫을 계산합니다", () => {
assert.equal(divide(10, 4), 2.5);
});
test("0으로 나누면 RangeError", () => {
assert.throws(() => divide(1, 0), RangeError);
});node --import tsx --test src/math.node.test.tsunknown이므로 instanceof Error 등으로 확인한 뒤 씁니다.Error를 상속한 클래스로 오류 종류를 구분하고, name과 cause로 정보를 남깁니다.{ ok: true } / { ok: false } 형태의 결과 타입으로 표현할 수 있습니다.node:test는 의존성 없이 쓸 수 있습니다.tsc --noEmit을 함께 실행합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.