2025년 04월 10일
Tanstack Query
서버 컴포넌트에서 데이터를 미리 불러오고(pre-fetch) 이를 클라이언트에 전달하는 패턴이 있지만, 이 방식이 클라이언트 데이터 갱신과 별개로 작동한다는 점이 핵심이에요. 서버에서 prefetch된 데이터는 초기 로드 시에는 별다른 fallback 없이 바로 보여지지만, 클라이언트에서 staleTime 이후 다시 데이터를 요청할 때는 Suspense의 fallback UI가 필요하죠. useSuspenseQuery는 로딩과 에러 관리를 선언적으로 처리하며, 이를 Suspense와 함께 사용하면 더 간단하게 UI 상태를 제어할 수 있어요. 결론적으로, 서버 prefetch와 클라이언트의 데이터 refetch는 별개이기 때문에, 클라이언트 갱신 시에는 Suspense fallback이 필요하며, useSuspenseQuery는 이를 더욱 쉽게 구현할 수 있게 도와준다는 내용입니다.
서버 컴포넌트에서 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
를 사용하면 로딩 상태와 에러 처리를 선언적
으로 관리할 수 있습니다.