현재는 성공한 코드에 대한 처리만 되어있고 로딩이나 에러에 관한 처리가 되어있지 않은 상태이다. 작업을 하기 전에 꼭 보면 좋을 영상을 추천받아서 먼저 보게 되었다. 이어질 내용은 토스 | slash21 - 프론트엔드 웹 서비스에서 우아하게 비동기 처리하기 영상에 나온 내용이다.
function fetchAccounts(callback) {
fetchUserEntity((err, user) => {
if (err != null) {
callback(err, null);
return;
}
fetchUserAccounts(user.no, (err,accounts) => {
if (err != null) {
callback(err, null);
return;
}
callback(null, accounts);
});
});
}
위 코드의 문제점은 성공하는 경우와 실패하는 경우가 분리되어 있지 않다는 것이다. 두 경우가 섞여서 처리되면 함수의 진짜 역할이 가려지고 코드를 작성하는 입장에서 매번 에러 유무를 확인해야 한다.
async function fetchAccounts() {
const user = await fetchUserEntity();
const accounts = await fetchUserAccounts(user.no);
return accounts;
}
반면 위의 코드는 성공하는 경우만 다루고 실패하는 경우는 catch 절에서 분리해서 처리할 수 있다. 실패할 수 있는 코드에 대한 처리를 외부에 위임할 수 있는 것이다.
이렇게 살펴본 좋은 코드의 특징은 아래와 같다.
- 성공, 실패의 경우를 분리해 처리할 수 있다.
- 비즈니스 로직을 한눈에 파악할 수 있다.
우리는 기존에 swr이나 react-query등을 사용해서 비동기 처리를 구현해왔다. (굉장히 찔리는 대목이었다..)
function Profile() {
const foo = useAsyncValue(()=>{
return fetchFoo();
});
if(foo.error) return <div>로딩에 실패했습니다.</div>
if(!foo.data) return <div>로딩중입니다...</div>
return <div>{foo.data.name}님 안녕하세요</div>
}
성공하는 경우와 실패하는 경우가 섞여서 처리되고 있었음을 알수있다. 이렇게 되면 실패하는 경우에 대한 처리를 외부에 위임하기가 어렵다고 한다. 이러한 문제는 여러개의 비동기 작업이 동시에 실행되면 더 심화된다.
<ErrorBoundary fallback={<MyErrorPage />}>
<Suspense fallback={<Loader />}>
<Profile />
</Suspense>
</ErrorBoundary>
위와 같이 Profile
컴포넌트를 ErrorBoundary
와 Suspense
로 감싸게 되면 에러 상태와 로딩 상태를 분리할 수 있다. 에러는 MyErrorPage
컴포넌트에서, 로딩 처리는 Loader
컴포넌트에서 처리하면 된다. 이렇게 하면 마치 동기적인 코드처럼 깔끔하게 처리할 수 있다.
현재 프로젝트에서 사용하고 있는 react-query에서 error 처리를 옵션으로 제공해주니 사용해봐야겠다.
에러를 구분해보니 페이지로 이동시켜줘야 할 경우와 snackbar 메시지만 띄워줘야 할 경우가 있었다.
작업을 하다보니 react-router-dom
설정 - queryClient 설정 - 각 요청마다 재설정 이렇게 돌아돌아 설정을 하게 되었는데 그 과정을 정리해보았다.
react-router-dom
설정내가 사용한 react-router-dom의 createBrowser에서는 라우팅시 에러가 생기면 자동으로 그들이 만들어 놓은 페이지로 이동시켰다. 못생긴 페이지로 말이다. 이것을 커스텀하려면 errorElement라는 걸 추가해주어야 했다.
const router = createBrowserRouter([
{
path: MAIN_URL,
element: <Root />,
errorElement: <ErrorPage />,
children: [
{ index: true, loader: mainLoader },
{
path: LOGOUT_URL,
loader: logoutLoader,
},
...
},
]);
추가한 errorElement
에서 메시지를 상황별로 보여줘야 할 것 같은데 어떻게 할지 막막했다. react query 공식문서의 예시에서 힌트를 얻을 수 있었다.
아래와 같이 useRouterError 훅을 사용해서 라우팅시 생기는 에러를 받아올 수 있었고 따로 interface를 정의해주었다. 꼭 라우팅 에러가 아닌 axiosError가 발생한다해도 모두 이 페이지로 받아오므로 AxiosError도 따로 정의해주었다.
import * as React from "react";
import { useRouteError } from "react-router-dom";
import { AxiosError } from "axios";
interface RouteError {
statusText: string;
message: string;
}
interface CustomAxiosError extends AxiosError<{ details: string }> {}
export default function Error() {
const error = useRouteError();
const isAxiosError = error instanceof AxiosError;
return (
<div>
<h1>Oops!</h1>
<p>Sorry, an unexpected error has occurred.</p>
<p>
<i>
{isAxiosError
? (error as CustomAxiosError)?.response?.data?.details
: (error as RouteError).statusText || (error as RouteError).message}
</i>
</p>
</div>
);
}
queryClient
설정위에서 말했듯이 어떤 에러가 발생하더라도 모두 에러페이지로 이동하므로 이동할 필요가 없을때를 따로 설정해줄 필요가 있었다. 예를 들어 로그인시 비밀번호가 틀려서 생기는 에러와 같은 경우에 말이다.
QueryClient의 useErrorBoundary를 true로 설정해줘서 네트워크 에러를 reactQuery에서 처리해줄 수 있도록 하였다.
const client = new QueryClient({
defaultOptions: {
queries: {
retry: 0,
useErrorBoundary: true,
},
},
});
하지만 이때 500에러는 따로 에러페이지로 이동시켜줘야하므로 각 요청 query와 mutation에 아래와 같이 useErrorBoundary를 재설정해주었다.
export const useDeleteTodoItem = (id: string) => {
return useMutation({
...
useErrorBoundary: (error) =>
!!error.response ? error.response?.status >= 500 : false,
});
};