Error Handler 개발기

Sian·2022년 5월 31일
3

SpaceONE FrontEnd 개발

목록 보기
6/6

기존 프로젝트는 에러가 발생했을 때, console.error로 에러를 콘솔에 찍어서 확인용으로만 작업하였다. 이렇게 조악하게 에러 처리를 하다보니 전역적인 핸들링이 필요한 경우 등에 대처할 수 없고 중복 코드가 양산되었다. 따라서 Error Handler를 개발했는데, 까먹기 전에 그 개발기를 적어보고자 한다.

목차

  • 에러 구분에 따른 처리 (API)
  • Error Handler (App Client)
  • Error Handler 사용 예

에러 구분에 따른 처리 (API)

다양한 API Error를 처리하기 위해 미리 에러 타입을 정의한다.

  1. NotFoundError : 리소스를 찾을 수 없을 때 (status: 404)
  2. BadRequestError : 잘못된 요청을 보낼 때 (status: 400)
  3. AuthenticationError: 인증이 실패했을 때 (status: 401)
  4. AuthorizationError: 권한 검증에 실패했을 때 (status: 403)
  5. APIError: 이외의 모든 API Error 및 서버 에러 (status: 500 등)

이 에러들을 axios api client의 interceptor에서 받아 핸들링한다.
이외에 다른 에러 코드를 정의할 수도 있을 것이다.

위의 에러는 에러 클래스를 따로 만들어 다음과 같이 정의한다.

export class APIError extends Error {
	status?: number;

	code?: string;

	axiosError?: AxiosError;

	constructor(axiosError: AxiosError) {
	    super();
	    this.name = RESPONSE.API_ERROR;
	    this.status = 500;
	    this.code = 'ERROR_UNKNOWN';
	    this.axiosError = axiosError;

	    if (axiosError.response) {
	        this.status = axiosError.response.status;

	        if (axiosError.response.data.error) {
	            this.message = axiosError.response.data.error.message;
	            this.code = axiosError.response.data.error.code;
	        } else {
	            this.message = axiosError.response.statusText;
	        }
	    } else {
	        this.message = axiosError.message;
	    }
	}
}

우선 기본이 되는 APIError 클래스를 만든다. (더 좋은 코드가 있을 수도 있고, 각자 백엔드와 합의된 에러 message와 status, code가 다를 것이다.)

그 후

export class AuthenticationError extends APIError {
	axiosError: AxiosError;

	constructor(axiosError: AxiosError) {
	    super(axiosError);
	    this.name = RESPONSE.AUTHENTICATION_ERROR;
	    this.axiosError = axiosError;
	}
}

...(생략)

export const isInstanceOfAuthenticationError = (e: unknown): e is AuthenticationError => e instanceof AuthenticationError;

APIError를 상속받아 다양한 에러 클래스를 구현하고, 에러를 판단하는 함수를 만든다.

그 후, axios의 interceptor에서 error가 왔을 때, 각 statusCode를 보고 판별하여 위에 미리 정의해둔 Error로 throw해준다.

ex)

private handleRequestError = (error: AxiosError) => {
     switch (error.response?.status) {
        ...생략
        case 400: {
             throw new BadRequestError(error);
        }
  		...
	}
}
...
// Axios response interceptor with error handling
this.instance.interceptors.response.use(
     (response: AxiosResponse) => response, (error) => Promise.reject(this.handleRequestError(error)));

이 때, 401이 오는 경우에는 token을 한 번 refresh 해주고, refresh에 실패하면 (access token이 만료된 경우) Authentication error를 throw한다.

API에서 발생한 에러가 아닌 경우, Client 측에서 따로 위와 같은 에러 클래스를 또 만들어서 catch문에서 해당 에러를 핸들링할 수 있도록 한다.(ex. Client 내부 리소스를 못 찾는 경우 등등)

Error Handler (Client)

API client를 통해 넘어온 Error를 이제 Client 쪽에서 핸들링해주는 방법에 대해 알아보자.

throw된 error의 유형을 판단하여 다음과 같이 핸들링한다.

  1. AuthenticationError의 경우

Token이 살아있는 지 확인하고, 현재 route가 authenticated level이면 authentication error handler를 호출한다. Authentication Error Handler는 다음과 같은 동작을 한다.

  • Session 만료 모달을 보여줌 (진행하는 프로젝트에서는, store를 사용해 modal을 보여주는 action을 dispatch하였다.)
  • User의 Session을 만료시킴
  1. AuthorizationError의 경우

Authorization Error Handler를 호출한다. 이 핸들러는 다음과 같은 동작을 한다.

  • 상단에 권한이 없다는 경고창을 노출시킴 (진행하는 프로젝트에서는, store를 사용해 상단바를 보여주는 action을 dispatch하였다.)
  1. NoResourceError의 경우

No Resource라는 토스트를 보여주고, redirect url이 있을 시 해당 url로 이동시킨다.

  1. APIError의 경우

콘솔창에 API Error라고 명시된 로그를 남긴다.

  1. BadRequestError의 경우

각 페이지에서 필요한 경고 메세지를 보여주어야하는 경우가 있다. (400 bad request가 보통 이에 해당한다.)
페이지에서 넘겨준 Error Message가 적힌 toast 경고창을 띄운다.

  1. APIError의 경우

관리자에게 문의하라는 문구가 적힌 toast 경고창을 띄운다.

위와 같은 핸들링은 다음과 같이 구현한다. (각 프로젝트마다 컴포넌트나 store 사용 방식 등이 다를 것이다.)

export default class ErrorHandler {
  handleError(error) {
    if (isInstanceOfAuthenticationError(error)) {
        // refresh token, session 만료 모달 보여주기 등
    }
    else if (isInstanceOfAuthorizationError(error)) {
    	// store.dispatch('error/showAuthorizationError'); 등 각자 필요한 UI 보여주기 
    }
   
   ... 생략
 }
  
   handleErrorWithMsg(error, errorMessage: TranslateResult) {
	...
    if (isInstanceOfBadRequestError(error) && errorMessage) showErrorMessage(errorMessage, error);
          ...
    }
}

매우 간략하게 작성하였지만 위와 같은 코드를 통해 전역적으로 같은 방식으로 에러를 핸들링할 수 있다.

Error Handler 사용 예

에러 클래스를 작성하고, 그 에러 클래스를 이용해 만든 에러 핸들러를 실제 프로젝트에서는 아래와 같이 사용한다.

  1. list 등 일반적인 작업
try {
	특정 동작
} catch (e) {
	ErrorHandler.handleError(e);
}
  1. create/update 등 실패 시 toast message를 보여주어야 하는 작업
try {
	특정 동작
} catch (e) {
	ErrorHandler.handleErrorWithMsg(e, 번역을 위한 i18n key값 or string);
}

기존에는

try {
  특정 동작
} catch (e) {
  console.error(e); 
}

위와 같이 작성하던 에러 핸들링에서 나름대로 많이 발전한 것 같다.

클라이언트에 이렇게 에러 핸들링을 하는 방식 외에도 ErrorBoundary를 만드는 방식도 있는데, 추후 리액트 프로젝트에 적용했던 ErrorBoundary에 대한 내용도 포스팅해야겠다.

0개의 댓글