무엇
useSuspenseInfiniteQuery는 무한 스크롤용 useInfiniteQuery의 Suspense 판이다. 데이터를 기다리는 동안 컴포넌트가 멈추고(suspend), 렌더에 도달했을 땐 데이터가 이미 있다.
일반 useInfiniteQuery는 로딩 상태를 직접 다뤄야 하지만, 이 훅은 로딩을 바깥 Suspense 경계에 맡긴다. 그래서 컴포넌트 안에서는 data가 항상 있다고 보고 쓴다.
무슨 일이 일어나나
첫 페이지를 불러오는 동안 컴포넌트는 렌더되지 않고 멈춘다. 위쪽 <Suspense>의 fallback이 그 자리를 대신 그린다. 데이터가 준비되면 컴포넌트가 렌더되고, 이때 data는 반드시 정의돼 있다.
그래서 반환 타입이 useInfiniteQuery와 다르다. status는 success나 error만 나오고 pending은 없다. isPending 같은 파생 플래그도 그에 맞춰 사라지고, isPlaceholderData도 없다. 그리고 다음 옵션은 여기서 쓸 수 없다 — suspense, throwOnError, enabled, placeholderData.
사용법
옵션은 useInfiniteQuery와 같다. queryKey, queryFn, initialPageParam, getNextPageParam이 핵심이다. 반환에서 fetchNextPage, hasNextPage로 다음 페이지를 이어 받는다.
import { useSuspenseInfiniteQuery } from '@tanstack/react-query'
export function Feed() {
const { data, fetchNextPage, hasNextPage } = useSuspenseInfiniteQuery({
queryKey: ['feed'],
queryFn: ({ pageParam }) => fetchFeed(pageParam),
initialPageParam: 0,
getNextPageParam: (last) => last.nextCursor,
})
// data는 항상 정의돼 있다 — 옵셔널 체이닝이 필요 없다
return (
<>
{data.pages.map((page) =>
page.items.map((item) => <Post key={item.id} {...item} />),
)}
{hasNextPage && <button onClick={() => fetchNextPage()}>더 보기</button>}
</>
)
}
이 컴포넌트는 반드시 위쪽 어딘가의 <Suspense fallback={…}>로 감싸야 한다.
실무 예시
로딩과 에러를 컴포넌트 밖으로 뺀 구성이다. Suspense가 첫 로딩을, ErrorBoundary가 실패를 맡으니 Feed 안에는 성공 경로만 남는다.
import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { Feed } from './Feed'
export function FeedScreen() {
return (
<ErrorBoundary fallback={<p>불러오지 못했다</p>}>
<Suspense fallback={<p>불러오는 중…</p>}>
<Feed />
</Suspense>
</ErrorBoundary>
)
}
왜 중요한가
Suspense를 쓰는 코드베이스에서 무한 목록을 다룰 때 자연스러운 선택이다. 로딩 분기를 컴포넌트마다 쓰지 않아도 되니 본문이 성공 경로만 남아 읽기 쉬워진다. 다만 Suspense 자체를 안 쓰는 프로젝트라면 굳이 옮겨올 이유는 없고, 취소가 필요한 경우도 마찬가지다. 팀이 Suspense를 기본으로 삼았을 때 값이 커지는, 상황을 타는 훅이다.
Reference