React Suspense with react-query

React Suspense 와 react-query를 이용해서 더 좋은 사용자 경험 만들어보기

실전에서 React Suspensereact-query 사용해보기

이전에 작성했던
🐣
React Suspense 란 무엇일까?
포스트에서 Suspense 에 대해서 살짝 맛만 보았다. 아직 Suspense 내부 구현이 어떻게 되어있는지 알수는 없다.
페칭 라이브러리 중에서 Suspense 를 지원하는 라이브러리는 SWR react-query 정도가 일반적으로 많이 쓰이는듯 하다. 이 외에는 페칭 함수를 프로미스로 한번 더 감싸주어야 한다. 이 블로그에서는 react-query 를 사용하고 리액트 버전 18 이상을 사용하므로 Suspense 를 사용해보기 적당해 보인다.
 
이 링크 에서 react-query 에서 공식적으로 도큐먼트를 제공한다. 그리고 제일 상단에 이런 문구가 있다.
NOTE: Suspense mode for React Query is experimental, same as Suspense for data fetching itself. These APIs WILL change and should not be used in production unless you lock both your React and React Query versions to patch-level versions that are compatible with each other.
해석해보자면 react-query 에서 제공하는 Suspense 기능은 실험적 기능이고, API들이 변경될 수 있으므로 호환되는 reactreact-query 버전을 더이상 바꾸지 않는 한 프로덕션에서는 사용해서는 안된다는 노트이다.
회사에서 제공하는 서비스나 프로젝트에서는 할 수 없겠지만, 나의경우에는 리스크가 없는 개인 블로그이다. 충분히 시도할 수 있다.
 
// Configure for all queries import { QueryClient, QueryClientProvider } from '@tanstack/react-query' const queryClient = new QueryClient({ defaultOptions: { queries: { suspense: true, }, }, }) function Root() { return ( <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> ) }
suspense 옵션을 true 로 활성화 해준다. 이건 클라이언트 전체에 글로벌 설정을 하는것이다. 특정 쿼리마다 적용하고 싶다면 쿼리에 직접 적용할 수 있다.
import { useQuery } from '@tanstack/react-query' // Enable for an individual query useQuery({ queryKey, queryFn, suspense: true })
이렇게 Suspense 모드를 활성화 했다면 status 상태와 error 객체는 더이상 필요하지 않고 React.Suspense 컴포넌트의 사용으로 대체된다. 그리고 query와 mutation은 약간 다르게 동작한다. mutation의 실패시 제공되는 error 객체는 가장 가까운 에러바운더리로 전달된다. 이것을 원하지 않으면 useErrorBoundary 옵션을 false 로 제공하면 된다. 아예 에러를 막으려면 throwOnError 옵션을 false 로 막으면 된다.
 

실전에서 적용해보기

먼저 블로그에서 유일하게 로딩을 표시할만곳인 포스트 리스트에 적용해보기로 했다.
// components/Posts/Posts.tsx const Posts: React.FC<IPostsProps> = ({ className, initialPosts, selectedCategory }) => { return ( <div className={`posts-wrapper ${className}`}> <QueryErrorResetBoundary> <CustomSuspense fallback={ <> <li> <PostCard.Skeleton /> </li> <li> <PostCard.Skeleton /> </li> <li> <PostCard.Skeleton /> </li> </> } > <PostList initialPosts={initialPosts} selectedCategory={selectedCategory} /> </CustomSuspense> </QueryErrorResetBoundary> </div> ); }; export default memo(Posts); const PostList: React.FC<IPostsProps> = ({ initialPosts, selectedCategory }) => { const mounted = useMounted(); const { data } = useGetPosts( { filter: JSON.stringify({ categories: selectedCategory }) }, { suspense: true, useErrorBoundary: true, initialData: !mounted ? initialPosts : undefined } ); return ( <ul> {data?.data.map((post) => ( <li key={post.id}> <PostCard.Contents data={post} /> </li> ))} </ul> ); };
수정한 전체적인 코드는 이렇다.
 
<QueryErrorResetBoundary> <CustomSuspense fallback={ <> <li> <PostCard.Skeleton /> </li> <li> <PostCard.Skeleton /> </li> <li> <PostCard.Skeleton /> </li> </> } > <PostList initialPosts={initialPosts} selectedCategory={selectedCategory} /> </CustomSuspense> </QueryErrorResetBoundary>
사실 아직까지는 에러가 날 일이 거의 없다. (노션측 API 에러가 아니라면) 아직 제대로 된 에러 핸들러도 없고 에러 때문에 화면이 깨지는 것만 막으려고 바운더리를 쳐놨다. 그리고 기존에 리스트를 렌더하는 부분을 컴포넌트로 한번 더 묶어서 CustomSuspense 의 자식컴포넌트로 두었다. 이렇게 되어야 제대로 바운더리에 걸린다.
그리고 적당한 스켈레톤을 만들어서 fallback 에 넣어두었다.
 
 
CustomSuspenseuseMounted 는 다음과 같다.
// CustomSuspesen.tsx const CustomSuspense = (props: ComponentProps<typeof Suspense>) => { const isMounted = useMounted(); if (isMounted) { return <Suspense {...props} />; } return <>{props.fallback}</>; }; export default CustomSuspense;
// useMounted.tsx const useMounted = () => { const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); return mounted; }; export default useMounted;
 
포스트 리스트를 조금 더 자세히 보자면
const PostList: React.FC<IPostsProps> = ({ initialPosts, selectedCategory }) => { const mounted = useMounted(); const { data } = useGetPosts( { filter: JSON.stringify({ categories: selectedCategory }) }, { suspense: true, useErrorBoundary: true, initialData: !mounted ? initialPosts : undefined } ); return ( <ul> {data?.data.map((post) => ( <li key={post.id}> <PostCard.Contents data={post} /> </li> ))} </ul> ); };
useGetPosts 라는 커스텀 query 훅에 suspense 옵션을 true 로 활성화 했다.
initialData 가 있다면 제대로 제대로 Suspense 가 일어나지 않는듯 보였다. 이 부분은 react-query 깃헙에도 문의를 남겨둔 상태이다. 지금은 어찌되었든 기존에 SSR을 유지하기 위해서 initialData 를 추가해두고 마운트가 된 이후에 undefined 로 제거했다.
 

마무리

일단은 원래 렌더를 하던 방식보다 확실히 개발시 간단한 장점이 있는것 같다. 개발자들의 수고를 꽤 많은부분이 라이브러리나 프레임워크가 보장해주는듯??
어찌저찌 잘 적용이 되었다. 추후에 Best Practice 에 대해 조금더 공부할 필요가 있어보인다.
 

레퍼런스