커스텀 에러 클래스 (Custom Error Classes)

커스텀 에러 클래스 (Custom Error Classes)

자바스크립트에서 에러 처리는 기본 Error 객체로 충분할 때도 있지만, 상황에 따라 더 구체적인 에러를 정의하고 싶을 때가 있다. 이때 커스텀 에러 클래스를 만들면 흐름을 더 명확하게 제어할 수 있다. 이번에는 커스텀 에러 클래스를 어떻게 만들고 활용하는지, 기본부터 심화까지 코드와 함께 자세히 풀어보려고 한다.


커스텀 에러를 잘 다루면 코드가 더 읽기 쉽고 안정적으로 변한다. 하나씩 단계별로 살펴보자.


기본 에러 클래스와의 차이

자바스크립트는 기본적으로 Error 객체를 제공한다. 근데 이건 단순히 메시지만 담고 있어서 구체적인 상황을 표현하기엔 좀 부족하다. 기본 사용법부터 보자:

try {
    throw new Error("뭔가 잘못됐다");
} catch (error) {
    console.log(error.name + ": " + error.message);
} // "Error: 뭔가 잘못됐다"

기본 Error는 이름이 "Error"로 고정돼 있고, 메시지만 커스터마이징할 수 있다. 이걸 확장해서 더 유용하게 만들어보자.


1. 간단한 커스텀 에러 클래스 만들기

Error를 상속받아서 커스텀 에러 클래스를 만들면 된다. 간단한 예시를 보자:

class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
    }
}

try {
    throw new ValidationError("입력값이 잘못됐다");
} catch (error) {
    console.log(error.name + ": " + error.message);
} // "ValidationError: 입력값이 잘못됐다"

extends Error로 상속받고, super로 메시지를 전달한다. name을 바꿔서 에러의 종류를 구체적으로 나타냈다.


2. 추가 속성 넣기

커스텀 에러에 속성을 추가하면 더 많은 정보를 담을 수 있다:

class AuthError extends Error {
    constructor(message, code) {
        super(message);
        this.name = "AuthError";
        this.code = code;
    }
}

try {
    throw new AuthError("로그인 실패", 401);
} catch (error) {
    console.log(error.name + ": " + error.message + ", 코드: " + error.code);
} // "AuthError: 로그인 실패, 코드: 401"

code 속성을 추가해서 에러의 세부 정보를 담았다. 이런 식으로 에러에 맥락을 더할 수 있다.


3. 에러 타입에 따라 분기하기

커스텀 에러를 쓰면 catch에서 에러 타입을 구분해서 다룰 수 있다:

class InputError extends Error {
    constructor(message) {
        super(message);
        this.name = "InputError";
    }
}

class NetworkError extends Error {
    constructor(message) {
        super(message);
        this.name = "NetworkError";
    }
}

function process(value) {
    if (!value) {
        throw new InputError("값이 없다");
    }
    if (value === "network") {
        throw new NetworkError("네트워크 문제");
    }
    console.log("처리 완료: " + value);
}

try {
    process("");
} catch (error) {
    if (error instanceof InputError) {
        console.log("입력 문제: " + error.message);
    } else if (error instanceof NetworkError) {
        console.log("네트워크 문제: " + error.message);
    }
} // "입력 문제: 값이 없다"

instanceof로 에러 타입을 체크해서 상황에 맞게 처리했다. 흐름을 세밀하게 조정할 때 유용하다.


4. 비동기에서 커스텀 에러 활용

비동기 코드에서도 커스텀 에러를 잘 쓸 수 있다:

class FetchError extends Error {
    constructor(message, status) {
        super(message);
        this.name = "FetchError";
        this.status = status;
    }
}

async function getData() {
    try {
        const response = await fetch("https://invalid.url");
        if (!response.ok) {
            throw new FetchError("데이터 가져오기 실패", response.status);
        }
        console.log("데이터 가져오기 성공");
    } catch (error) {
        if (error instanceof FetchError) {
            console.log(error.name + ": " + error.message + ", 상태: " + error.status);
        } else {
            console.log("알 수 없는 에러: " + error.message);
        }
    }
}

getData(); // "FetchError: 데이터 가져오기 실패, 상태: undefined" (URL이 잘못된 경우)

비동기 작업에서 에러를 구체적으로 정의해서 처리했다. status 같은 속성을 추가해서 더 많은 정보를 전달할 수 있다.


5. 계층적인 에러 구조 만들기

에러를 계층적으로 설계하면 복잡한 상황도 체계적으로 다룰 수 있다:

class AppError extends Error {
    constructor(message) {
        super(message);
        this.name = "AppError";
    }
}

class DatabaseError extends AppError {
    constructor(message, query) {
        super(message);
        this.name = "DatabaseError";
        this.query = query;
    }
}

try {
    throw new DatabaseError("쿼리 실패", "SELECT * FROM users");
} catch (error) {
    if (error instanceof DatabaseError) {
        console.log(error.name + ": " + error.message + ", 쿼리: " + error.query);
    } else if (error instanceof AppError) {
        console.log("앱 에러: " + error.message);
    }
} // "DatabaseError: 쿼리 실패, 쿼리: SELECT * FROM users"

AppError를 부모로 두고 DatabaseError를 자식으로 만들어 계층을 나눴다. 이렇게 하면 에러를 체계적으로 분류할 수 있다.


6. 커스텀 에러로 흐름 제어하기

커스텀 에러를 활용하면 복잡한 로직의 흐름도 깔끔하게 정리된다:

class UserError extends Error {
    constructor(message, field) {
        super(message);
        this.name = "UserError";
        this.field = field;
    }
}

function validateUser(user) {
    if (!user.name) {
        throw new UserError("이름이 없다", "name");
    }
    if (user.age < 18) {
        throw new UserError("미성년자다", "age");
    }
    console.log(`유저 확인: ${user.name}, ${user.age}세`);
}

try {
    validateUser({ name: "철수", age: 15 });
} catch (error) {
    console.log(error.name + ": " + error.message + ", 문제 필드: " + error.field);
} // "UserError: 미성년자다, 문제 필드: age"

field 속성을 추가해서 어떤 부분에서 문제가 생겼는지 명확히 했다. 복잡한 검증 로직을 간결하게 처리할 수 있다.


7. 커스텀 에러와 함께 디버깅 정보 제공

에러에 디버깅 정보를 추가하면 문제를 추적하기 쉬워진다:

class ProcessingError extends Error {
    constructor(message, data) {
        super(message);
        this.name = "ProcessingError";
        this.data = data;
        this.timestamp = new Date();
    }
}

try {
    const input = { value: null };
    if (!input.value) {
        throw new ProcessingError("값이 유효하지 않다", input);
    }
} catch (error) {
    console.log(error.name + ": " + error.message);
    console.log("데이터: ", error.data);
    console.log("시간: ", error.timestamp);
} 
// "ProcessingError: 값이 유효하지 않다"
// "데이터: { value: null }"
// "시간: (현재 날짜와 시간)"

datatimestamp를 추가해서 에러 발생 시점과 원인을 추적할 수 있게 했다.


8. 성능과 가독성에 미치는 영향

커스텀 에러 클래스가 성능과 가독성에 어떤 영향을 주는지 보자:

- 성능: 기본 Error와 큰 차이는 없지만, 객체 생성 오버헤드가 조금 더 생길 수 있다. 그래도 안정성과 디버깅 이점이 크다.

- 가독성: 에러를 명확히 구분하고 추가 정보를 담으면 코드가 훨씬 읽기 쉬워진다.

커스텀 속성으로 맥락을 추가하고 계층적 설계로 체계성을 높이는 점이 커스텀 에러의 강점이다.


마무리

커스텀 에러 클래스는 기본 에러를 넘어 더 구체적이고 유연한 에러 처리를 가능하게 한다. 속성 추가, 계층 설계, 비동기 활용 등 다양한 방법으로 흐름을 제어하면 코드가 더 명확해진다.


+ Recent posts