현재 에러 처리가 커스텀 fetch 함수인 apiFetch
와 전역 팝업 상태인 PopupContext
에서 이루어지고 있다.
이렇게 에러 처리 흐름이 분산되어있다 보니 흐름을 파악하기 어렵고, 새로운 타입의 에러 처리가 필요할 때 어디에 구현해야 할 지 고민이 필요했다.
또한 apiFetch
함수는 api를 호출하는 역할에 집중하고, PopupContext
는 팝업을 렌더링하는데만 집중하고 싶었다.
그래서 글로벌 에러 핸들러를 도입하면 에러의 단일 진입점을 확보해 흐름을 파악하는데 더 용이하지 않을까? 하고 생각했다.
기존의 에러 응답 타입은 아래와 같았다.
type LegacyErrorType = {
detail: {
error_code: string;
message: string;
status_code: number;
};
};
그런데 백엔드에서 글로벌 익셉션 핸들러를 도입하면서 에러 응답 타입이 아래와 같이 바뀌었다.
type NewErrorType = {
STATUS_CODE: number;
ERROR_CODE: string;
MESSAGE: string;
ERROR_CLASS: string;
};
detail
이 빠지고 대문자로 변경된 것이다!
그래서 매번 타입 캐스팅을 통해 에러를 처리해야 하는데.. 앞으로 모든 api에 저 포맷이 적용될 예정이기 때문에 이를 반영할 수 있는 효율적인 처리 방식이 필요했다.
새로운 QueryClient
를 생성할 때 mutationCache
나 queryCache
속성으로 onError
를 정의해주면 각각의 뮤테이션이나 쿼리에서 에러가 일어날 때 에러 처리를 한 번에 할 수 있다.
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: ...
}),
mutationCache: new MutationCache({
onError: ...
}),
});
우리의 어드민 페이지는 모든 에러 처리를 하나의 공통된 팝업 창으로 해결하도록 기획되어있다. 해당 팝업은 usePopup
hook으로 호출할 수 있는데, 훅은 컴포넌트 외부에서 호출할 수 없어서 QueryClientProvider
를 감싸는 함수 컴포넌트를 만들어주었다.
const CustomQueryClientProvider = ({ children }: { children: ReactNode }) => {
const { openPopup } = usePopup();
// 최초 렌더링 시에만 초기화하기 위해 useState 사용
const [queryClient] = useState(() => {
return new QueryClient({
queryCache: new QueryCache({
onError: (error) =>
openPopup({
title: error.message,
}),
}),
});
});
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
export default CustomQueryClientProvider;
위의 컴포넌트를 기존의 QueryClientProvider
와 교체하면 된다.
// index.tsx
ReactDOM.createRoot(document.getElementById("root")!).render(
<PopupContextProvider>
<CustomQueryClientProvider>
<App />
</CustomQueryClientProvider>
</PopupContextProvider>
);
처음에는 QueryClient를 생성하는 곳에서 바로 에러 처리 로직을 작성하려고 했는데, 에러 종류에 따라 처리 방식이 조금씩 달라서 useErrorHandlers
라는 커스텀 훅을 분리해서 여기서만 에러 처리 로직을 작성하기로 했다.
export const useErrorHandlers = () => {
const { open: openPopup } = usePopup();
return (error: ErrorResponseType | Error) => {
// 서버 에러
if (error.status_code >= 500) {
return openPopup({
title: "서버와 통신 중 에러가 발생했습니다.",
});
}
// 기본 에러 메시지
openPopup({
title: error.message,
});
// 그 외 다양한 처리 로직...
};
};
그리고 useErrorHandlers
을 new QueryClient
생성 시 활용한다.
const queryClient = useMemo(
() =>
new QueryClient({
mutationCache: new MutationCache({
onError: handleError,
}),
queryCache: new QueryCache({
onError: handleError,
}),
}),
[handleError]
);
meta
필드를 사용하면 쿼리 별로 원하는 에러 메시지를 보여줄 수 있다.
useMutation({
mutationFn: payment,
meta: {
errorMessage: "결제에 실패했어요.",
},
});