<aside> 📚 Suspense 다이브
Suspense는 React16에서 처음 등장했으며, React.lazy()와 함께 사용하여 Code Splitting을 할 수 있었다.
React.lazy()를 사용하여 초기에 불필요한 컴포넌트를 렌더링하지 않을 수 있다.
React18로 넘어오면서 data fetching에도 확대 적용이 되었다. 비동기 처리에 대한 책임을 Suspense에 위임할 수 있다.
<aside> ❓ Suspense는 하위 children 컴포넌트들의 비동기 상태를 어떻게 감지할까?
핵심은 하위 컴포넌트에서 Promise를 throw한다는 것이다. promise가 pending 혹은 error라면 상위로 throw하고, 데이터가 준비된 시점에는 response를 return한다. error를 throw하면 errorboundary와 상호작용을 하는 것이다.
아래의 wrapPromise함수는 Suspense와 자식 컴포넌트가 어떻게 소통하는 지에 대한 힌트를 얻을 수 있다.
function wrapPromise(promise) {
let status = "pending";
let response;
const suspender = promise.then(
(res) => {
status = "success";
response = res;
},
(err) => {
status = "error";
response = err;
}
);
const read = () => {
switch (status) {
case "pending":
throw suspender;
case "error":
throw response;
default:
return response;
}
};
return { read };
}
export default wrapPromise;
wrapPromise는 promise를 한 번 감싸서, promise가 pending 혹은 erorr상태라면 상위로 throw하고, 데이터가 준비된 시점에는 response를 return한다.
이렇게 Promise 상태에 따라 상위로 throw함으로써 상위에 존재하는 Suspense, ErrorBoundary컴포넌트와 커뮤니케이션할 수 있다.
</aside>
</aside>
<>
<Suspense fallback={<Loading />}>
<GroupingHeader />
<HydrationProvider queryFn={() => getGroups(0)} queryKey={Keys.getGroups()} isInfiniteQuery>
<GroupingCardList />
<CreateGroupButton />
</HydrationProvider>
</Suspense>
<Spacing size={60} />
</>
위 코드에서, Suspense는 데이터를 미리 서버에서 받아오는 HydrationProvider컴포넌트를 감싸고 있다. 이 컴포넌트가 Next.js서버에서 먼저 실행되고, 그 안의 비동기 함수가 pending 상태라면 Suspense의 fallback으로 넘긴 컴포넌트를 보여주는 것이다.
<>
<GroupingHeader />
<Suspense fallback={<Loading />}>
<HydrationProvider queryFn={() => getGroups(0)} queryKey={Keys.getGroups()} isInfiniteQuery>
<GroupingCardList />
</HydrationProvider>
</Suspense>
<CreateGroupButton />
<Spacing size={60} />
</>
현재 컴포넌트 구조는 다음과 같다.
<>
<GroupDetailHeader />
<Suspense fallback={<Loading className="h-[calc(100dvh-48px)]" />}>
<HydrationProvider
queryMultipleFn={[
() => getGroupDetail(groupId),
() => getGroupMembers(groupId),
() => getNotices(groupId),
]}
queryMultipleKey={[
Keys.getGroupDetail(groupId),
Keys.getGroupMembers(groupId),
Keys.getNotices(groupId),
]}
>
<GroupDetailPage />
</HydrationProvider>
</Suspense>
</>
Suspense > HydrationProvider가 모든 하위 컴포넌트를 감싸고 있다.
1,2번 째 api인 getGroupDetail과 getGroupMembers가 첫 번째 탭, 3번째 api인 getNotices는 두 번째 탭에서 필요한 데이터이다.
이 데이터들을 모두 첫 페이지 로드할 때부터 불러올 필요가 있을까?
이렇게 구현할 때는 세 개의 api를 서버 단에서 모두 병렬적으로 받아오기에 성능상 차이가 없을 것이라 생각했다. 정말로 과연 그럴까?
// HydrationProvider
if (queryMultipleFn && queryMultipleKey) {
await Promise.all(
queryMultipleFn.map((queryFn, index) => {
return queryClient.prefetchQuery({ queryKey: queryMultipleKey[index], queryFn });
})
);
}
여러 함수를 넘기면 서버 단에서는 Promise.all을 사용한다. Promise.all은 병렬로 비동기 처리를 수행한다. 단, 가장 마지막으로 응답이 돌아온 api까지 완료되었을 때 resolve처리를 한다.