TanStack Query useInfiniteQuery로 다음 목록을 이어 불러오려면 무엇을 설정하나요?
기존 목록 아래에 다음 자료를 이어 붙이는 화면에는 useInfiniteQuery를 사용할 수 있습니다. initialPageParam으로 첫 조회 값을 정하고 getNextPageParam으로 다음 조회 위치와 종료 조건을 알려줍니다.
적용 조건·범위: 실행 환경은 React·TypeScript와 @tanstack/react-query 5.104.0입니다. React 환경이 없다면 React Vite 사용법 Vite 8 프로젝트 생성 실행 빌드를, 라이브러리 설치는 TanStack Query Provider와 첫 useQuery 상태 처리 실습 가이드의 설치 항목을 확인합니다. 아래는 별도 Vite 연습 앱 src/App.tsx 전체 내용입니다. src/main.tsx는 유지하며 이전 TaskFlow나 API 서버는 필요하지 않습니다. 요청 함수는 이 파일 안에 정의된 비동기 모형입니다.
본문 근거: tanstack.com/query/latest/docs/framework/react/guides/infinite-queries · tanstack.com/query/latest/docs/framework/react/reference/functions/useInfiniteQuery
이 기술만 따로 연습하기
이 개념 직접 실행
버튼을 조작하고 로딩·결과·캐시의 변화를 확인합니다.
예제 크게 보기 (새 탭)기대 결과: 항목 1 아래에 더 보기로 항목 2·3이 누적됩니다. 추가 요청 중 중복 버튼이 막히며 세 번째 페이지 뒤에는 마지막입니다 상태가 됩니다.
직접 실행: 환경 준비와 전체 파일
실행 환경은 React·TypeScript와 @tanstack/react-query 5.104.0입니다. React 환경이 없다면 React Vite 사용법 Vite 8 프로젝트 생성 실행 빌드를, 라이브러리 설치는 TanStack Query Provider와 첫 useQuery 상태 처리 실습 가이드의 설치 항목을 확인합니다. 아래는 별도 Vite 연습 앱 src/App.tsx 전체 내용입니다. src/main.tsx는 유지하며 이전 TaskFlow나 API 서버는 필요하지 않습니다. 요청 함수는 이 파일 안에 정의된 비동기 모형입니다.
import {
QueryClient,
QueryClientProvider,
useInfiniteQuery
} from '@tanstack/react-query';
const wait = (ms = 400) => new Promise<void>((resolve) => setTimeout(resolve, ms));
const client = new QueryClient({
defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false } },
});
async function readPage(page: number) {
await wait();
return { items: [`항목 ${page}`], next: page < 3 ? page + 1 : undefined };
}
function Demo() {
const query = useInfiniteQuery({
queryKey: ['infinite-rows'],
initialPageParam: 1,
queryFn: ({ pageParam }) => readPage(pageParam),
getNextPageParam: (lastPage) => lastPage.next,
});
return (
<main>
{query.isPending ? <p>처음 불러오기</p> : query.isError ? <p>{query.error.message}</p>
: <ul>{query.data.pages.flatMap((page) => page.items).map((item) => <li key={item}>{item}</li>)}</ul>}
<button disabled={!query.hasNextPage || query.isFetching}
onClick={() => { if (!query.isFetching) void query.fetchNextPage(); }}>
{query.isFetchingNextPage ? '추가 요청 중' : query.hasNextPage ? '더 보기' : '마지막입니다'}
</button>
</main>
);
}
export default function App() {
return (
<QueryClientProvider client={client}>
<Demo />
</QueryClientProvider>
);
}
npm run dev
npm run build
자가 확인·변형: queryKey에 pageParam을 넣지 않아도 pages 배열이 늘어납니다. 필터를 새로 도입한다면 필터 값은 왜 queryKey에 들어가야 할까요?
검증 기록: 위 전체 코드는 타입검사·빌드와 jsdom 동작 검증을 통과했습니다. 실제 브라우저 화면과 누적 Next.js 프로젝트 통합 실행은 별도 확인 대상입니다.
기본 useInfiniteQuery
const query = useInfiniteQuery({
queryKey: [
'tasks',
'infinite',
],
initialPageParam: 1,
queryFn: ({ pageParam }) =>
getTaskPage(pageParam),
getNextPageParam: (
lastPage,
) => lastPage.nextPage,
});
initialPageParam
처음 호출할 pageParam을 지정합니다.
initialPageParam: 1
queryFn은 pageParam을 받아 해당 페이지를 요청합니다.
queryFn: ({ pageParam }) =>
getTaskPage(pageParam)
getNextPageParam
마지막으로 받은 page에서 다음 요청에 사용할 pageParam을 반환합니다.
getNextPageParam: (
lastPage,
) => lastPage.nextPage
다음 페이지가 없으면 null 또는 undefined를 반환하도록 API shape와 맞춥니다.
data.pages
infinite query 결과는 여러 page를 보관합니다.
data.pages[0]
data.pages[1]
data.pages[2]
...
화면에서는 하나의 배열로 펼칠 수 있습니다.
const tasks =
query.data?.pages.flatMap(
(page) => page.items,
) ?? [];
fetchNextPage
<button
type="button"
disabled={
!query.hasNextPage ||
query.isFetchingNextPage
}
onClick={() =>
query.fetchNextPage()
}
>
{query.isFetchingNextPage
? '불러오는 중...'
: '더 불러오기'}
</button>
isPending과 isFetchingNextPage
isPending
→ 첫 데이터가 아직 없음
isFetchingNextPage
→ 기존 목록은 있고 다음 page를 요청 중
두 상태를 구분하면 기존 목록을 유지한 채 하단에 로딩 UI를 보여줄 수 있습니다.
queryKey에 pageParam을 직접 넣지 않는 이유
useInfiniteQuery는 하나의 infinite query 안에서 pageParams와 pages를 관리합니다.
['tasks', 'infinite']
→ pages + pageParams
필터가 바뀐다면 필터는 queryKey에 포함합니다.
[
'tasks',
'infinite',
{ status },
]
실제 무한 스크롤
“더 불러오기” 버튼 대신 IntersectionObserver로 하단 sentinel이 보일 때 fetchNextPage를 호출할 수 있습니다.
핵심 데이터 구조는 동일합니다.
IntersectionObserver
→ fetchNextPage()
TanStack Query
→ pages 관리
처음 학습할 때는 버튼으로 동작을 확인한 뒤 observer를 붙이는 편이 디버깅하기 쉽습니다.
pagination과 infinite query 선택
페이지 번호 공유·검색 결과 탐색
→ useQuery + page queryKey
피드·더보기·연속 목록
→ useInfiniteQuery
UI 요구사항에 따라 선택합니다.
학습 완료 체크리스트
- useInfiniteQuery의 pageParam 흐름을 설명할 수 있습니다.
- initialPageParam을 지정할 수 있습니다.
- getNextPageParam으로 다음 page를 결정할 수 있습니다.
- data.pages를 하나의 목록으로 펼칠 수 있습니다.
- hasNextPage와 isFetchingNextPage를 사용할 수 있습니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 서버 데이터의 조회·변경과 캐시를 관리합니다.
TaskFlow 작업 피드를 useInfiniteQuery로 바꿔 기존 목록 아래에 다음 데이터를 이어 붙입니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 프로젝트 적용
중간부터 시작하는 전체 실습 파일
기존 Next.js Route Handler TaskFlow의 /tasks 화면과 /api/tasks를 이어서 확장합니다. 인증 없는 demo 메모리 저장소를 사용하는 로컬 실습이며 계정·외부 DB·API 키가 필요 없습니다. MSW를 활성화하지 않습니다. 서버를 다시 시작하면 데이터가 초기화됩니다.
이 글의 시작본은 직전 프로젝트 단계 완료본 전체입니다. 이미 직전 단계를 마쳤다면 그 폴더를 이어 쓰셔도 됩니다. Node.js 24.x와 npm을 사용하며 패키지 버전은 package-lock.json에 고정했습니다.
시작본에서 직접 바꾸기
시작본을 실행한 뒤 아래 변경 파일을 순서대로 수정합니다. 완료본의 같은 경로 파일에는 import와 입력 타입을 포함한 전체 코드가 있습니다.
app/tasks/InfiniteTaskList.tsx— 이전 단계에 없던 파일을 만듭니다.app/tasks/TaskDashboard.tsx— 기존 파일의 같은 위치를 갱신합니다.
변경 이유와 결과: cursor 계약과 InfiniteTaskList를 추가합니다. getNextPageParam이 null에서 끝나고 페이지형·무한형 캐시를 별도 키로 유지합니다.
완료본의 docs/changes.patch에서 시작·완료 차이를, docs/current-step.md에서 파일별 적용 위치를 확인할 수 있습니다. 아래 전체 파일을 같은 경로에 반영한 다음 실행하세요.
app/tasks/InfiniteTaskList.tsx 전체 파일
'use client';
import { useInfiniteQuery } from '@tanstack/react-query';
import { getTaskCursorPage } from '../lib/tasks-api';
import { taskKeys } from '../lib/task-keys';
export default function InfiniteTaskList() {
const query = useInfiniteQuery({ queryKey: taskKeys.infinite(), initialPageParam: null as string | null,
queryFn: ({ pageParam, signal }) => getTaskCursorPage(pageParam, signal), getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined });
return (
<section aria-label="무한 목록">
<h2>이어 불러오기</h2>
{query.isPending ? <p>피드 로딩 중</p> : query.isError ? <p role="alert">{query.error.message}</p> : <ul>{query.data.pages.flatMap((page) => page.items).map((task) => <li key={task.id}>{task.title}</li>)}</ul>}
<button disabled={!query.hasNextPage || query.isFetchingNextPage} onClick={() => void query.fetchNextPage()}>{query.hasNextPage ? '더 보기' : '마지막 항목'}</button>
</section>
);
}
app/tasks/TaskDashboard.tsx 전체 파일
'use client';
import { useState } from 'react';
import TaskListClient from './TaskListClient';
import type { TaskStatus } from '../lib/tasks-api';
import TaskDetailClient from './TaskDetailClient';
import TaskForm from './TaskForm';
import QueryStatePanel from './QueryStatePanel';
import PaginatedTaskList from './PaginatedTaskList';
import InfiniteTaskList from './InfiniteTaskList';
export default function TaskDashboard() {
const [status, setStatus] = useState<TaskStatus>('all');
const [selected, setSelected] = useState<string | null>(null);
return (
<main>
<h1>TaskFlow Query</h1>
<label>필터 <select value={status} onChange={(event) => setStatus(event.target.value as TaskStatus)}>
<option value="all">전체</option><option value="active">진행 중</option><option value="completed">완료</option>
</select></label>
<TaskForm />
<TaskListClient status={status} onSelect={setSelected} />
{selected && <TaskDetailClient taskId={selected} />}
<QueryStatePanel />
<PaginatedTaskList />
<InfiniteTaskList />
</main>
);
}
완료 확인
- 시작본은 직전 완료본과 동일한 ZIP입니다. 압축을 푼 뒤
npm ci로 잠금 파일의 의존성을 설치합니다. npm run dev실행 후http://localhost:3000/tasks를 엽니다.- cursor 계약과 InfiniteTaskList를 추가합니다. getNextPageParam이 null에서 끝나고 페이지형·무한형 캐시를 별도 키로 유지합니다.
npm run typecheck와npm run build가 성공하면 다음 프로젝트 단계로 갑니다.
이 체크포인트는 TypeScript 검사·프로덕션 빌드와 실제 소스를 사용한 JSDOM/상태 시나리오 검증을 통과했습니다. 별도 실제 브라우저 화면 검수는 이 파일 검증 범위에 포함하지 않았습니다.
아래 부분 예제는 핵심 변경을 설명합니다. 실행에 필요한 전체 파일과 보조 타입·API는 위 완료본에서 확인하세요.
누적 실습은 이 절의 프로젝트 단계 링크를 따라 진행합니다. 페이지 끝 학습 순서는 기술 주제 읽기 순서입니다.
이전 프로젝트 단계: TanStack Query 페이지네이션 실습: placeholderData로 이전 목록 유지하기. 위 시작본 ZIP 전체를 내려받으면 이전 글을 다시 조립하지 않고 이 단계부터 실행할 수 있습니다.
InfiniteTaskList에서 initialPageParam과 next cursor를 연결하고 기존 페이지를 pages에 누적합니다. 페이지형 캐시와 infinite 캐시 키는 구분합니다.
이전 체크포인트
query-step-08-pagination
이번 목표
TaskFlow 작업 피드를 useInfiniteQuery로 바꿔 기존 목록 아래에 다음 데이터를 이어 붙입니다.
전체 파일·코드 보기
app/tasks/InfiniteTaskList.tsx
'use client';
import {
useInfiniteQuery,
} from '@tanstack/react-query';
import {
getTaskPage,
} from '../lib/tasks-api';
export default function InfiniteTaskList() {
const query =
useInfiniteQuery({
queryKey: [
'tasks',
'infinite',
],
initialPageParam: 1,
queryFn: ({
pageParam,
}) =>
getTaskPage(pageParam),
getNextPageParam: (
lastPage,
) =>
lastPage.nextPage ??
undefined,
});
if (query.isPending) {
return <p>불러오는 중입니다.</p>;
}
if (query.isError) {
return <p>{query.error.message}</p>;
}
const tasks =
query.data.pages.flatMap(
(page) => page.items,
);
return (
<section>
<ul>
{tasks.map((task) => (
<li key={task.id}>
{task.title}
</li>
))}
</ul>
<button
type="button"
disabled={
!query.hasNextPage ||
query.isFetchingNextPage
}
onClick={() =>
query.fetchNextPage()
}
>
{query.isFetchingNextPage
? '불러오는 중...'
: query.hasNextPage
? '더 불러오기'
: '마지막 목록'}
</button>
</section>
);
}
Network 확인
첫 요청
/api/tasks?page=1&pageSize=5
다음 요청
/api/tasks?page=2&pageSize=5
다음 요청
/api/tasks?page=3&pageSize=5
기존 DOM 목록이 유지된 상태로 항목이 아래에 추가되는지 확인합니다.
npm run build
현재 확장 단계
✓ 1~8. Query·mutation·pagination
→ 9. useInfiniteQuery
○ 10. optimistic update / rollback
○ 11. 목록·상세 cache 동기화
○ 12~15. SSR·Hydration
완료 체크포인트
query-step-09-infinite
다음 학습
다음 글에서는 서버 응답을 기다리기 전에 작업 상태를 먼저 바꾸고, 실패하면 이전 cache로 되돌리는 optimistic update를 구현합니다.
다음 프로젝트 단계: TanStack Query Todo 실습: 서버 연결·낙관적 업데이트·실패 롤백
공식 문서
- Infinite Queries: https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries
- useInfiniteQuery: https://tanstack.com/query/latest/docs/framework/react/reference/functions/useInfiniteQuery
프로젝트 적용 후 확인: 더 보기 요청이 중복되지 않게 하려면?
관찰 절차 /tasks의 더 보기 버튼을 연속 두 번 눌러 Network의 cursor/pageParam과 Devtools의 pages, pageParams를 확인합니다. 마지막 페이지에서 버튼 상태도 확인합니다.
예상 결과 다음 페이지 데이터가 기존 목록 뒤에 붙고, 마지막 페이지에서 hasNextPage가 false가 되어 추가 요청을 막습니다.
오류 점검 같은 항목이 반복되면 getNextPageParam 반환값과 서버 cursor를 확인합니다. 동시 호출은 isFetchingNextPage 상태로 막습니다.
다음 학습 10회차에서 변경 요청 중 낙관적 화면을 다룹니다.
검증 범위 전체 체크포인트의 빌드·상태 동작은 로컬 검증을 통과했습니다. 실제 브라우저 시각 확인은 별도로 진행해야 합니다.
글 소개·학습 준비·전체 목차

TanStack Query · 9 / 15 · 중급 학습 · useInfiniteQuery
페이지네이션은 사용자가 페이지를 명시적으로 이동하는 UI에 적합합니다.
피드나 긴 작업 목록처럼 기존 항목 아래에 다음 데이터를 계속 붙여야 한다면 useInfiniteQuery를 사용할 수 있습니다.
TanStack Query v5의 infinite query에서는 initialPageParam과 getNextPageParam을 명확하게 설정합니다.
이번 글에서 배우는 내용
- useInfiniteQuery
- initialPageParam
- queryFn의 pageParam
- getNextPageParam
- hasNextPage
- fetchNextPage
- pages를 하나의 목록으로 펼치는 방법
- 무한 스크롤과 pagination 선택 기준
목차
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 15개 · 필수 14개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.