무엇
infiniteQueryOptions는 무한 쿼리의 옵션을 한 객체로 묶어 돌려주는 헬퍼다. queryOptions의 무한 스크롤 버전으로, useInfiniteQuery와 prefetchInfiniteQuery 등이 같은 정의를 공유하게 한다.
일반 쿼리와 달리 무한 쿼리는 페이지 파라미터를 다루는 필드가 더 필요하다 — initialPageParam(첫 페이지 파라미터)과 getNextPageParam(다음 페이지 파라미터 계산)이다.
import { infiniteQueryOptions } from '@tanstack/react-query'
const todosInfinite = infiniteQueryOptions({
queryKey: ['todos', 'infinite'],
queryFn: ({ pageParam }) => fetchTodos(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
})
무슨 일이 일어나나
동작 원리는 queryOptions와 같다. 런타임에서는 넘긴 객체를 거의 그대로 돌려주고, 값어치는 타입 추론에 있다. 반환된 queryKey에 데이터 타입이 새겨져, 이 정의를 훅과 클라이언트 메서드 어디에 넘겨도 타입이 일관되게 이어진다.
무한 쿼리의 데이터는 단일 값이 아니라 { pages, pageParams } 구조다. pages는 지금까지 가져온 페이지들의 배열, pageParams는 각 페이지를 가져올 때 쓴 파라미터들이다. getNextPageParam이 undefined나 null을 돌려주면 "다음 페이지 없음"으로 판단해 hasNextPage가 false가 된다.
v5에서 initialPageParam은 필수다. 예전에 queryFn의 기본값(pageParam = 0)으로 처리하던 것을 명시적 필드로 바꿨다 — 직렬화 가능성을 보장하기 위해서다.
사용법
한 번 정의해 훅과 프리페치가 함께 쓴다. 네 필드(queryKey, queryFn, initialPageParam, getNextPageParam)를 채워 정의하고, 그 객체를 그대로 넘긴다.
import { infiniteQueryOptions } from '@tanstack/react-query'
export const todosInfinite = infiniteQueryOptions({
queryKey: ['todos', 'infinite'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/todos?cursor=${pageParam}`)
return res.json() as Promise<{ items: Todo[]; nextCursor: number | null }>
},
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
})
// 훅에서
useInfiniteQuery(todosInfinite)
// 프리페치에서
queryClient.prefetchInfiniteQuery(todosInfinite)
실무 예시
커서 기반 무한 스크롤에서 목록을 미리 가져오면서 화면에서도 같은 정의를 쓰는 경우다. data.pages를 펼쳐 렌더하고, fetchNextPage로 다음 페이지를 이어 붙인다.
import { useInfiniteQuery } from '@tanstack/react-query'
import { todosInfinite } from './todosInfinite'
function TodoList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery(todosInfinite)
return (
<>
{data?.pages.flatMap((page) => page.items).map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
<button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage}>
{isFetchingNextPage ? '불러오는 중…' : '더 보기'}
</button>
</>
)
}
왜 중요한가
무한 쿼리는 옵션이 많고 페이지 파라미터 로직이 얽혀서, 훅과 프리페치에 같은 설정을 두 번 적으면 어긋나기 쉽다. infiniteQueryOptions는 그 정의를 한 곳에 모아 타입까지 이어 준다. 무한 스크롤을 여러 곳에서 재사용하거나 라우트 프리페치와 함께 쓴다면 유용하지만, 무한 쿼리 자체가 흔한 요구는 아니라서 일반 queryOptions만큼 자주 손에 잡히지는 않는다.
Reference