API 호출 시 발생하는 에러를 체계적으로 처리하는 방법을 알아보겠습니다. 서버에서 구조화된 에러 응답이 올 때, 이를 프론트엔드에서 어떻게 타입 안전하게 처리할 수 있는지 살펴보겠어요.
서버에서 이런 에러 응답이 온다고 가정해봅시다:
{
"type": "TRANSMIT_DIRECTORY_REQUEST_ACCEPTED_EXIST",
"title": "디스패치 요청 처리 중 오류가 발생했습니다.",
"status": 400,
"detail": "이미 해당 디렉토리 전송 요청을 수락하였습니다.",
"errorCode": "TRANSMIT_DIRECTORY_REQUEST_ACCEPTED_EXIST",
"instance": "/api/dispatch/directory-transmissions"
}
이런 구조화된 에러를 단순히 Error 타입으로 처리하면 타입 안전성을 잃게 됩니다.
먼저 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;
}
}
이제 커스텀 에러 타입을 사용하는 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);
},
});
}
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);
},
이 패턴의 주요 장점들:
error.errorCode, error.detail 등에 타입 안전하게 접근TanStack Query와 TypeScript를 함께 사용할 때 에러 처리를 체계적으로 하면, 런타임 에러를 줄이고 더 안정적인 애플리케이션을 만들 수 있습니다.
특히 서버에서 구조화된 에러 응답을 보내는 경우, 커스텀 에러 클래스를 통해 이를 타입 안전하게 처리하는 것이 중요합니다.
참고 링크