TanStack Query로 API 에러 처리하기

oversleep·2025년 8월 14일

API 호출 시 발생하는 에러를 체계적으로 처리하는 방법을 알아보겠습니다. 서버에서 구조화된 에러 응답이 올 때, 이를 프론트엔드에서 어떻게 타입 안전하게 처리할 수 있는지 살펴보겠어요.

문제 상황

서버에서 이런 에러 응답이 온다고 가정해봅시다:

{
  "type": "TRANSMIT_DIRECTORY_REQUEST_ACCEPTED_EXIST",
  "title": "디스패치 요청 처리 중 오류가 발생했습니다.",
  "status": 400,
  "detail": "이미 해당 디렉토리 전송 요청을 수락하였습니다.",
  "errorCode": "TRANSMIT_DIRECTORY_REQUEST_ACCEPTED_EXIST",
  "instance": "/api/dispatch/directory-transmissions"
}

이런 구조화된 에러를 단순히 Error 타입으로 처리하면 타입 안전성을 잃게 됩니다.

1단계: 커스텀 에러 클래스 정의

먼저 API 파일에서 커스텀 에러 클래스를 정의합니다:

// transferFolder.ts
import { TransferFolderData } from '@/types/folders';
import { axiosInstance } from '../axiosInstance';
import axios from 'axios';

export class TransferFolderError extends Error {
  constructor(
    public errorCode: string,
    public status: number,
    public detail: string,
    public instance: string
  ) {
    super(detail);
    this.name = 'TransferFolderError';
  }
}

export async function transferFolder(data: TransferFolderData) {
  try {
    const res = await axiosInstance.post(
      '/api/dispatch/directory-transmissions',
      data
    );
    return res.data;
  } catch (e: any) {
    if (axios.isAxiosError(e)) {
      const errorData = e.response?.data;
      
      // 서버 에러 응답을 구조화된 형태로 변환
      if (errorData?.errorCode) {
        throw new TransferFolderError(
          errorData.errorCode,
          errorData.status || e.response?.status || 500,
          errorData.detail || '알 수 없는 오류가 발생했습니다.',
          errorData.instance || ''
        );
      }
    }
    throw e;
  }
}

2단계: TanStack Query Hook 구현

이제 커스텀 에러 타입을 사용하는 Hook을 만듭니다:

// useTransferFolder.ts
import { transferFolder, TransferFolderError } from '@/apis/folder-apis/transferFolder';
import { TransferFolderData, TransferFolderResponse } from '@/types/folders';
import { useMutation, UseMutationOptions } from '@tanstack/react-query';

export function useTransferFolder(
  options?: UseMutationOptions<
    TransferFolderResponse,
    TransferFolderError, // 커스텀 에러 타입 사용
    TransferFolderData
  >
) {
  return useMutation<TransferFolderResponse, TransferFolderError, TransferFolderData>({
    mutationFn: transferFolder,
    onSuccess: (data, variables, context) => {
      options?.onSuccess?.(data, variables, context);
    },
    onError: (error, variables, context) => {
      // 이제 error.errorCode, error.detail 등에 타입 안전하게 접근 가능
      console.log('에러 코드:', error.errorCode);
      console.log('에러 메시지:', error.detail);
      
      options?.onError?.(error, variables, context);
    },
    onSettled: (data, error, variables, context) => {
      options?.onSettled?.(data, error, variables, context);
    },
  });
}

3단계: 컴포넌트에서 사용하기

function TransferComponent() {
  const transferMutation = useTransferFolder({
    onSuccess: (data) => {
      toast.success('디렉토리 전송이 완료되었습니다!');
      // data.receiverEmail, data.directoryName 등에 접근 가능
    },
    onError: (error) => {
      // 에러 코드별로 다른 처리
      switch (error.errorCode) {
        case 'TRANSMIT_DIRECTORY_REQUEST_ACCEPTED_EXIST':
          toast.error('이미 수락된 디렉토리 전송 요청입니다.');
          break;
        default:
          toast.error(error.detail || '전송 중 오류가 발생했습니다.');
      }
    }
  });

  const handleTransfer = (data: TransferFolderData) => {
    transferMutation.mutate(data);
  };

  return (
    <div>
      {transferMutation.isError && (
        <div className="error-message">
          {transferMutation.error?.detail}
        </div>
      )}
      
      <button 
        onClick={() => handleTransfer(formData)}
        disabled={transferMutation.isLoading}
      >
        {transferMutation.isLoading ? '전송 중...' : '디렉토리 전송'}
      </button>
    </div>
  );
}

콜백 함수의 역할 이해하기

Hook 내부의 콜백 함수들이 하는 일을 자세히 살펴보겠습니다:

onSuccess: (data, variables, context) => {
  // TanStack Query가 성공 시 이 함수를 호출
  // 외부에서 전달받은 onSuccess가 있으면 그것도 실행
  options?.onSuccess?.(data, variables, context);
},

이는 패스스루(pass-through) 패턴으로, Hook을 사용하는 곳에서 자유롭게 콜백을 정의할 수 있게 해줍니다.

더 간단한 방법

사실 대부분의 경우 이렇게 간단하게 할 수 있습니다:

export function useTransferFolder(
  options?: UseMutationOptions<TransferFolderResponse, TransferFolderError, TransferFolderData>
) {
  return useMutation<TransferFolderResponse, TransferFolderError, TransferFolderData>({
    mutationFn: transferFolder,
    ...options, // 스프레드 연산자로 모든 옵션 전달
  });
}

공통 로직이 필요할 때

Hook 내부에서 공통 처리 로직을 추가하고 싶다면 패스스루 방식을 사용합니다:

onError: (error, variables, context) => {
  // 공통 에러 로깅
  console.error('API Error:', error.errorCode, error.detail);
  
  // 모든 에러를 analytics에 전송
  analytics.track('api_error', {
    errorCode: error.errorCode,
    endpoint: error.instance
  });
  
  // 외부에서 전달받은 콜백도 실행
  options?.onError?.(error, variables, context);
},

장점

이 패턴의 주요 장점들:

  1. 타입 안전성: error.errorCode, error.detail 등에 타입 안전하게 접근
  2. 재사용성: 다른 API에도 같은 패턴 적용 가능
  3. 확장성: 공통 로직과 개별 로직을 분리하여 관리
  4. 개발자 경험: IDE에서 자동완성과 타입 체크 지원

마무리

TanStack Query와 TypeScript를 함께 사용할 때 에러 처리를 체계적으로 하면, 런타임 에러를 줄이고 더 안정적인 애플리케이션을 만들 수 있습니다.

특히 서버에서 구조화된 에러 응답을 보내는 경우, 커스텀 에러 클래스를 통해 이를 타입 안전하게 처리하는 것이 중요합니다.


참고 링크

profile
궁금한 것, 했던 것, 시행착오 그리고 기억하고 싶은 것들을 기록합니다.

0개의 댓글