react-query 는 아래와 같이 로딩과 에러 처리에 대한 상태 데이터를 개발자가 사용하게 편리한 형태로 제공해준다.
const { data, isLoading, isError } = useQuery({
queryKey: ['key'],
queryFn: getData,
});
하지만 이렇게 제공되는 데이터를 어떻게 UI 요소로까지 연결시킬지는 고민해보아야 할 문제다.
react-query 에서 GET 메서드 네트워크 요청을 하기 위해선 useQuery 를, 서버의 상태를 변경 시키는 PUT, DELETE, PATCH 등의 요청을 위해선 useMutation 을 사용해야한다.
문제는 이 두 메서드의 반환값이나 옵션이 미묘하게 다르단 점이다.
에러를 처리하기 위해선 useQuery 는 반환값을 사용하고, useMutation 은 콜백함수를 사용한다.
const { isError } = useQuery();
const mutate = useMutation({
onError: () => {}
})
이 두개의 경우에 대해서 각각 어떻게 에러 처리를 해야할지 고민을 해야한다.
다른 사람들은 react-query 를 사용하여 어떻게 로딩과 에러 처리를 해주는지를 구글링 해보았다. 찾아보니 useQuery 와 useMutation 의 경우 반환값이나 콜백함수 등도 다르지만, 이 둘의 로딩과 에러를 분리해서 처리해야하는 이유에 대해 알게 되었다.
서버의 상태를 변경 시키는 PUT, DELETE 등의 메서드와 달리 GET 요청은 서버로부터 데이터를 받아오기만 하는 아주 간단한 요청이다.
클라이언트 측에서 잘못된 요청을 할 일이 거의없다. 이렇게 간단한 GET 요청에서 에러가 난다는 것은 무슨 의미일까?
이 경우엔 서버에 문제가 있어 데이터를 받아오지 못한다고 판단한다. 즉 서비스에 문제가 생겼을 가능성이 높다. 이런 경우 페이지 전체를 리다이렉팅 하는 방식을 사용한다고 한다.
이와 달리 서버로 데이터 변경 요청을 보내는 메서드에서 에러가 발생한 경우 Toast UI 등으로 간단하게 에러 표시를 하는 것이 일반적으로 쓰는 방법처럼 보였다.
그래서 useQuery 와 useMutation 은 조금 다르게 로딩 및 에러를 처리할 수 밖에 없겠다는 생각이 들었다.
useQuery 의 로딩은 React.Suspense 와 react-query 의 useSuspenseQuery 를 사용하여 처리했다.
사용방법은 아래와 같다.
import { Suspense } from 'react';
<Suspense fallback={<Loading/>}>
{children}
</Suspense>
이렇게 하위 컴포넌트를 Suspense 태그로 감싸주면 된다. 하위 컴포넌트에서 useSuspenseQuery 를 사용하여 데이터를 가져오게 되면 자동으로 로딩 컴포넌트를 보여주게 된다.
다이어리 페이지의 사이드바가 데이터를 받아올 때 해당 방식을 사용했다.
const DiarySideBar = ({ children }: PropsWithChildren) => {
const { methods, sideBarToggle } = useDiaryContext();
const { toggleSideBar } = methods.handleSideBar;
return (
<StyledDiarySideBar className={sideBarToggle ? 'open' : 'closed'}>
<StyledToggleButton onClick={toggleSideBar}>
<StyledArrowIcon className={sideBarToggle ? 'open' : 'closed'} />
</StyledToggleButton>
<ErrorBoundary FallbackComponent={SideBarFallBack}>
<Suspense fallback={<Loading size="large" />}>
{children}
</Suspense>
</ErrorBoundary>
</StyledDiarySideBar>
);
};
아래는 사이드바 로딩 화면은 아니고 페이지 전체 로딩 화면이다. 하지만 전체적인 코드 구조는 사이드바와 비슷하기 때문에 이해를 위해 사진을 가져왔다.

데이터를 받아오는 useQuery 의 경우 데이터를 받아오지 않으면 페이지 자체가 채워지지 않는 경우가 많기 때문에 Suspense 태그를 사용하였다.
하지만 useMutation 의 경우 사용자와 상호작용을 통해 로딩이 발생하기 때문에 Suspense 를 사용한 처리보다는 직접 loading 에 대한 state 를 만들어주었다.
const [loading, setLoading] = useState(false);
그리고 useMutation 의 콜백함수인 onMutate, onSettled 를 통해 Loading 을 관리했다.
return useMutation({
onMutate: () => {
setLoading(true);
},
onSettled: () => {
setLoading(false);
}
})
포스트를 발행할 경우 중복 발행을 막기 위해 Loading 상태가 true 일시 발행버튼을 disabled 로 만들어주거나, 이에 걸맞은 스타일을 입히기 위해 유용했다.
const { loadingStatus } = context;
return (
<Button
size="large"
disabled={loadingStatus}
/>
)
에러는 useQuery 와 useMutation 모두 Errorboundary 를 통해 처리해주었다.
React 의 Error Boundary 는 클래스 형태로 이뤄져 있다. 우리는 빠르고 쉬운 에러 처리를 원했기 때문에 react-error-boundary 라는 라이브러리를 사용하여 Suspense 처럼 폴백 컴포넌트를 넘겨주는 방식을 사용했다.
위 사이드바 코드에서도 Error boundary 가 사용된 모습을 확인할 수 있다.
<ErrorBoundary FallbackComponent={SideBarFallBack}>
<Suspense fallback={<Loading size="large" />}>
{children}
</Suspense>
</ErrorBoundary>
Error boundary 는 Error 객체가 반환되면 가장 가까이 있는 Error boundary 컴포넌트에서 해당 에러를 캐치하여 UI 형태로 처리할 수 있다.
if (isError) {
throw new Error('알 수 없는 에러가 발생했습니다 😢')
}
이렇게 Error 객체를 통해 넘겨준 메세지를 불러올 수도 있다.
const SideBarFallBack = ({ error, resetErrorBoundary }) => {
return (
<div> {error.message} </div>
)
}
에러와 로딩을 지금은 일일이 코드에서 선언하여 사용하고 있다. 구글링을 해보니 에러에 관한 메세지를 따온 모아서 커스텀 훅으로 관리하거나, queryClient 를 사용하여 전역적으로 기본 에러 핸들링 함수를 설정하는 방법도 있다고 한다.
또 에러와 로딩 처리를 프로젝트 끝 부분에서 한 나머지 에러 바운더리가 적용되지 않은 곳도 보인다. 추후 페이지를 업그레이드 하며 아래의 부분을 적용시킬 생각이다.