<aside> ❔
서버에서 prefetchInfiniteQuery로 prefetch하는가? ├─ Yes → 로딩 UI를 <Suspense>로 처리할 것인가? │ ├─ Yes → useSuspenseInfiniteQuery ✅ (권장) │ └─ No → useInfiniteQuery (status 분기) └─ No → 클라이언트에서만 fetch하는가? ├─ 로딩 중 UI 필요 → useInfiniteQuery └─ Suspense 경계 사용 → useSuspenseInfiniteQuery ⚠️ (주의)
</aside>
https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr
https://tanstack.com/query/latest/docs/framework/react/guides/ssr
Server Component에서 prefetchInfiniteQuery로 데이터를 미리 채운 뒤,
Client Component에서 useSuspenseInfiniteQuery사용
// app/projects/page.tsx (Server Component)
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { getQueryClient } from './get-query-client'
import { ProjectList } from './project-list'
export default function ProjectsPage() {
const queryClient = getQueryClient()
// 클라이언트 컴포넌트에서 Suspense 경계 앞에 실행되는 경우,
// usePrefetchInfiniteQuery를 사용
queryClient.prefetchInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }: { pageParam: number }) =>
fetch(`/api/projects?cursor=${pageParam}`).then((r) => r.json()),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
pages: 1, // 첫 페이지만 prefetch
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ProjectList />
</HydrationBoundary>
)
}
// app/projects/project-list.tsx (Client Component)
'use client'
import { useSuspenseInfiniteQuery } from '@tanstack/react-query'
export function ProjectList() {
const { data, fetchNextPage, hasNextPage } = useSuspenseInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }: { pageParam: number }) =>
fetch(`/api/projects?cursor=${pageParam}`).then((r) => r.json()),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
// data는 항상 정의되어 있음
return <>{data.pages.map(/* ... */)}</>
}
useInfiniteQuery 사용
'use client'
import { useInfiniteQuery } from '@tanstack/react-query'
export function Comments() {
const { data, status, error, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
queryKey: ['comments'],
queryFn: ({ pageParam }: { pageParam: number }) =>
fetch(`/api/comments?cursor=${pageParam}`).then((r) => r.json()),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
if (status === 'pending') return <Spinner />
if (status === 'error') return <Error message={error.message} />
return <>{data.pages.map(/* ... */)}</>
}
| 기준 | useInfiniteQuery |
useSuspenseInfiniteQuery |
|---|---|---|
| 서버 prefetch 필수 여부 | 불필요 | 사실상 필수 |
| (안 하면 Suspend) | ||
| 로딩 UI 위치 | 컴포넌트 내부 (status === 'pending') |
상위 <Suspense> fallback |
| 에러 UI 위치 | 컴포넌트 내부 | 상위 <ErrorBoundary> |
| Pages Router 적합성 | ✅ 안전 | ⚠️ 제한적 |
| App Router + Server Component | ✅ 가능 | ✅ 권장 |
| TTFB | 빠름 | await prefetch 시 느려짐, prefetch 없이 스트리밍 시 빠름 |
| 쿼리 취소(Cancellation) | 지원 | 미지원 |
| 조건부/lazy 로드 | enabled: false로 제어 가능 |
enabled 옵션 없음 → 부적합 |
| 코드 간결성 | 분기문 필요 | 분기 없이 성공 케이스만 처리 |
https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation