에러 처리와 예외 관리: 타입스크립트 접근법 🚀
안녕, 친구들! 오늘은 우리가 프로그래밍을 하면서 항상 마주치는 골치 아픈 녀석들, 바로 에러와 예외에 대해 얘기해볼 거야. 특히 타입스크립트에서 이 녀석들을 어떻게 다루는지 알아보자고! 🤓
우리가 코드를 짜다 보면 항상 뭔가 잘못될 수 있어. 그게 바로 에러와 예외야. 근데 이런 상황을 잘 관리하면? 우리 프로그램은 더 안정적이고 믿음직스러워질 거야. 마치 재능넷에서 다양한 재능을 안전하게 거래하는 것처럼 말이야!
🔍 잠깐! 알고 가자: 타입스크립트는 자바스크립트의 슈퍼셋이야. 즉, 자바스크립트의 모든 기능을 포함하면서 추가적인 기능을 제공한다는 거지. 그중에서도 타입 안정성이 가장 큰 특징이야. 이게 에러 처리에 어떤 영향을 미칠까? 함께 알아보자!
1. 타입스크립트에서의 에러란? 🤔
자, 먼저 에러가 뭔지부터 확실히 알고 가자. 에러는 우리 프로그램이 예상치 못한 상황에 빠졌을 때 발생해. 예를 들어, 없는 파일을 열려고 한다거나, 0으로 나누려고 할 때 말이야. 타입스크립트에서는 이런 에러를 크게 두 가지로 나눌 수 있어:
- 컴파일 타임 에러: 코드를 실행하기 전에 발견되는 에러야. 타입스크립트의 강력한 타입 체크 덕분에 많은 에러를 미리 잡을 수 있지.
- 런타임 에러: 프로그램이 실행 중일 때 발생하는 에러야. 이건 좀 더 까다로워서 특별한 관리가 필요해.
타입스크립트의 강점은 바로 컴파일 타임에 많은 에러를 잡아낼 수 있다는 거야. 마치 재능넷에서 거래 전에 미리 안전을 체크하는 것처럼 말이야! 😉
2. 타입스크립트의 에러 처리 방식 💪
타입스크립트에서 에러를 처리하는 방법은 크게 세 가지야. 하나씩 살펴보자!
2.1 try-catch 문 사용하기
가장 기본적인 방법이지만, 여전히 강력해. 코드를 try 블록 안에 넣고, 발생할 수 있는 에러를 catch 블록에서 잡는 거야.
try {
// 에러가 발생할 수 있는 코드
const result = someRiskyFunction();
console.log(result);
} catch (error) {
// 에러 처리
console.error("오류 발생:", error.message);
}
이 방식의 장점은 런타임 에러를 우아하게 처리할 수 있다는 거야. 프로그램이 갑자기 멈추는 대신, 우리가 정의한 방식대로 에러를 처리할 수 있지.
2.2 타입 가드 사용하기
타입스크립트의 특별한 기능 중 하나인 타입 가드를 사용하면, 런타임에 타입을 체크하고 그에 따라 다르게 동작하도록 할 수 있어.
function processValue(value: string | number) {
if (typeof value === "string") {
// value는 여기서 string 타입으로 처리됨
console.log(value.toUpperCase());
} else {
// value는 여기서 number 타입으로 처리됨
console.log(value.toFixed(2));
}
}
타입 가드를 사용하면 컴파일 타임에 많은 에러를 방지할 수 있어. 이건 마치 재능넷에서 서비스를 이용하기 전에 사용자의 자격을 확인하는 것과 비슷해!
2.3 사용자 정의 에러 만들기
때로는 우리만의 특별한 에러가 필요할 때가 있어. 타입스크립트에서는 Error 클래스를 상속받아 커스텀 에러를 만들 수 있지.
class CustomError extends Error {
constructor(message: string) {
super(message);
this.name = "CustomError";
}
}
try {
throw new CustomError("이런, 뭔가 잘못됐어!");
} catch (error) {
if (error instanceof CustomError) {
console.log("커스텀 에러 발생:", error.message);
} else {
console.log("알 수 없는 에러 발생");
}
}
사용자 정의 에러를 사용하면 더 구체적이고 의미 있는 에러 처리가 가능해져. 이렇게 하면 문제가 발생했을 때 더 빠르고 정확하게 대응할 수 있지!
💡 Pro Tip: 에러 처리는 단순히 문제를 잡아내는 것뿐만 아니라, 그 문제를 어떻게 해결할지에 대한 전략이기도 해. 항상 "이 에러가 발생하면 어떻게 대응해야 할까?"를 고민해봐!
3. 타입스크립트의 고급 에러 처리 기법 🧠
자, 이제 좀 더 깊이 들어가볼까? 타입스크립트에서 사용할 수 있는 몇 가지 고급 에러 처리 기법을 알아보자!
3.1 Union Types를 활용한 에러 처리
타입스크립트의 Union Types를 사용하면 함수가 반환할 수 있는 여러 타입을 명시적으로 정의할 수 있어. 이를 통해 에러 상황을 타입 시스템 내에서 표현할 수 있지.
type Result<t> = { success: true; value: T } | { success: false; error: string };
function divide(a: number, b: number): Result<number> {
if (b === 0) {
return { success: false, error: "0으로 나눌 수 없어요!" };
}
return { success: true, value: a / b };
}
const result = divide(10, 2);
if (result.success) {
console.log("결과:", result.value);
} else {
console.error("에러:", result.error);
}
</number></t>
이 방식을 사용하면 컴파일러가 모든 가능한 결과를 처리했는지 확인할 수 있어. 마치 재능넷에서 모든 가능한 거래 결과를 미리 정의해두는 것과 비슷하지!
3.2 제네릭을 활용한 유연한 에러 처리
제네릭을 사용하면 다양한 타입에 대해 재사용 가능한 에러 처리 로직을 만들 수 있어.
class Result<t e> {
private constructor(private value: T | null, private error: E | null) {}
static ok<t>(value: T): Result<t never> {
return new Result(value, null);
}
static err<e>(error: E): Result<never e> {
return new Result(null, error);
}
unwrap(): T {
if (this.error) {
throw this.error;
}
return this.value as T;
}
}
function safeOperation(): Result<number string> {
// 실제 연산 수행
if (Math.random() > 0.5) {
return Result.ok(42);
} else {
return Result.err("연산 실패!");
}
}
const result = safeOperation();
try {
const value = result.unwrap();
console.log("성공:", value);
} catch (error) {
console.error("실패:", error);
}
</number></never></e></t></t></t>
이 패턴을 사용하면 에러 처리를 타입 안전하게 할 수 있어. 컴파일러가 우리가 모든 가능한 결과를 처리했는지 확인해주니까, 런타임 에러의 위험을 크게 줄일 수 있지!
3.3 데코레이터를 활용한 에러 로깅
타입스크립트의 실험적 기능인 데코레이터를 사용하면, 메서드에 자동으로 에러 처리 로직을 추가할 수 있어.
function logError(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
try {
return originalMethod.apply(this, args);
} catch (error) {
console.error(`Error in ${propertyKey}:`, error);
throw error;
}
};
return descriptor;
}
class Calculator {
@logError
divide(a: number, b: number): number {
if (b === 0) throw new Error("0으로 나눌 수 없어요!");
return a / b;
}
}
const calc = new Calculator();
try {
calc.divide(10, 0);
} catch (error) {
console.log("에러가 잘 처리되었습니다.");
}
데코레이터를 사용하면 반복적인 에러 처리 코드를 줄이고, 관심사를 분리할 수 있어. 이는 코드의 가독성과 유지보수성을 크게 향상시키지!
🌟 Remember: 에러 처리는 단순히 프로그램의 충돌을 방지하는 것 이상의 의미가 있어. 잘 설계된 에러 처리 시스템은 프로그램의 안정성을 높이고, 디버깅을 쉽게 만들며, 사용자 경험을 개선해. 마치 재능넷이 안전하고 신뢰할 수 있는 거래 환경을 제공하는 것처럼 말이야!
4. 실전 예제: 타입스크립트로 안전한 API 호출 만들기 🌐
자, 이제 우리가 배운 내용을 실제로 적용해볼 차례야! API 호출은 많은 웹 애플리케이션에서 중요한 부분이지만, 동시에 많은 에러의 원인이 되기도 해. 타입스크립트를 사용해 어떻게 안전한 API 호출을 만들 수 있는지 살펴보자.
4.1 기본적인 API 호출 함수
먼저, fetch를 사용해 기본적인 API 호출 함수를 만들어볼게.
async function fetchData<t>(url: string): Promise<t> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
</t></t>
이 함수는 제네릭을 사용해 다양한 타입의 데이터를 처리할 수 있어. 하지만 아직 에러 처리가 충분하지 않아. 개선해볼까?
4.2 에러 타입 정의하기
API 호출 중 발생할 수 있는 다양한 에러 타입을 정의해보자.
type NetworkError = {
type: 'NetworkError';
message: string;
};
type ApiError = {
type: 'ApiError';
statusCode: number;
message: string;
};
type ParseError = {
type: 'ParseError';
message: string;
};
type AppError = NetworkError | ApiError | ParseError;
이렇게 에러 타입을 명확히 정의하면, 각 에러 상황에 대해 더 구체적으로 대응할 수 있어.
4.3 Result 타입 사용하기
이제 앞서 배운 Result 타입을 활용해 API 호출 함수를 개선해보자.
type Result<t> = { ok: true; value: T } | { ok: false; error: AppError };
async function fetchData<t>(url: string): Promise<result>> {
try {
const response = await fetch(url);
if (!response.ok) {
return {
ok: false,
error: {
type: 'ApiError',
statusCode: response.status,
message: `HTTP error! status: ${response.status}`
}
};
}
const data = await response.json();
return { ok: true, value: data };
} catch (error) {
if (error instanceof TypeError) {
return {
ok: false,
error: {
type: 'NetworkError',
message: 'Network error occurred'
}
};
}
return {
ok: false,
error: {
type: 'ParseError',
message: 'Failed to parse JSON'
}
};
}
}
</result></t></t>
이제 이 함수는 성공과 실패의 경우를 명확히 구분하고, 각 에러 상황에 대해 구체적인 정보를 제공해. 이는 마치 재능넷에서 거래 과정의 각 단계마다 상세한 상태 정보를 제공하는 것과 비슷해!
4.4 API 호출 사용 예제
이제 우리가 만든 안전한 API 호출 함수를 사용해보자.
interface User {
id: number;
name: string;
email: string;
}
async function getUser(id: number) {
const result = await fetchData<user>(`https://api.example.com/users/${id}`);
if (result.ok) {
console.log('User:', result.value);
} else {
switch (result.error.type) {
case 'NetworkError':
console.error('네트워크 에러:', result.error.message);
break;
case 'ApiError':
console.error(`API 에러 (${result.error.statusCode}):`, result.error.message);
break;
case 'ParseError':
console.error('파싱 에러:', result.error.message);
break;
}
}
}
getUser(1);
</user>
이 방식을 사용하면 모든 가능한 에러 상황을 빠짐없이 처리할 수 있어. 컴파일러가 우리가 모든 에러 타입을 처리했는지 확인해주니까, 예상치 못한 런타임 에러의 위험을 크게 줄일 수 있지!
🚀 Pro Tip: 실제 프로젝트에서는 이런 에러 처리 로직을 재사용 가능한 유틸리티 함수나 클래스로 만들어 사용하면 좋아. 이렇게 하면 애플리케이션 전체에서 일관된 방식으로 에러를 처리할 수 있고, 코드 중복도 줄일 수 있지!
5. 타입스크립트 에러 처리의 모범 사례 🏆
자, 이제 우리가 배운 내용을 바탕으로 타입스크립트에서 에러를 처리할 때 따르면 좋을 몇 가지 모범 사례를 정리해볼게.
5.1 명시적인 에러 타입 사용하기
가능한 한 `any` 타입 대신 구체적인 에러 타입을 사용해. 이렇게 하면 컴파일러가 더 많은 도움을 줄 수 있어.
// 좋지 않은 예
function riskyOperation(): any {
// ...
}
// 좋은 예
type OperationError = {
code: number;
message: string;
};
function riskyOperation(): string | OperationError {
// ...
}
명시적인 에러 타입을 사용하면 코드의 가독성이 높아지고, 에러 처리가 더 안전해져. 마치 재능넷에서 각 거래 상태를 명확히 정의하는 것과 같아!
5.2 비동기 코드에서의 에러 처리
Promise를 사용할 때는 항상 catch를 사용해 에러를 처리해. async/await를 사용할 때는 try-catch 블록을 활용하자.
// Promise 사용 시
fetchData()
.then(data => console.log(data))
.catch(error => console.error('에러 발생:', error));
// async/await 사용 시
async function getData() {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error('에러 발생:', error);
}
}
비동기 코드에서의 에러 처리는 특히 중요해. 놓치기 쉽지만, 제대로 처리하지 않으면 프로그램 전체가 멈출 수 있으니까!
5.3 에러 객체 확장하기
기본 Error 객체를 확장해 더 많은 정보를 포함하는 커스텀 에러를 만들어 사용해.
class DatabaseError extends Error {
constructor(
message: string,
public readonly code: number,
public readonly query: string
) {
super(message);
this.name = 'DatabaseError';
}
}
try {
throw new DatabaseError('쿼리 실행 실패', 500, 'SELECT * FROM users');
} catch (error) {
if (error instanceof DatabaseError) {
console.error(`DB 에러 (${error.code}): ${error.message}`);
console.error('실행된 쿼리:', error.query);
}
}
커스텀 에러 객체를 사용하면 에러와 관련된 더 많은 컨텍스트 정보를 제공할 수 있어. 이는 디버깅과 로깅에 매우 유용하지!
5.4 타입 가드 활용하기
여러 타입의 에러를 처리할 때는 타입 가드를 사용해 각 에러 타입에 맞는 처리를 할 수 있어.
type NetworkError = { kind: 'NetworkError', message: string };
type ValidationError = { kind: 'ValidationError', errors: string[] };
type AppError = NetworkError | ValidationError;
function handleError(error: AppError) {
if (error.kind === 'NetworkError') {
console.error('네트워크 에러:', error.message);
} else {
console.error('유효성 검사 에러:', error.errors.join(', '));
}
}
타입 가드를 사용하면 컴파일러가 각 분기에서의 정확한 타입을 알 수 있어, 타입 안전성이 높아져. 이는 마치 재능넷에서 각 사용자의 역할에 따라 다른 기능을 제공하는 것과 비슷해!
5.5 에러 처리를 위한 유틸리티 함수 만들기
자주 사용되는 에러 처리 패턴은 유틸리티 함수로 만들어 재사용하자.
function tryCatch<t>(fn: () => T, errorHandler: (error: unknown) => void): T | undefined {
try {
return fn();
} catch (error) {
errorHandler(error);
return undefined;
}
}
const result = tryCatch(
() => JSON.parse('{"name": "John"}'),
(error) => console.error('JSON 파싱 에러:', error)
);
console.log(result); // { name: 'John' } 또는 undefined
</t>