Tanstack Query + Axios로 전역 에러 처리하기

김민서·2025년 2월 6일
3

DOKBARO

목록 보기
4/6
post-thumbnail

들어가기 앞서

이 포스팅은 DOKBARO를 개발하면서 경험한 것을 기반으로 제작하였습니다.

DOKBARO란 ?

자기개발과 성장을 위해 독서와 스터디를 활용하는 개발자들을 위한 퀴즈 학습 플랫폼, DOKBARO입니다.

개발 서적을 즐겨 읽지만, 매번 내용을 제대로 이해했는지 확인하기 어렵지 않으셨나요? 혹은 이해 부족으로 인해 독서 스터디가 소수만 적극적으로 참여하는 형태로 변질되는 경험을 하셨을지도 모릅니다.

그래서, DOKBARO는

📚 퀴즈 출제 및 풀이 기능으로 도서 내용을 재미있고 효과적으로 이해하도록 도와드려요.

💡 스터디 리포트 기능으로 스터디원들이 책에 대해 자유롭게 의견을 나누고, 서로의 학습 현황을 확인할 수 있어요.

DOKBARO와 함께라면 도서 이해도를 높이고, 스터디 활동을 보다 풍성하고 활발하게 만들어 이상적인 독서 환경을 경험하실 수 있습니다. ✌️

현재 베타 오픈중이니 아래 링크를 통해 이용해보실 수 있어요!
https://dokbaro.com


개요

프로젝트를 진행하면서 처음에는 개별 API 호출마다 try-catch를 사용하는 기본적인 방식으로 에러 처리를 했다.

하지만 프로젝트의 규모가 커질수록 같은 로직이 반복되었고, 예를 들어 사용자 토큰이 만료되었을 때 자동으로 로그아웃을 처리하는 등의 전역적인 로직을 어떻게 관리할지에 대한 고민이 생겼다. 따라서 전역적인 에러 핸들링을 도입하게 되었다.

프로젝트에서는 Axios와 Tanstack Query(React Query)를 사용하고 있기 때문에, 이를 활용하여 에러 처리를 구현해보았다.

1. Axios 에러 일괄 캐칭

Axios를 사용할 때 서버에서 반환하는 에러 응답을 통일된 형식으로 변환하는 것이 중요하다.
따라서, 응답 코드(code)와 에러 메시지(details)를 통일되게 담아주기 위해 ErrorType 타입을 선언했다.

interface ErrorType extends Error {
  code?: number;
  details?: unknown;
}

그리고 서버에서 반환하는 에러 응답을 ErrorType 형식으로 변환해서 던져주는 handleAxiosError 함수를 작성했다.

import { AxiosError } from "axios";
import { ErrorType } from "@/types/ErrorType";

export const handleAxiosError = (error: unknown) => {
  const err = error as AxiosError;
  
  if (err.response) {
    const { status, data } = err.response;
    const message = (data as { message: string })?.message;
    throw { code: status, message, details: data } as ErrorType;
  }
  throw new Error("알 수 없는 오류가 발생했습니다.");
};

그리고 이렇게 Axios를 통해 API를 호출하는 부분에서 try-catch로 포착한 에러를 아까 작성해준 handleAxiosError 함수에 전달해주어 에러 응답을 통일된 형식으로 반환하도록 했다.

fetchQuizzes = async (
    params: FetchQuizzesParams,
  ): Promise<{ data: QuizType[]; endPageNumber: number } | null> => {
    try {
      const { page, size, bookId, sort, direction } = params;

      const { data } = await axiosInstance.get("/book-quizzes", {
        params: { page, size, bookId, sort, direction },
      });

      return data;
    } catch (error) {
      // 에러 발생
      handleAxiosError(error);
      return null;
    }
  };

2. 캐치한 에러를 전역으로 핸들링

이제 개별 API 요청에서 에러가 발생해서 handleAxiosError를 통해 반환된 에러를, 에러 코드에 따라 전역적으로 핸들링해주는 handleQueryError 함수를 작성했다.
이 함수에서는 에러 코드에 따른 에러 처리(해당 코드에서는 주로 토스트 알림을 띄움)를 해줄 수 있다.

이를 통해 개별 API 요청마다 응답 코드에 대한 처리를 각각 구현할 필요 없이, 한 곳에서 에러 처리 로직을 일괄적으로 관리할 수 있다.

import toast from "react-hot-toast";
import { ErrorType } from "@/types/ErrorType";

export const handleQueryError = async (error: ErrorType) => {
  switch (error.code) {
    case 401:
      toast.error("로그인이 만료되었습니다. 다시 로그인해주세요.");
      window.location.href = "/";
      break;
    case 403:
      toast.error("이 기능을 사용하기 위해서는 적절한 권한이 필요합니다.");
      break;
    case 500:
      toast.error("서버 오류가 발생했습니다. 관리자에게 문의해주세요.");
      break;
    default:
      toast.error(
        error.message || "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
      );
  }
};

3. Tanstack Query의 전역 에러 핸들링

Tanstack Query의 쿼리 및 뮤테이션에서 발생한 에러를 처리하기 위해 queryClient(데이터의 페칭, 캐싱, 동기화 및 전역 상태 관리를 담당하는 핵심 객체)를 설정해주었다.

각각 쿼리, 뮤테이션에서 에러가 발생하면, 이 에러를 handleQueryError 함수로 전달하여 전역적으로 에러를 처리할 수 있게 했다.

이때, 전역 상태 관리 라이브러리인 Jotai의 Atom에 특정 상태에 대한 에러 처리 스킵 여부를 담아서, 이를 통해 필요에 따라 에러 처리를 건너뛸 수 있도록 했다.

import { ErrorType } from "@/types/ErrorType";
import { QueryCache, QueryClient } from "@tanstack/react-query";
import { handleQueryError } from "@/utils/errorHandler";
import { getDefaultStore } from "jotai";
import { skipGlobalErrorHandlingAtom } from "@/store/skipGlobalErrorHandlingAtom";

const store = getDefaultStore();

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
    },
    mutations: {
      onError: (error: ErrorType) => {
        // 뮤테이션 에러 발생
        handleQueryError(error);
      },
    },
  },
  queryCache: new QueryCache({
    onError: (error: ErrorType) => {
      if (store.get(skipGlobalErrorHandlingAtom)) {
         // 특정 조건에서는 전역 에러 핸들링 스킵
        return;
      }
      // 쿼리 에러 발생
      handleQueryError(error);
    },
  }),
});

queryClient 적용

<QueryClientProvider client={queryClient}>
  ...
</QueryClientProvider>

마무리

전역 에러 처리는 제대로 해본 적이 없어서 처음부터 적용할 생각을 못하고 놓친 부분이었다. 하지만 프로젝트를 진행하면서 불편함을 느꼈고, 도입이 필수적이라 생각했다.
다른 급한 부분을 먼저 진행하느라 계속 미루고 있었는데, 이렇게 변경하고 나니 속이 시원했다.
분명 개선할 점도 있고 더 효율적인 방법이 있겠지만, 확실히 이를 통해 프로젝트의 유지보수성과 개발 효율이 높아졌다고 느꼈다.

1개의 댓글

comment-user-thumbnail
2025년 2월 6일

많이 배우고 갑니다~

답글 달기