Promise.all()
과 같은 방식으로 동시에 비동기 처리를 함으로써 Fetch-on-render의 동시성 보장 문제를 해결할 수 있다. 하지만, 자식 컴포넌트에서 처리해야 할 데이터를 부모 컴포넌트에 위임함으로써 관심사 분리가 제대로 이루어지지 못하는 문제가 있다.💡 자식 컴포넌트 로드 전 대체 UI를 보여주는 컴포넌트
Suspense는 자식 요소를 로드하기 전까지 화면에 대체 UI를 제공하는 React의 컴포넌트이다.
prop | 설명 |
---|---|
children | 렌더링하려는 실제 UI |
fallback | 실제 UI가 로딩되기 전까지 대신 렌더링되는 대체 UI로, 보통 로딩 스피너나 스켈레톤처럼 간단한 Placeholder를 활용한다. |
주의할 점
모든 컴포넌트 주위에 Suspense를 두지 마세요. Suspense는 사용자가 경험하기를 원하는 로딩 순서보다 더 세분화되어서는 안 됩니다. 디자이너와 함께 작업하는 경우 로딩 상태를 어디에 배치해야 하는지 디자이너에게 물어보세요. 디자이너가 이미 디자인 와이어 프레임에 포함했을 가능성이 높습니다. - 리액트 공식문서
사용자의 최근 기록을 조회하는 페이지에서는 데이터를 비동기로 불러오고 화면에 로딩중, 에러에 따른 분기 처리를 해주었다. 우선 데이터를 불러오는 코드에서 tanstack query(리액트 쿼리)의 useInfiniteQuery 훅을 useSuspenseInfiniteQuery 훅으로 교체한다. Suspense는 자식 컴포넌트로부터 throw된 promise의 상태에 따라 fallback을 보여줄지를 결정할 수 있다. 기존의 useInfiniteQuery는 기본적으로 promise를 반환하는 것이 아닌 빈 데이터에 점진적으로 데이터를 채워넣는 방식을 취하기 때문에 Suspense를 적용할 수 없다. 따라서, Suspense를 지원하는 useSuspenseInfiniteQuery를 사용해야 한다.
useSuspenseQuery
이와 마찬가지로 Suspense를 지원하는 useQuery가 존재한다.
// recent-history.tsx
// COMPONENT: 최근 기록
import { useEffect, useState } from "react";
import OvalLoadingSpinner from "../../../../_components/loading-spinner/oval-loading-spinner";
import { useGetRecentDriverActions } from "../../../../api/action";
import LoadingErrorIcon from "../../../../_components/icon/loading-error-icon";
export default function RecentHistory() {
const {
data: recentDriverActionsPages,
isLoading,
isError,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useGetRecentDriverActions();
useEffect(() => {
if (hasNextPage && inView) {
fetchNextPage();
}
});
// 에러 발생 시 에러 메시지 표시
if (isError)
return (
<ContentBlockWrapper height={"16rem"}>
<LoadingErrorIcon color="red" size="2.4rem" />
</ContentBlockWrapper>
);
return (
<ContentBlockWrapper height={"20rem"} padding={"1rem"}>
<S.Header>최근 기록</S.Header>
<S.RecentHistoryContainer>
<S.RecentHistoryWrapper>
{/* 무한스크롤로 데이터 페칭: page[]>page>item 구조 */}
{isLoading ? (
<OvalLoadingSpinner />
) : (
<>
{recentDriverActionsPages?.pages.map(
(recentDriverActionsPage: IDriverActionResponse["action"][]) =>
recentDriverActionsPage?.map(
(
recentDriverActionItem: IDriverActionResponse["action"],
idx: number
) => <S.HistoryItem key={recentDriverActionItem.id} />
)
)}
</>
)}
{isFetchingNextPage && (
<div>
<OvalLoadingSpinner />
</div>
)}
</S.RecentHistoryWrapper>
</S.RecentHistoryContainer>
</ContentBlockWrapper>
);
}
// recent-history.tsx
// COMPONENT: 최근 기록
import { Suspense } from "react";
export default function RecentHistory() {
return (
<ContentBlockWrapper height={"20rem"} padding={"1rem"}>
<S.Header>최근 기록</S.Header>
<Suspense fallback={<OvalLoadingSpinner />}>
<RecentHistoryContent />
</Suspense>
</ContentBlockWrapper>
);
}
// recent-history-content.tsx
// COMPONENT: 최근 운전자 행위 기록 내용 (데이터)
import { useEffect } from "react";
import { useGetRecentDriverActions } from "../../../../../api/action";
export default function RecentHistoryContent() {
// 데이터 페칭 수행
const {
data: recentDriverActionsPages,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useGetRecentDriverActions();
return (
<S.RecentHistoryContainer>
<S.RecentHistoryWrapper>
{/* 무한스크롤 가져온 데이터: page[]>page>item 구조 */}
</S.RecentHistoryWrapper>
</S.RecentHistoryContainer>
);
}
개발자 탭의 네트워크 환경을 느린 속도로 바꾸어 진행했다.
참고자료
[10분 테코톡] 클린의 서스펜스와 에러바운더리
React Suspense 소개 (feat. React v18)