2025년 04월 10일
Tanstack Query
서버 컴포넌트에서 prefetch를 하고, dehydratedState를 자식 컴포넌트로 전달하는 패턴을 사용하면서 든 궁금증이였습니다. 이미 서버에서 데이터를 prefetch하여 렌더링 준비를 마친 HTML을 생성하고 클라이언트로 넘겨주는 패턴을 사용할경우 Suspense 컴포넌트를 이용해 fallback UI를 보여줄 필요가 과연 생길까?
라는 생각이였는데요. 우선 명심해야 할 것은 서버 컴포넌트에서의 prefetch와 클라이언트에서의 데이터 갱신은 서로 다른 사이클로 동작한다는 것 입니다.
서버 컴포넌트
에서의 prefetch가 실행되는 조건revalidatePath
혹은 revalidateTag
, router.refresh
를 사용하여 강제로 서버 컴포넌트를 갱신할때params
나 searchParams
가 변경되어 페이지가 다시 로드될 때클라이언트 컴포넌트
에서의 데이터 갱신이 경우 클라이언트 상태가 갱신될 뿐 서버 컴포넌트 재실행을 트리거하지는 않습니다. 결과적으로 클라이언트에서 데이터가 갱신되더라도 서버의 prefetch 데이터는 자동으로 업데이트 되지 않습니다.
결과적으로 prefetch가 다시 실행되는 조건은 위에 언급된 상황들인데요. 클라이언트 컴포넌트에서 기본 default stale time이 지난경우 서버에서 다시 prefetch가 되지 않고 클라이언트에서 데이터가 다시 요청되는데 이때 해당 자식 컴포넌트를 Suspense로 감쌀경우 fallback UI를 보여줄 수 있게 되는 것입니다.
특성 | useQuery | useSuspenseQuery |
---|---|---|
로딩 상태 | 컴포넌트 내에서 isLoading 으로 처리 | Suspense 컴포넌트에 위임 |
에러 처리 | error 를 throw 해야함 | 자동으로 throw (개발자가 throw 할 필요 없음) |
자식 컴포넌트에서 useSuspenseQuery
를 이용해 캐싱된 데이터를 이용하면서 fallback UI를 보여줄 필요가 있는 경우, 해당 자식 컴포넌트를 Suspense
로 감싸면 됩니다.
이로써 로딩 상태와 관련된 관심사를 분리하고, 선언적으로 코드를 작성할 수 있습니다.
또한 useSuspenseQuery
는 에러 발생 시 자동으로 throw
하기 때문에, 별도의 에러 처리 로직이 필요 없습니다.
반면 useQuery
는 수동으로 throw 해야 ErrorBoundary
에서 잡을 수 있습니다.
1// useQuery 사용 시 에러 처리 코드 2const { data, error } = useQuery({ ... }); 3if (error) throw error; // ErrorBoundary가 에러를 포착하도록 수동으로 throw 4 5// useSuspenseQuery 사용 시 에러 처리 코드 불필요 6const { data } = useSuspenseQuery({ ... }); 7// 에러 발생 시 자동으로 throw되어 ErrorBoundary가 포착
1// 서버 컴포넌트 2async function ProductPage({ id }) { 3 // 서버에서 prefetch 4 await prefetchQuery({ 5 queryKey: ['product', id], 6 queryFn: () => fetchProduct(id), 7 staleTime: 60000 // 1분 8 }); 9 10 return ( 11 <HydrationBoundary state={dehydrate(queryClient)}> 12 <ErrorBoundary fallback={<ProductError />}> 13 <Suspense fallback={<ProductSkeleton />}> 14 <ProductDetails id={id} /> 15 </Suspense> 16 </ErrorBoundary> 17 </HydrationBoundary> 18 ); 19}
1'use client' 2 3function ProductDetails({ id }) { 4 const { data } = useSuspenseQuery({ 5 queryKey: ['product', id], 6 queryFn: () => fetchProduct(id), 7 staleTime: 60000 8 }); 9 10 return <div>{data.name} - {data.price}원</div>; 11}
useSuspenseQuery
를 사용하면 로딩 상태와 에러 처리를 선언적
으로 관리할 수 있습니다.