<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

상황 1: App Router + 서버에서 prefetch (권장)


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(/* ... */)}</>
}

상황 2: 클라이언트에서만 데이터 fetch


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