React Query로 글로벌하게 에러 핸들링 하기

박먼지·2025년 1월 23일
0

문제 상황

1. 에러 처리 흐름 파편화 문제

현재 에러 처리가 커스텀 fetch 함수인 apiFetch와 전역 팝업 상태인 PopupContext에서 이루어지고 있다.
이렇게 에러 처리 흐름이 분산되어있다 보니 흐름을 파악하기 어렵고, 새로운 타입의 에러 처리가 필요할 때 어디에 구현해야 할 지 고민이 필요했다.
또한 apiFetch 함수는 api를 호출하는 역할에 집중하고, PopupContext는 팝업을 렌더링하는데만 집중하고 싶었다.
그래서 글로벌 에러 핸들러를 도입하면 에러의 단일 진입점을 확보해 흐름을 파악하는데 더 용이하지 않을까? 하고 생각했다.

2. 에러 응답 포맷 변경

기존의 에러 응답 타입은 아래와 같았다.

type LegacyErrorType = {
  detail: {
    error_code: string;
    message: string;
    status_code: number;
  };
};

그런데 백엔드에서 글로벌 익셉션 핸들러를 도입하면서 에러 응답 타입이 아래와 같이 바뀌었다.

type NewErrorType = {
  STATUS_CODE: number;
  ERROR_CODE: string;
  MESSAGE: string;
  ERROR_CLASS: string;
};

detail이 빠지고 대문자로 변경된 것이다!
그래서 매번 타입 캐스팅을 통해 에러를 처리해야 하는데.. 앞으로 모든 api에 저 포맷이 적용될 예정이기 때문에 이를 반영할 수 있는 효율적인 처리 방식이 필요했다.

적용 과정

Global callbacks

새로운 QueryClient를 생성할 때 mutationCachequeryCache 속성으로 onError를 정의해주면 각각의 뮤테이션이나 쿼리에서 에러가 일어날 때 에러 처리를 한 번에 할 수 있다.

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: ...
  }),
  mutationCache: new MutationCache({
    onError: ...
  }),
});

Custom hook과 함께 사용하기

우리의 어드민 페이지는 모든 에러 처리를 하나의 공통된 팝업 창으로 해결하도록 기획되어있다. 해당 팝업은 usePopup hook으로 호출할 수 있는데, 훅은 컴포넌트 외부에서 호출할 수 없어서 QueryClientProvider를 감싸는 함수 컴포넌트를 만들어주었다.

const CustomQueryClientProvider = ({ children }: { children: ReactNode }) => {
  const { openPopup } = usePopup();

  // 최초 렌더링 시에만 초기화하기 위해 useState 사용
  const [queryClient] = useState(() => {
    return new QueryClient({
      queryCache: new QueryCache({
        onError: (error) =>
          openPopup({
            title: error.message,
          }),
      }),
    });
  });

  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

export default CustomQueryClientProvider;

위의 컴포넌트를 기존의 QueryClientProvider와 교체하면 된다.

// index.tsx
ReactDOM.createRoot(document.getElementById("root")!).render(
  <PopupContextProvider>
    <CustomQueryClientProvider>
      <App />
    </CustomQueryClientProvider>
  </PopupContextProvider>
);

처음에는 QueryClient를 생성하는 곳에서 바로 에러 처리 로직을 작성하려고 했는데, 에러 종류에 따라 처리 방식이 조금씩 달라서 useErrorHandlers라는 커스텀 훅을 분리해서 여기서만 에러 처리 로직을 작성하기로 했다.

export const useErrorHandlers = () => {
  const { open: openPopup } = usePopup();

  return (error: ErrorResponseType | Error) => {
    // 서버 에러
    if (error.status_code >= 500) {
      return openPopup({
        title: "서버와 통신 중 에러가 발생했습니다.",
      });
    }
    // 기본 에러 메시지
    openPopup({
      title: error.message,
    });
    // 그 외 다양한 처리 로직...
  };
};

그리고 useErrorHandlersnew QueryClient생성 시 활용한다.

const queryClient = useMemo(
  () =>
    new QueryClient({
      mutationCache: new MutationCache({
        onError: handleError,
      }),
      queryCache: new QueryCache({
        onError: handleError,
      }),
    }),
  [handleError]
);

에러 메시지 커스터마이징 하기

meta 필드를 사용하면 쿼리 별로 원하는 에러 메시지를 보여줄 수 있다.

useMutation({
  mutationFn: payment,
  meta: {
    errorMessage: "결제에 실패했어요.",
  },
});
profile
개발괴발

0개의 댓글