QueryCache와 함께하는 전역 에러처리

김인태·2024년 11월 13일
1
post-thumbnail

🙄 개요

에러 처리에 대한 의문점들이 생겼습니다. 제가 구현하려고 했던 것은 만약 서버에서 400 에러 혹은 403 에러가

나타났을 때 거기에 대응하는 에러 페이지를 보여주는 것인데, 현재 리액트 쿼리를 사용하고 있는 가운데,

어떻게 전역적으로 에러처리를 할 수 있을까? 라는 생각으로 시작하였습니다.

💩 현재 코드

const AdminDashboard = () => {
  const [currentPage, setCurrentPage] = useState(0);
  const accessToken = localStorage.getItem("admin_accessToken");

  const { data, isLoading, statusCode } = useUserData({
    page: currentPage,
    token: accessToken as string,
  });

  if (isLoading) return <div>loading중..</div>;

  if (statusCode === 403) return <GuestPage />;
  if (statusCode === 400) return <ExpireTokenPage />;
  
  return ( //레이아웃 생략 );
  }

저는 리액트 쿼리와 axios를 사용해서 서버에 요청하고 거기에 대한 상태를 관리하고 있습니다.

제가 느낀 문제는 무엇이냐면 바로 다른 코드를 봐볼까요?

const TimeLineComponent = () => {
  const param = useParams();
  const inspectionId = Number(param.inspectionId);
  const accessToken = localStorage.getItem("accessToken") as string;
  const { data, isLoading, progress, statusCode } = useGetTimelineData({
    token: accessToken,
    inspectionId,
  });
  const navigate = useNavigate();

  if (isLoading) return <LoadingPage progress={progress} />;

  if (statusCode === 403) return <GuestPage />;
  if (statusCode === 400) return <ExpireTokenPage />;

일단 제가 인식한 문제는 세 가지 입니다.

  1. localStorage를 사용하는 함수를 모듈화해야함.
    1. 에러 핸들링 용이하게 하기 위해
    2. 데이터 형식을 표준화 하기 위해
    3. 스토리지의 교체가 용이하게 하기 위해
  2. as 와 같은 타입 단언을 사용해서 accessToken이 할당되어 있음 (1번 이유과 거의 비슷합니다.)
  3. 같은 형태의 코드 (if(statusCode…. )..) 가 반복되고 있음.

저는 1,2 번은 일단 차치하고 오늘은 3번 문제에 집중을 한 번 해보겠습니다.

react-hook 코드

사용되고 있는 react - hook은 이러합니다.

import { getAdminUserData } from "@/api/admin/api";
import { useQuery } from "@tanstack/react-query";
import { AxiosError } from "axios";

const useUserData = ({ page, token }: { page: number; token: string }) => {
  const { data, error, isLoading } = useQuery({
    queryKey: ["adminUser"],
    queryFn: () => getAdminUserData({ page, token }),
    retry: 1,
  });

  let statusCode;
  if (error instanceof AxiosError) {
    statusCode = error.status;
  }

  return { data, statusCode, isLoading };
};

export default useUserData;

useQuery와 axios를 통해서 요청된 값을 관리하고, status code를 return 하는 함수입니다.

아주 간단합니다. status code를 통해서 분기를하고 해당하는 컴포넌트들을 보여주고자 했습니다.

🦾 목표 & 고쳐봅시다!

결론적으로는 제게 필요한 것은 지금 에러처리 하는 방법에서 중복적인 코드를 발생시키기 때문에

전역적으로 에러를 처리할 수 있는 방법입니다.

ai와 동료들의 조언을 구했고, 거기에 따른 방법을 2가지로 정리해봤습니다.

  1. axios interceptor
    1. 서버 요청 직후에 바로 인터셉트해서 에러 페이지로 리다이렉트 시켜버린다.
  2. queryCache
    1. 에러타입별 처리를 할 수 있는 Provider를 만들어서 queryCache에 적용한다.

둘 다 괜찮은 방법처럼 보입니다.

하지만 저에게 주어진 상황에서는 QueryCache를 사용하는게 조금 더 나아보입니다. 그 이유는

  1. 현재 react-query 를 사용 중이므로 프레임워크의 일관성을 유지 할 수 있음
  2. 에러 처리, 상태관리를 한 곳에서 할 수 있음.
  3. 인터셉터보다 더 유연한 에러처리 가능(재시도, 캐싱 등의 옵션을 활용)

그러므로 queryCache를 사용해서 전역적으로 세팅을 한 번 해보겠습니다.

전역적으로 사용하기 위해서는 일단 Provider 단에서 설정을 해줘야겠다고 생각했습니다.

다음은 제 QueryProvider의 코드입니다.


import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { PropsWithChildren } from "react";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 1,
      refetchOnWindowFocus: false,
      staleTime: 5 * 60 * 1000,
    },
    mutations: {
      retry: 1,
    },
  },
});

function QueryProvider({ children }: PropsWithChildren) {
  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}

export default QueryProvider;

여기에다 추가적으로 query Cache를 이용해서 작성해보겠습니다.

// src/providers/QueryProvider.tsx
import ExpireTokenPage from "@/components/common/error/ExpireTokenPage";
import GuestPage from "@/components/common/error/GuestPage";
import {
  QueryCache,
  QueryClient,
  QueryClientProvider,
} from "@tanstack/react-query";
import { AxiosError } from "axios";
import type { PropsWithChildren } from "react";

const handleError = (error: unknown) => {
  if (error instanceof AxiosError) {
    const status = error.response?.status;

    switch (status) {
      case 400:
        return <ExpireTokenPage />;
        break;
      case 403:
        return <GuestPage />;
    }
  }
};

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: handleError,
  }),
  defaultOptions: {
    queries: {
      retry: 1,
      refetchOnWindowFocus: false,
      staleTime: 5 * 60 * 1000,
    },
    mutations: {
      retry: 1,
    },
  },
});

function QueryProvider({ children }: PropsWithChildren) {
  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}

export default QueryProvider;

이런식으로해서 전역적인 에러처리를 컴포넌트를 리턴하는 것으로 수정하였습니다!

⛔️ 주의!!

여기서 사용되는 에러처리는 모든 patch나 axios 요청에 해당되는 것이 아니라

react-query를 통한 요청만 처리한다는 사실?!

직접적인 요청은 axios interceptor에서 해야겠죠?!

하지만 제 프로젝트에는 직접 요청을 보내는 것은 없기 때문에 지금 일관성있게 처리할 수 있었던 것 같습니다~

감사합니다~!!

잠깐만요~~!!! 수정사항!

//변경전
const handleError = (error: unknown) => {
  if (error instanceof AxiosError) {
    const status = error.response?.status;

    switch (status) {
      case 400:
        return <ExpireTokenPage />;
        break;
      case 403:
        return <GuestPage />;
    }
  }
};

handleError에서 Jsx문을 리턴하고 있기 때문에 에러처리가 안됐습니다.
그래서 redirect 해주기로 했습니다!
onError 핸들러는 컴포넌를 렌더링하지 않고 , 단순히 에러처리 로직만 수행해야하며ㅡ
현재 코드에서 에러를 전역적으로 처리하지만 에러 상태를 화면에 표시하는 로직이 없기 때문에 아래와 같이 수정했습니다!

//변경 후

const handleError = (error: unknown) => {
  if (error instanceof AxiosError) {
    const status = error.response?.status;

    switch (status) {
      case 400:
        window.location.href = "/error/token-expired";
        break;
      case 403:
        window.location.href = "/error/guest";
        break;
    }
  }
};

[tanstack query 공식문서] https://tanstack.com/query/latest/docs/reference/QueryCache#querycache

profile
새로운 걸 배우는 것을 좋아하는 프론트엔드 개발자입니다!

0개의 댓글