Suspense란?
Suspense는 아직 렌더링이 준비되지 않은 컴포넌트가 있을때 로딩 화면을 보여주고 로딩이 완료되면 해당 컴포넌트를 보여주는 React에 내장되어 있는 기능입니다.
Suspense를 사용하려는 이유
- 복잡한 비동기 처리를 해결하기 위해
⇒ 로딩시 , 성공시, 실패시를 선언적으로 처리 가능
Suspense 도입을 고민하게 된 사례
- ReceivedPostListPage.tsx - isLoading, data값의 존재 유무등으로 처리
function ReceivedPostListPage() { const { data, isLoading } = useChannelPost(); return ( <> <PostList type="post" posts={!isLoading && data ? (data as UserPost[]) : []} /> </> ); }
- Channel.tsx - 비동기 값을 한번더 비동기(useState)로 묶어서 사용
function Channel() { const [data, setData] = useState({ name: '프룽', posts: [], description: '' }); const [visible, handleModalClick] = useModal(); const [isOpened, setIsOpened] = useState(false); const { channelName } = useParams(); const { data: channelInfo } = useChannelQuery(channelName ?? ''); useEffect(() => { if (channelInfo) setData(channelInfo); console.log(channelInfo); }, [channelInfo]);
각 방법의 공통 문제점
- 받아온 데이터에 관한 처리가 복잡합니다. 지금은 성공시의 화면만 신경써서 data가 undefined일 때만 처리하고 있지만 실패시, 로딩시 화면까지 처리하려 한다면 더욱 복잡해집니다,
Suspense 사용시 이점
성공시 처리는 데이터가
undefined
가 아닌 상태를 보장하므로 위와 같은 처리가 필요없고 실패시, 로딩시는 ErrorBoundary, Suspense fallback ui로 선언적으로 나타낼 수 있습니다.Suspense 적용
tanstack query의 suspense
tanstack query에서 suspense 기능을 제공해줍니다.
v5부터 suspense=true는 deprecated되었고 대신
useSuspenseQuery
, useSuspenseQueries
, useSuspenseInfiniteQuery
훅을 제공해줍니다.

위의 3가지 훅을 사용할 시 타입 레벨에서
data
가 undefined
상태가 되지 않습니다.⇒
undefined
인 상태에서는 Suspense에서의 fullback UI가 렌더링 되기 때문입니다.Suspense 사용
suspense를 사용할 mypage컴포넌트의 상위에 작성해줍니다. fallback에 로딩시 보여줄 컴포넌트를 작성합니다.
import { Suspense } from 'react'; ... <Suspense fallback={<fallbackUi />}> <PostListPage /> </Suspense>
tanstack query hook 수정
useQuery → useSuspenseQuery
useQueries→ useSuspenseQueries
로 변경해주면 됩니다.
- ReceivedPostListPage.tsx
function ReceivedPostListPage() { const { data, isLoading } = useChannelPost(); console.log(isLoading, data); return ( <> <PostList type="post" posts={!isLoading && data ? (data as UserPost[]) : []} /> </> ); }

isLoading이 true가 되어도 data가 undefined인 경우가 있습니다. 때문에 따로 예외처리가 필요했는데 suspense훅을 사용할 경우 undefined가 아닌 타입을 보장해서 불필요한 코드를 줄일 수 있습니다.
수정후
- ReceivedPostListPage.tsx
function ReceivedPostListPage() { const { data } = useChannelPost(); return ( <> <PostList type="post" posts={data} /> </> ); }
- Channel.tsx
function Channel() { // const [data, setData] = useState({ // name: '프룽', // posts: [], // description: '' // }); const [visible, handleModalClick] = useModal(); const [isOpened, setIsOpened] = useState(false); const { channelName } = useParams(); const { data: channelInfo } = useChannelQuery(channelName ?? ''); // useEffect(() => { // if (channelInfo) setData(channelInfo); // console.log(channelInfo); // }, [channelInfo]);
useChannelQuery를 suspenseQuery로 변경했기때문에 주석처리된 부분을 제거할 수 있습니다.
- AppRouter.ts
component: ( <Suspense fallback={<CommentSkeleton />}> <CommentListPage /> </Suspense> )
⚠ Suspense로 감싸주지 않으면 로딩되는 동안 보여줄 페이지가 없어서 오류가 발생하므로 반드시 감싸줘야 합니다.
적용 화면

ErrorBoundary
suspense의 fullback은 로딩중일때의 ui를 그리고 실패시의 ui는 ErrorBoundary에서 처리 가능합니다.
간편한 에러처리를 위해
react-error-boundary
라이브러리를 설치해서 사용했습니다.- App.tsx
{userRoutes.mypage.map((route, idx) => ( <Route path={route.path} element={ <QueryErrorBoundary> <AuthMiddleware> <>{route.component}</> </AuthMiddleware> </QueryErrorBoundary> } key={idx} ></Route> ))}
function QueryErrorBoundary({ children }: ErrorFullback) { const { reset } = useQueryErrorResetBoundary(); const location = useLocation(); return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <Style.Container> {mypagePathToSkeletonType[location.pathname] === 'post' ? ( <PostSkeleton /> ) : null} {mypagePathToSkeletonType[location.pathname] === 'comment' ? ( <CommentSkeleton /> ) : null} {mypagePathToSkeletonType[location.pathname] === 'follow' ? ( <FollowSkeleton /> ) : null} <div className="info-container"> <Style.InfoWrap> <span>⚠ 문제가 발생했습니다</span> <button className="button" onClick={() => { resetErrorBoundary(); }}> 다시 시도하기 </button> </Style.InfoWrap> </div> </Style.Container> )}> {children} </ErrorBoundary> ); }
각 pathname마다 다른 화면을 보여주기 위해 QueryErrorBoundary컴포넌트를 만들어 사용했습니다.
적용 화면

이렇게 하면 로딩중 컴포넌트, 성공시 컴포넌트, 실패시 컴포넌트를 선언적으로 각각 처리할 수 있습니다.
설명이 부족했다면 언제든 물어봐 주세요. 그리고 틀린 게 있거나 다른 더 좋은 방법이 있다면 언제든 공유해주시면 바로 반영하겠습니다!!
[참고자료]
- tanstack query suspense
- suspense
- errory boundary