728x90

이 장에서는 타입스크립트에 직결되는 느낌보다는, 배민팀이 API 사용시 에러 핸들링과 모킹을 어떻게 했는지에 대한 내용입니다. 즉, 배민팀에 코드에 관심이 있다면 교재를 구매해서 보는 것이 효율적일 것이라 생각하고,, 어떠한 방법들을 사용했는지 정도만 저는 기록하였습니다.

 

API 에러 핸들링

비동기 API 호출을 하다 보면 다양한 에러가 발생할 수 있다. 이를 타입스크립트에서 구체적이고 명시적으로 핸들링하는 방법을 알아보자

1. 타입가드

Axios에서는 Axios에 대해 isAxiosError라는 타입가드를 제공한다. 이 타입가드를 가공해서 , 서버 에러임을 명확하게 표시하고 서버에서 내려주는 에러 응답 객체에 대해서도 구체적으로 정의함으로써 에러 객체가 어떤 속성을 가졌는지 파악할 수 있다.

 

// 공통에러에 대한 정의
import axios, { AxiosError } from 'axios';

interface ErrorResponse {
  status: string;
  serverDateTime: string;
  errorCode: string;
  errorMessage: string;
}

function isServerError(error: unknown): error is AxiosError<ErrorResponse> {
  return axios.isAxiosError(error);
}

2. 에러 서브 클래싱하기

서브 클래싱이란 기존 클래스를 확장하여 새로운 클래스를 만드는 과정이다.

단순한 서버 에러말고 인증 정보 에러, 네트워크 에러등 다양한 에러를 명시적으로 표시하기 위해 서브클래싱을 사용할 수 있다. 서브클래싱을  활용하면 에러가 발생했을 때 코스당에서 어떤 에러인지를 바로 확인할 수 있고 에러 인스턴스에 따라 처리 방식을 다르게 구현할 수 있다.

 

class OrderHttpError extends Error {
  private readonly privateResponse: AxiosResponse<ErrorResponse> | undefined;

  constructor(message?: string, response?: AxiosResponse<ErrorResponse>) {
    super(message);
    this.name = 'OrderHttpError';
    this.privateResponse = response;
  }

  get response(): AxiosResponse<ErrorResponse> | undefined {
    return this.privateResponse;
  }
}

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

class UnauthorizedError extends Error {
  constructor(message: string, response?: AxiosResponse<ErrorResponse>) {
    super(message);
    this.name = 'UnauthorizedError';
  }
}

 

 

 

const httpErrorHandler = (
  error: AxiosError<ErrorResponse> | Error
): Promise<Error> => {
  let promiseError: Promise<Error>;

  if (axios.isAxiosError(error)) {
    if (Object.is(error.code, 'ECONNABORTED')) {
      promiseError = Promise.reject(new TimeoutError());
    } else if (Object.is(error.code, 'Network Error')) {
      promiseError = Promise.reject(new NetworError());
    } else {
      const { response } = error as AxiosError<ErrorResponse>;

      switch (response?.status) {
        case HttpStatusCode.Unauthorized:
          promiseError = Promise.reject(
            new UnauthorizedError(response?.data.errorMessage, response)
          );
          break;
        default:
          promiseError = Promise.reject(
            new OrderHttpError(response?.data.errorMessage, response)
          );
      }
    }
  } else {
    promiseError = Promise.reject(error);
  }
  return promiseError;
};

이후 error instance of OrderHttpError와 같이 작성된 타입 가드문을 통해 코드상에서 에러핸들링에 대한 부분을 한눈에 볼 수 있게 만들 수 있다.

const onUnauthorizedError = (message: string, callback?: () => void) => {
  console.error(`Unauthorized Error: ${message}`);
  if (callback) {
    callback();
  }
};
const onActionError = (
  error: unknown,
  params?: Omit<AlertPopup, 'type' | 'message'>
) => {
  if (error instanceof UnauthorizedError) {
    onUnauthorizedError(error.message);
  } else if (error instanceof NetworkError) {
    // ...
    alert('내트워크 연결이 이상합니다.');
  }
};

 

3. Axios 인터셉터를 활용한 에러처리

const httpErrorHanlder = (
  error: AxiosError<ErrorResponse> | Error
): Promise<Error> => {
  (error) => {
    if (error.response && error.response.status === 401) {
      window.location.href = `${backofficeAuthHost}`;
    }
    return Promise.reject(error);
  };
};

orderApiRequester.interceptors.response.use(
  (response: AxiosResponse) => response,
  httpErrorHandler
);

응답 시에 인터셉터를 통해 처리 가능하다.

 

4. 에러 바운더리를 활용한 에러처리

에러 바운더리는 리액트 컴포넌트 트리에서 에러가 발생할 때 공통으로 에러를 처리하는 리액트 컴퍼넌트이다. 에러 바운더리는 에러가 발생한 컴퍼넌트 대신에 에러 처리를 하거나 예상치 못한 에러를 공통 처리할 떄 사용할 수 있다.

 

5. 상태 관리 라이브러리에서 에러 처리

6. react-query 를 활용한 에러 처리

요청에 대한 상태를 반환해 주기 때문에 요청 상태를 확인하기 쉽다.

 

7. 그 밖의 에러처리

커스텀 에러를 만들어서 처리할 수도 있다.

 

API 모킹

서버 API가 완성되기 전에 가짜 모듈을 활용하는 것을 모킹이라고 한다.

모킹의 사용 예시로는

  1. JSON 파일 불러오기
    간단한  경우 사용하는 방법. 
  2. NextApiHandler 활용하기
    Next.js에 존재함. 응답 처리 로직도 추가 가능
  3. API 요청 핸들러에 분기 추가하기
    분기처리를 통해서 필요할 떄에만 실제 요청을 보낼 수 있다. 이 방법은 개발 이후에도 쓸 수 있으니, 모든 api요청에 if 분기문을 추가해야하므로 번거로울 수 있다.

    const mockFetchBrands = (): Promise<FetchBrandsResponse> =>
      new Promise((resolve) => {
        setTimeout(() => {
          resolve({
            status: 'SUCCESS',
            message: null,
            data: [
              {
                id: 1,
                label: '배민스토어',
              },
              {
                id: 2,
                label: '비마트',
              },
            ],
          });
        }, 500);
      });
    
    const fetchBrands = () =>{
        if(useMock){
            return mockFetchBrands();
        }
        return requester.get("/brands")
    }
  4. axios-mock-adapter로 모킹하기
    서비스에 분기문이 추가되는 것을 바라지 않는다면 ,axios-mock-adapter 라이브러리를 사용하면 된다. 해당 라이브러리는 Axios 요청을 가로채서 요청에 대한 응답 값을 대신 반환한다.
    에러 및 HTTP 메서드에 대한 목업도 작성 가능하다.
  5. 목업 사용 여부 제어하기
    플래그를 사용하여 목업으로 개발할 떄와 개발하지 않을 때를 구분할 수 있다.

const useMock = Object.is(REACT_APP_MOCK, 'true');
const mockFn = ({status=200,time=100,use=true}:MockResult) => use &&
    mock.onGet(/\/order\/list/).reply(()=>
    new Promise((resolve)=>
    setTimeout(()=>{
        resolve([
            status,
            status ===200? fetchOrderListSuccessResponse : undefined,
        ]);
    },time)
    )

    if(useMock){
        mockFn({status:200,time:100,use:true})
    }
)

위 처럼 플래그에 따라 mockFN을 제어할 수 있는데, 매개변수를 넘겨 특정 mock함수만 동작 여부를 선택할 수 있다.
이후 스크립트 실행 시 구분 짓고자 한다면 package.json에 관련 스크립트를 추가해줄 수도 있다.

+ Recent posts