본문 바로가기

프로젝트/You-Together

react-query 에러 중앙화 처리

- 이전부터 프로젝트를 진행하면서 에러 처리를 일관적이고 효율적으로 처리하는 방법에 대해서 고민을 많이 했고 이번 프로젝트에서는 개선된 내용을 적용해보고자 했다.

- 특히 이번 프로젝트에서는 모든 API를 호출하는 커스텀 훅을 react-query를 사용하여 만들고, 각 훅에서 API 호출 시마다 개별적으로 에러를 처리하는 것이 비효율적이라고 생각했다.

- 또한, react-query의 최신 버전인 5부터는 useQuery의 onError 콜백이 제거되었기 때문에, useQuery를 사용하는 컴포넌트 내에서 에러 처리를 해야 했기 때문에 에러 처리를 중앙화하는 방법이 훨씬 효율적이라고 판단했다.

 

문제정의

- 먼저 중앙화 처리 이전에 각 훅에서 API를 호출할 때마다 개별적으로 error를 처리하는 로직을 살펴보자.(에러 처리는 toast를 사용, useQuery의 경우에는 onError 콜백이 사라졌기 때문에 useMutation을 사용하는 훅을 예시로 사용)

// useChangeNickname 커스텀 훅

import { changeNickname } from '@/api/change-nickname'; // api 호출 함수
import { TNicknameChangePayload } from '@/schemas/change-nickname';
import { useMutation } from '@tanstack/react-query';
import { toast } from 'react-toastify';

const useChangeNickname = () => {
  return useMutation({
    mutationFn: ({ newNickname }: TNicknameChangePayload) =>
      changeNickname({ newNickname }),
    onError: (error) => {
      if (error instanceof Error) {
        toast.error(error.message || '알 수 없는 오류가 발생했습니다.', {
          position: 'top-right',
          autoClose: 5000,
          hideProgressBar: false,
          closeOnClick: true,
          pauseOnHover: true,
          draggable: true,
          progress: undefined,
        });
      }
    },
  });
};

export default useChangeNickname;

// useDeletePlaylist 커스텀 훅
import { useMutation } from '@tanstack/react-query';
import { deletePlaylist } from '@/api/delete-playlist'; // api 호출 함수
import { toast } from 'react-toastify';

const useDeletePlaylist = () => {
  return useMutation({
    mutationFn: ({ videoNumber }: { videoNumber: number }) =>
      deletePlaylist({ videoNumber }),
    onError: (error) => {
      if (error instanceof Error) {
        toast.error(error.message || '알 수 없는 오류가 발생했습니다.', {
          position: 'top-right',
          autoClose: 5000,
          hideProgressBar: false,
          closeOnClick: true,
          pauseOnHover: true,
          draggable: true,
          progress: undefined,
        });
      }
    },
  });
};

export default useDeletePlaylist;

 

- 위 두 가지의 커스텀 훅은 사용자의 닉네임을 변경할 때, 플레이리스트 항목을 삭제할 때 사용하는 훅이다.

- 한 눈에 보아도 onError 콜백 코드가 중복되고 있다는 것을 알 수 있다. 또한 만약 toast가 아닌 다른 방식으로 수정하는 등 유지보수를 위해서는, 모든 훅을 수정해야하는 번거로움도 존재하게 된다.

 

해결방안

- 그렇다면, 에러 중앙화 처리라는 것은 무엇이며 react-query를 사용할 때는 어떻게 구현할 수 있을까?

- 내가 적용한 방법은 react-query의 QueryClient를 사용하는 방법이었다.

- QueryClient는 react-query에서 사용되는 주요 객체로, 쿼리 상태를 관리하고 쿼리 및 변이(mutation)를 위한 설정을 제공하며 쿼리를 관리하고 캐싱, 동기화, 무효화 등을 처리하는 중심 역할을 한다.

- 따라서, 애플리케이션 내에서 하나의 QueryClient 인스턴스를 생성하여 모든 쿼리를 관리할 수 있기 때문에 이를 통해 에러 중앙화 처리를 적용했다.

// queryClient.ts
import { QueryClient, QueryCache, MutationCache } from '@tanstack/react-query';
import { toast } from 'react-toastify';

export const errorHandler = (message: string) => {
  const id = 'react-query-toast';

  if (!toast.isActive(id)) {
    toast.error(message, {
      toastId: id,
      position: 'top-right',
      autoClose: 5000,
      hideProgressBar: false,
      closeOnClick: true,
      pauseOnHover: true,
      draggable: true,
      progress: undefined,
    });
  }
};

const createQueryClient = () => {
  return new QueryClient({
    queryCache: new QueryCache({
      onError: (error) => {
        if (error instanceof Error) {
          errorHandler(error.message || '알 수 없는 오류가 발생했습니다.');
        }
      },
    }),
    mutationCache: new MutationCache({
      onError: (error) => {
        if (error instanceof Error) {
          errorHandler(error.message || '알 수 없는 오류가 발생했습니다.');
        }
      },
    }),
  });
};

export default createQueryClient;

 

- 먼저 QueryClient 인스턴스를 생성하고 쿼리 데이터를 저장하고 관리하는 QueryCache, 변이(mutation) 작업의 상태를 관리하는 MutationCache의 onError 콜백을 사용하여 에러를 일관되게 처리하고 있다.

// provider.tsx
'use client';

import { NextUIProvider } from '@nextui-org/react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental';
import createQueryClient from '@/lib/query-client';
import { useState } from 'react';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

export function NextUIProviders({ children }: { children: React.ReactNode }) {
  return <NextUIProvider>{children}</NextUIProvider>;
}

export function ReactQueryProviders({ children }: React.PropsWithChildren) {
  const [client] = useState(createQueryClient);

  return (
    <QueryClientProvider client={client}>
      <ReactQueryStreamedHydration>{children}</ReactQueryStreamedHydration>
      <ReactQueryDevtools />
      <ToastContainer />
    </QueryClientProvider>
  );
}

- provider.tsx 파일에서 QueryClientProvider를 사용하여 생성한 QueryClient를 주입하였다.

 

결과

- 사용자의 닉네임 변경에 실패하였을 때, toast와 에러 메시지가 정상적으로 출력되는 것을 확인할 수 있다.

- 에러 중앙화 처리를 통해 애플리케이션의 모든 에러를 한 곳에서 관리하고 처리함으로써, 개발 과정에서 일관성을 유지하고, 코드의 가독성을 높이며, 유지보수를 훨씬 쉽게 만들 수 있었다.

 

배운 점

- 항상 에러를 효율적으로 처리하는 방법에 대해 고민이 많았었는데, 이번에 적용한 에러 중앙화를 통해 에러 처리를 일관되게 하고, 코드 중복을 줄이며, 유지보수를 용이하게 할 수 있어서 스스로 한 단계 더 나아갔다는 생각이 들어 만족스럽다.

 


참고 : https://velog.io/@zooyaho/React-Query-%EC%A4%91%EC%95%99-%EC%A7%91%EC%A4%91%ED%99%94