커스텀 에러(Custom Error)에 대해서 공부해보기

해니니·2024년 11월 23일
0
post-thumbnail

정말 개발 공부의 끝은 없는 것 같다.
예전에 퍼블리셔로 회사를 다녔을 때, 에러 처리를 어떻게 해결했었나 되돌아보면, 에러가 어디서 발생했는지 알 수 없어 코드의 여기저기를 뒤지며 수정했던 기억이 난다.
때로는 코드를 vs code에 깃렌즈 익스텐션을 깔아가며, 코드를 작성한 사람을 찾아가 직접 물어보기도 했던 경험도 있었다..!

프론트를 공부하면서 400, 403, 404, 500 같은 HTTP 오류 코드가 점점 익숙해지던 중, 커스텀 에러를 활용하면 더욱 체계적으로 다양한 에러를 관리할 수 있다는 사실을 알게 되었다. 그래서 이 글을 한번 써보고자 한다.✍️


커스텀 에러란?

프로그래밍에서 커스텀 에러(Custom Error)란 기본 제공되는 Error 객체를 확장하여 특정한 에러 상황에 맞게 설계된 에러 클래스다.

기본 Error 객체는 단순히 메시지와 스택 정보를 제공하지만, 구체적인 상황을 표현하기 어렵다.
커스텀 에러는 에러의 종류를 명확히 구분하고, 상황에 맞는 메시지를 전달하며, 디버깅과 유지보수성을 높여준다.

기본 Error 객체와 커스텀 에러의 차이

간단하게 기본 에러 객체와 커스텀 에러를 간단하게 아래와 같이 확인해볼 수 있다.

기본 Error 객체를 사용한 예시

여기서 에러 메시지는 "에러가 발생했습니다"로 모호하다.
이 에러가 API 키 누락 때문인지, 네트워크 오류 때문인지 명확히 구분할 방법이 없다.

function fetchData(apiKey) {
  if (!apiKey) {
    throw new Error('에러가 발생했습니다.');
  }
}

try {
  fetchData(null);
} catch (error) {
  console.error(error.message); // "에러가 발생했습니다."
}

커스텀 에러를 사용한 예시

name 속성으로 에러의 종류를 명확히 구분할 수 있다.
에러 메시지는 상황에 맞게 작성되어 디버깅과 유지보수가 훨씬 쉬워진다.

class ApiKeyMissingError extends Error {
  constructor() {
    super('API 키가 설정되지 않았습니다.');
    this.name = 'ApiKeyMissingError';
  }
}

function fetchData(apiKey) {
  if (!apiKey) {
    throw new ApiKeyMissingError();
  }
}

try {
  fetchData(null);
} catch (error) {
  console.error(error.name); // "ApiKeyMissingError"
  console.error(error.message); // "API 키가 설정되지 않았습니다."
}

커스텀 에러의 장점

커스텀 에러를 사용하면 좋은 이유를 먼저, 더 자세하게 알아보자면 아래와 같이 이야기할 수 있다.

  • 에러 상황의 명확한 표현
    단순히 "에러가 발생했습니다" 같은 메시지가 아니라, 어떤 상황에서 에러가 발생했는지 구체적으로 설명할 수 있다.

  • 코드 가독성 및 유지보수성 향상
    커스텀 에러를 사용하면 에러가 발생한 맥락과 원인을 한눈에 파악할 수 있다. 이는 디버깅과 유지보수에서 큰 이점을 제공한다.

  • 에러 처리의 일관성 제공
    에러를 일관되게 정의하고 처리하면 코드의 통일성과 안정성이 높아진다. 특히, 팀 단위로 작업할 때 커스텀 에러는 통일된 에러 핸들링 방식을 제공한다.

  • 재사용 가능한 에러 정의
    커스텀 에러 클래스는 재사용 가능하기 때문에, 프로젝트 전반에서 동일한 유형의 에러를 일관되게 관리할 수 있다.


커스텀 에러 만들어보기

커스텀 에러는 기본 Error 객체를 상속받아 작성한다. 이렇게 하면 기본 Error 객체의 기능을 활용하면서도 에러의 종류를 구체적으로 정의할 수 있다.

아래는 커스텀 에러 클래스의 기본 구조다.

class CustomError extends Error {
  constructor(message: string) {
    super(message); // 부모 클래스의 message 설정
    this.name = 'CustomError'; // 에러의 이름 설정
  }
}

name 속성과 메시지의 역할

  • name: name은 에러의 종류를 나타낸다. 예를 들어, "CustomError", "ValidationError", "ApiRequestError"처럼 에러를 명확히 구분할 수 있다. 이를 통해 개발자는 발생한 에러가 어떤 종류인지 쉽게 알 수 있다.
  • message: 에러가 발생한 상황을 구체적으로 설명하는 문자열이다. 디버깅 과정에서 매우 유용하며, 개발자나 사용자에게 에러의 원인을 직관적으로 전달한다.

실제 코드로 배우는 커스텀 에러

예제를 통해 특정한 상황에 맞는 커스텀 에러를 작성하는 방법을 알아보자!☺️
아래는 내가 실제로 개인 프로젝트를 진행하면서 썼던 코드들이다.

API 키가 누락된 경우 (ApiKeyMissingError)

export class ApiKeyMissingError extends Error {
  constructor(message = 'API 키가 설정되지 않았습니다.') {
    super(message);
    this.name = 'ApiKeyMissingError';
  }
}

환경 변수나 설정 파일에 API 키가 누락된 경우를 처리하기 위해 사용된다.
외부 API 호출에 필요한 키가 설정되지 않은 경우, 이 에러를 발생시켜 문제를 알릴 수 있다.

API 요청 실패 (ApiRequestError)

export class ApiRequestError extends Error {
  constructor(message = 'API 요청에 실패했습니다.') {
    super(message);
    this.name = 'ApiRequestError';
  }
}

외부 API에서 오류가 발생하거나 응답 상태 코드가 200이 아닌 경우 이 에러를 사용한다.
예를 들어, 서버 에러(500)나 잘못된 요청(400)을 처리할 때 유용하다.

XML 응답을 파싱하다가 실패한 경우 (ApiResponseParsingError)

export class ApiResponseParsingError extends Error {
  constructor(message = 'API 응답을 파싱하는 중 오류가 발생했습니다.') {
    super(message);
    this.name = 'ApiResponseParsingError';
  }
}

API에서 받은 XML 데이터를 JSON으로 변환하는 과정에서 오류가 발생했을 때 사용한다.
예를 들어, API 응답 데이터가 형식에 맞지 않거나 파싱 로직에 문제가 있을 경우 이 에러를 발생시킨다.


에러 메시지 관리

에러 메시지를 상수화하면 코드의 재사용성과 가독성이 높아진다. 아래는 에러 메시지를 별도의 상수 파일에서 관리하는 예시다.

export const SEARCH_ERROR_MESSAGES = {
  API_KEY_MISSING: 'API 키가 설정되지 않았습니다.',
  API_REQUEST_ERROR: 'API 요청에 실패했습니다.',
  API_RESPONSE_PARSING_ERROR: 'API 응답을 파싱하는 중 오류가 발생했습니다.',
};

이렇게 하면 에러 메시지를 수정하거나 다국어를 지원할 때 매우 편리하다.


커스텀 에러 처리

커스텀 에러를 처리할 때는 에러 타입별로 로직을 분리할 수 있다. 아래는 handleError 함수의 예시다. 메세지는 위에 상수화했던 코드를 가져와서 변경해줄 수 있다.

function handleError(error: unknown) {
  if (error instanceof ApiKeyMissingError) {
    return { message: SEARCH_ERROR_MESSAGES.API_KEY_MISSING, status: 500 };
  }

  if (error instanceof ApiRequestError) {
    return { message: SEARCH_ERROR_MESSAGES.API_REQUEST_ERROR, status: 502 };
  }

  if (error instanceof ApiResponseParsingError) {
    return { message: SEARCH_ERROR_MESSAGES.API_RESPONSE_PARSING_ERROR, status: 500 };
  }

  // 기타 에러 처리
  return { message: '알 수 없는 오류가 발생했습니다.', status: 500 };
}

커스텀 에러를 올바르게 사용하는 방법과 주의점

  • 주요 에러 상황에 맞게 에러를 세분화하기
    모든 에러를 하나의 Error 객체로 처리하기보다는, 주요 에러 상황에 맞게 커스텀 에러를 정의하는 것이 좋다.
  • 명확한 이름과 메시지를 사용하기
    커스텀 에러의 이름은 에러 상황을 직관적으로 나타내야 하고, 메시지는 문제를 파악할 수 있도록 구체적이어야 한다.
    특히 사용자에게 노출될 가능성이 있는 메시지라면, 민감한 정보를 포함하지 않고 친절한 표현을 사용하는 것이 좋다.
  • 에러 메시지를 상수로 관리하기
    에러 메시지를 코드에 하드코딩하는 대신, 상수 파일로 관리하는 것이 좋다.
    이렇게 하면 유지보수가 쉬워지고, 다국어 지원 같은 확장 작업도 간단해진다.
export const ERROR_MESSAGES = {
  API_KEY_MISSING: 'API 키가 설정되지 않았습니다.',
  API_REQUEST_ERROR: 'API 요청에 실패했습니다.',
  API_RESPONSE_PARSING_ERROR: 'API 응답을 파싱하는 중 오류가 발생했습니다.',
};
  • 스택 추적과 디버깅을 고려하기
    커스텀 에러를 작성할 때는 반드시 super(message)를 호출해 스택 추적(Stack Trace)을 활성화하는 것이 중요하다.
    예를 들어, 요청 상태 코드나 API URL을 포함한 커스텀 에러를 설계할 수 있다.
class ApiRequestError extends Error {
  statusCode: number;

  constructor(message: string, statusCode: number) {
    super(message); // 스택 추적 활성화
    this.name = 'ApiRequestError';
    this.statusCode = statusCode; // 추가 정보 포함
  }
}
  • 사용자 경험(UX)을 고려하기
    커스텀 에러는 사용자에게 노출될 가능성이 있는 메시지에 대해 신중하게 설계하는 것이 중요하다.
    "서버 오류가 발생했습니다"보다는 "일시적인 문제가 발생했습니다. 잠시 후 다시 시도해주세요" 같은 표현이 더 적합하다.
    민감한 데이터나 내부 정보를 포함한 에러 메시지는 절대 사용자에게 노출되지 않도록 주의해야 한다.

결론

커스텀 에러는 복잡한 애플리케이션의 에러 처리를 체계적이고 명확하게 만들어주는 강력한 도구다. 프로젝트에서 발생할 수 있는 주요 에러 상황을 미리 정의하고, 이를 일관성 있게 처리하면 코드의 품질과 유지보수성이 크게 향상된다.

공부를 하면 할수록 느끼는 것은, 코드의 품질을 높이는 방법뿐만 아니라 팀원들과의 협업을 위해 내 코드가 남들에게도 이해하기 쉽게 작성되는 방법을 배워간다는 점이다. 내가 없어도 동료들이 내 코드를 쉽게 파악하고 유지보수할 수 있는 구조를 만드는 것이 개발자로서 성장의 중요한 부분임을 깨닫는다.
남들에게도 편하지만 내가 휴가를 가더라도 불안하지 않고 당당하게 다녀올 수 있는 계기가 되는 것 같기도 하다.😊

profile
Front.Dev 연습생

0개의 댓글

관련 채용 정보