본문 바로가기
Front End

React Query v5에서 에러처리하기

by 코딩파이 2024. 10. 1.
똑똑한개발자 내부 코드리뷰의 외부 공개용 재정리 파일입니다. @24.05.16
이후 후속코드까지 포함되어 내용이 추가되었습니다. @24.06.25

 

React Query 4이전까지는 useQuery 내부에서 onError라는 Props를 제공했습니다.

그러나 캐싱관련 문제를 해결하지 못하고, 결국 v5 이후로는 useQuery의 onError는 deprecated 되었습니다.

자세한 사항은 아래 번역본 을 참고해주세요.

 

아무튼, onError는 없어졌지만 에러처리는 하지 않을 수 없기 떄문에 권장하는 qeuryCache를 이용한 방법으로 새로 에러처리를 적용했습니다.

이렇게 한번 적용해 놓으니 기본 에러부터 커스텀 에러까지 더 좋은 DX를 보장받을 수 있었습니다.

 

0. 공통 에러 핸들링 함수 작성

우선 v4부터 현재까지 에러 처리하는 함수를 따로 빼서 사용했습니다.

input 자체에서 표출되는 에러를 제외한다면, 보통 Toast 방식으로 공통적으로 에러를 표출했거든요.

예시 함수는 아래와 같습니다.

const DEFAULT_ERROR_MESSAGE = "알 수 없는 오류가 발생했습니다.";


const useHandleError = () => {
  const toast = useCustomToast();

  const handleApiError = (error: any) => {
    const { message } = error?.response?.data || {};
    toast.open({
      description: message ?? DEFAULT_ERROR_MESSAGE,
    });
  };

  return { handleApiError };
};

 

 

1. v4 및 v5 초기 에러 처리방식

그리고 위 핸들러 함수를 사용한 이전 에러 처리방식은 아래와 같습니다.

onError가 존재하던 v4

  const { handleApiError } = useHandleError();

  const { data: logoData, isFetching: isFetchingLogo } =
    useGetLogoQuery({
      options: {
        onError: handleApiError,
      },
    });

 

onError가 없어진 v5 초기

export const useHandleApiErrorEffect = (err: AxiosError<any, any> | null) => {
  const { handleApiError } = useHandleError();

  useEffect(() => {
    err && handleApiError(err);
  }, [err]);
};
  const { data: logoData, error: logoError } = useGetLogoQuery({});
  
  useHandleApiErrorEffect(logoError);

 

두 방법 모두 onError 가 잘 작동하긴 합니다만,

onError가 없어진 사유인 cache fetching시 문제는 해결하지 못했습니다.

또한 매번 에러를 설정해줘야하는 귀찮음은 덤이구요.

그래서 여러 시행착오 끝에 위 문제를 모두 해결했습니다.

 

2. queryCache 인자 활용하여 에러처리 및 default Error 추가

코드부터 보겠습니다.

import '@tanstack/react-query';

type MetaTypeType = 'toast' | 'callback' | 'none';

interface MetaProps {
  type: MetaTypeType;
  message?: string;
  callback?: (...args: any) => void;
}

declare module '@tanstack/react-query' {
  interface Register {
    // P_TODO: interface 타입은 왜 못받을까요...
    queryMeta:
      | {
          type: MetaTypeType;
          message?: string;
          callback?: (...args: any) => void;
        }
      | undefined;
    mutationMeta: Record<string, unknown>;
  }
}

 

우선 meta 인자로 에러를 관리할 때 타입 추론이 가능하게끔 하고싶었습니다.

다릍 팀원분들과 협업할 때 좀 더 편리한 DX를 보장하기 위함입니다.

d.ts파일을 생성 후, 사용할 타입에 대해 임의로 정의했습니다.

 

import React, { useMemo } from 'react';

import {
  QueryCache,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query';

import { MetaProps } from '../../declaration';
import useHandleError from './useHandleError';

type CustomQueryClientProviderProps = {
  children: JSX.Element;
};

const CustomQueryClientProvider = ({
  children,
}: CustomQueryClientProviderProps) => {
  const { handleApiError } = useHandleError();

  const queryCache = useMemo(
    () =>
      new QueryCache({
        onError: (error: any, query) => {
          const meta = query.meta as MetaProps;
          if (!meta || meta.type === 'toast') {
            handleApiError(error, { message: meta?.message });
          } else if (meta.type === 'callback' && meta.callback) {
            meta?.callback(error);
          }
          // P_MEMO: none 타입은 작동안함
        },
      }),
    [handleApiError],
  );

  const queryClient = useMemo(
    () =>
      new QueryClient({
        defaultOptions: {},
        queryCache,
        ...
      }),
    [queryCache],
  );

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

export default CustomQueryClientProvider;

 

아래는 사용예시입니다. 

  const { data: userData } = useUserRetrieveQuery({
    options: {
      meta: {
        type: 'none', 
      },
    },
  });

    Query1({
    options: {
      meta: {
        type: 'callback',
        callback: (err) => {
          console.log('콘솔을 찍거나 다른 액션도 가능합니다.', err?.response.message)
        }
      },
    },
  });

    Query2({
    options: {
      meta: {
        type: 'message',
        message: '직접 메세지를 넘겨줍니다.'
      },
    },
  });

 

그리고 QueryClientProvider에 queryCache까지 추가하여 파일을 새로 만들었습니다.

여기서 주의점은 꼭 queryCache를 useMemo로 감싸야 합니다.

그게 아니라면 매 React Query가 동작할 때 마다 새로운 QueryCache 인스턴스를 생성하며 로직이 동작하지 않습니다.

 

아무튼, 이전에 작성한 handleApiError 를 활용하여 조건부 함수를 적용했습니다.

아무것도 넘기지 않아도 자동으로 API 에러 메세지를 읽어오고,

callback을 넘긴다면 onError처럼 사용을,

message를 넘긴다면 서버 에러 메세지가 아닌 클라이언트 에러메세지를,

none을 넘긴다면 고의로 아무 에러 피드백도 오지 않게 설정할 수 있습니다.

 

아무튼 이렇게 적용하니 onError를 쓸 때와 같이 유연성은 그대로 보장되면서,

매번 귀찮게 에러 처리를 하지 않아도 자동으로 해 주니 개발 편의성 또한 매우 향상되었습니다.

 

이후 mutation까지 이런 방식으로 처리해주니 더더욱 편리해졌구요.

실제 사용중인 코드는 깃허브 링크에서 확인 가능합니다.