TanStack Query와 ErrorBoundary, Suspense 의 조합

도현수·2024년 11월 20일
0

프로젝트의 에러 처리를 하던 중 의문이 생겼다.

다음과 같이 tanstack query를 사용한 기본적인 에러 처리를 하고 있었는데…

  const { data, isLoading, isError } = useMyTeam();
  if (isLoading) {
    return <Loading />;
  }

  if (isError) {
    return <ErrorPage/>;
  }
  ...

이런 식의 에러 처리를 다른 컴포넌트에서 하다보니 다음과 같은 의문이 생겼다.

오잉… 그러면 필요한 컴포넌트마다 요런 에러 처리를 일일이 해줘야 한다고…?

이런 방식은 너무 비효율적이라는 생각이 들었고, 공식문서와 블로그를 찾아보며 다른 방법을 찾아보게 되었다. 다행히도 많은 분들이 같은 고민을 하셔서… 많은 자료를 찾아볼 수 있었다.

전역 콜백 설정해서 에러 처리하기(로그인이 만료된 경우)

QueryCache 를 사용해 전역으로 에러 처리를 할 수 있다.

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error) => {
      if (isAxiosError(error)) {
        const status = error.response?.status;
        if (status === 403) {
          useAuthStore.getState().logout();
          alertToast("로그인이 만료되었습니다. 다시 로그인 해주세요!", "info");
        }
      }
    },
  }),
 

위는 쿼리에 대한 에러 처리이고, 뮤테이션에 대해서는 MutationCache 를 생성해서 같은 작업을 반복하면 됨. 저 QueryCache 는 쿼리의 저장소인데, 저기서 onError 설정을 하면 모든 쿼리가 에러에 마주쳤을 때 한가지 동작을 하도록 설정이 가능하다.

로그인이 만료된 경우(403 에러를 받음) 로그아웃 처리를 해주고 사용자에게 알리는 작업을 수행한다.

그렇다면 403 이외의 에러에는 어떻게 할까… 하고 열심히 찾아보았고, ErrorBoundarySuspense 를 찾았다.

ErrorBoundary

ErrorBoundary 는 하위 컴포넌트에서 JS 에러가 발생하면 fallback을 보여주는 컴포넌트이다. 단, 이벤트 핸들러와 비동기 작업에서 발생한 에러는 포착하지 못한다. 즉, 렌더링 과정에서 발생한 에러만을 포착한다.

react-error-boundary 를 설치해서 사용한다.
fallback을 렌더링하기 위한 props로 fallback, FallbackComponent, fallbackRender 이 있는데, 내가 만든건 에러 리셋 기능을 포함한 조금 복잡한 컴포넌트기 때문에 FallbackComponent를 사용했다.

// AuthRoute/index.tsx

  const { reset } = useQueryErrorResetBoundary();
...
       <ErrorBoundary
          FallbackComponent={ErrorPage}
          onReset={reset}
          resetKeys={[location.pathname]}
        >
          <Outlet />
        </ErrorBoundary>
  • onReset 은 에러를 초기화하는 props를 전달해야하는데, tanstack-query에서 제공하는 useQueryErrorResetBoundaryreset을 사용했다. FallbackComponent 에 전달된 컴포넌트의 props인 resetErrorBoundary 와 연결되어 사용된다.
  • resetKeys 의 경우, 배열에 전달된 키들 중 하나라도 변경이 되면 에러가 초기화된다. 여기서는 경로를 키로 삼아 페이지가 전환될 때마다 에러가 초기화되도록 했다.

throwOnError: true, 설정

하지만 ErrorBoundary 는 기본적으로 렌더링 중 발생한 에러만을 포착하기에 QueryClient 에서throwOnError: true, 설정 했다. 해당 설정을 통해 ErrorBoundary 가 비동기 작업에서의 에러를 포착할 수 있다(참고1).(참고2).

에러가 발생하면 다음과 같이 렌더링 된다.
스크린샷 2024-11-20 오후 5 30 58

! 단, 밑에서 언급할 useSuspenseQuery 의 경우 보여줄 다른 데이터가 없는 경우에는 에러 바운더리로 에러를 던진다. 따라서 프로젝트 내부에서 useSuspenseQuery 만을 사용해서 에러를 던질 수 있을 경우 별도의 throwOnError 설정은 필요가 없다. 공식문서의 이 부분 참고.

Suspense

Suspense 는 비동기 작업에서 사용자 경험을 향상시키는 컴포넌트이다. 비동기 처리의 응답을 기다리는 동안 fallback을 보여준다.
tanstack query에서 서스펜스를 사용하기 위해선 useQuery 가 아닌 useSuspenseQuery, useSuspenseInfiniteQuery, useSuspenseQueries 3가지 훅 중 하나를 사용해야한다. 해당 훅들은 프라미스를 반환하기 때문에 Suspense 에서 감지할 수 있다.

따라서 팝업으로 접근할 수 있는 페이지에서 호출하는 useQuery를 전부 useSuspenseQuery 로 바꾸고,SuspenseOutlet 을 감쌌다.

      <Suspense fallback={<Loading />}>
        <ErrorBoundary
          FallbackComponent={ErrorPage}
          onReset={reset}
          resetKeys={[location.pathname]}
        >
          <Outlet />
        </ErrorBoundary>
      </Suspense>
스크린샷 2024-11-20 오후 6 44 49

실제 적용

실제 적용은 다음과 같았다. 헤딩과 설명 및의 내용물에 fallback 이 적용된다.

export default function AuthLayout() {
  const { accessToken } = useAuthStore((state) => ({
    accessToken: state.accessToken,
  }));
  const { heading, paragraph } = useContent();
  const { reset } = useQueryErrorResetBoundary();
  // 사용자가 로그인되지 않았으면 로그인 페이지로
  if (!accessToken) {
    return <Navigate to="/login" />;
  }

  // 로그인된 경우 렌더링
  return (
    <>
      <div className="flex flex-col items-center">
        <AuthFirstHeading content={heading} />
        <p className="mb-5 font-medium text-text-tertiary">{paragraph}</p>
        {location.pathname.includes("/my-tickets") && (
          <div className="mb-4 flex gap-4">
            {myTicketMenu.map((menu) => (
              <MenuButton menu={menu} key={menu} />
            ))}
          </div>
        )}
      </div>
      <hr className="border-1 mb-10 border-borders" />
      <Suspense fallback={<Loading />}>
        <ErrorBoundary
          FallbackComponent={ErrorPage}
          onReset={reset}
          resetKeys={[location.pathname]}
        >
          <Outlet />
        </ErrorBoundary>
      </Suspense>
    </>
  );
}

마무리

생각보다 적용에 오랜 시간이 필요했다. 스스로 더 나은 에러 핸들링을 위한 방법을 찾아보고 적용시켰다는 점에서 개인적으로는 좋은 경험이었다고 생각한다. 하지만 프로젝트를 진행함에 따라 더 개선될 여지가 있다고 생각되고, 그 때 다시 한 번 더 나은 방법에 대해 생각해보도록 해야지...

참고자료

tkdodo 블로그 참고

블로그 참고 1

블로그 참고 2

블로그 참고 3

블로그 참고 4

깃허브 정리 저장소

서스펜스 관련

0개의 댓글

관련 채용 정보