최근 tanStack Query를 사용하며, 기존의 useQuery와 infinite 관련 메소드 간의 데이터 유형 차이로 인한 문제를 경험했습니다. 특히, 무한 스크롤 기능을 구현하는 과정에서 이러한 차이가 중요한 트러블슈팅 포인트가 되었습니다. 이 경험을 바탕으로, infinite 관련 메소드들의 특징과 사용법을 집중적으로 정리해보고자 합니다. 본 글에서는 기본적인 useQuery 사용법보다는, 무한 스크롤 구현에 특화된 메소드들에 초점을 맞출 예정입니다.
useInfiniteQuery
는 무한 스크롤과 데이터 캐싱 기능을 지원하기 위해 설계된 메소드입니다. 시작하기 전에 기본적인 사용법을 알아봅시다.
const {
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
...result
} = useInfiniteQuery({
queryKey,
queryFn: ({ pageParam }) => fetchPage(pageParam),
initialPageParam: 1,
...options,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) =>
firstPage.prevCursor,
})
fetchNextPage()
: 스크롤 다운 시 추가 데이터를 로드합니다. getNextPageParam
에서 정의된 로직에 따라 다음 페이지의 데이터를 자동으로 요청합니다.
fetchPreviousPage()
: 스크롤 업 시 이전 데이터를 로드합니다. getPreviousPageParam
에서 설정된 로직을 통해 이전 페이지의 데이터를 요청합니다.
hasNextPage
: true
면 더 로드할 다음 페이지가 존재합니다.
hasPreviousPage
: true
면 더 로드할 이전 페이지가 존재합니다.
isFetchingNextPage
: 다음 페이지 로딩 중인지 여부. 로딩 중이면 true
입니다.
isFetchingPreviousPage
: 이전 페이지 로딩 중인지 여부. 로딩 중이면 true
입니다.
...result
: useInfiniteQuery
로부터 반환되는 추가 정보들로, data
, error
, status
, isFetching
, isError
, isLoading
, refetch
등의 useQuery
속성 및 메소드가 포함됩니다. 이를 통해 쿼리 상태를 상세하게 관리할 수 있습니다.useInfiniteQuery
옵션queryFn
: 데이터를 가져오는 함수. pageParam
을 인자로 받아 API 호출을 구성합니다.
// 페이지별 데이터 패칭 함수
queryFn: ({ pageParam }) => fetchPage(pageParam)
initialPageParam
: 초기 페이지 매개변수. 첫 페이지 데이터 패칭 시 사용됩니다.
// 첫 페이지 데이터를 위한 초기 파라미터
initialPageParam: 1
getNextPageParam
: 다음 페이지 데이터를 어떻게 요청할지 정의. 마지막 페이지 데이터, 모든 페이지 데이터, 마지막 pageParam
을 인자로 받아 다음 페이지 요청을 위한 파라미터를 결정합니다.
// 다음 페이지 요청을 위한 파라미터 결정 로직
getNextPageParam: (lastPage) => lastPage.nextCursor
getPreviousPageParam
: 이전 페이지 데이터를 어떻게 요청할지 정의. 첫 페이지 데이터, 모든 페이지 데이터, 첫 pageParam
을 인자로 받아 이전 페이지 요청을 위한 파라미터를 결정합니다.
// 이전 페이지 요청을 위한 파라미터 결정 로직
getPreviousPageParam: (firstPage) => firstPage.prevCursor
maxPages
: 캐시에 저장할 최대 페이지 수. 이 한계를 초과하면 새 페이지가 추가될 때 가장 오래된 페이지가 제거됩니다.
// 캐시에 저장할 최대 페이지 수
maxPages: 5
위 옵션 중 핵심 옵션인 getNextPageParam에 대해 짚고갈 필요가 있습니다.
getNextPageParam
활용 예시와 전략 🚀getNextPageParam
은 useInfiniteQuery
에서 다음 페이지의 데이터를 어떻게 요청할지 결정하는 핵심적인 함수입니다. 여기에는 몇 가지 전략이 있으며, 선택한 전략은 API 설계와 데이터 구조에 따라 달라집니다.
파라미터 구성 📌
- 첫 번째 인자: 마지막 요청에서 받은 페이지 데이터입니다. 이를 통해 다음 페이지 요청 시 필요한 정보를 추출할 수 있습니다.
- 두 번째 인자: 지금까지 요청된 모든 페이지의 데이터입니다. 이 데이터를 기반으로 복잡한 로직을 수립할 수 있습니다.
- 세 번째 인자: 이전 페이지 요청에서 사용된 마지막
pageParam
입니다.
ID 기반 전략
getNextPageParam: (lastPage) => lastPage[lastPage.length - 1].id + 1
Offset 기반 전략
getNextPageParam: (_, allPages) => allPages.flat().length
Cursor 기반 전략
getNextPageParam: (lastPage) => lastPage.nextCursor
// Cursor 기반 페이징 예시
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = '' }) => fetchPosts(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
// 스크롤 이벤트 핸들러
window.addEventListener('scroll', () => {
if (window.scrollY + window.innerHeight >= document.body.scrollHeight && hasNextPage) {
fetchNextPage();
}
});
애플리케이션의 요구 사항에 맞게 최적화된 전략을 선택하는 것이 중요합니다.
useSuspenseInfiniteQuery
를 고려해 볼 수 있습니다. 이를 통해 데이터 로딩 상태를 Suspense 컴포넌트로 쉽게 관리할 수 있습니다.fetchInfiniteQuery
vs prefetchInfiniteQuery
React Query의 QueryClient
는 데이터 캐싱 및 쿼리 관리에 핵심적인 역할을 합니다. 특히, fetchInfiniteQuery
와 prefetchInfiniteQuery
메소드는 무한 스크롤 데이터를 효율적으로 관리하고, 사용자 경험을 향상시키는 데 유용합니다.
queryClient.fetchInfiniteQuery
Promise<InfiniteData<TData, TPageParam>>
을 반환하며, 쿼리 실행 성공 시 무한 쿼리 데이터를 포함하는 프라미스 객체입니다. 실패 시 오류를 throw 합니다.queryClient.prefetchInfiniteQuery
Promise<void>
을 반환합니다. 쿼리 실행 후 데이터가 캐시에 저장됨을 의미하며, 직접적인 데이터 반환 값은 없습니다.사용 목적: fetchInfiniteQuery
는 데이터를 즉시 요청하고 활용할 때 사용되며, prefetchInfiniteQuery
는 사용자 경험을 개선하기 위해 데이터를 미리 로드할 때 사용됩니다.
오류 처리: fetchInfiniteQuery
는 실행 중 오류가 발생하면 오류를 throw하여 Error Boundary
등을 통해 처리할 수 있습니다. 반면, prefetchInfiniteQuery
는 오류가 발생해도 프라미스가 reject되지 않으므로, 오류 처리가 다르게 이루어집니다.
fetchInfiniteQuery
예시: 사용자가 특정 섹션에 도달했을 때 추가 데이터를 즉시 로드하고, 해당 데이터를 기반으로 UI를 업데이트하려는 경우 사용됩니다. 오류 발생 시 Error Boundary
를 활용하여 사용자에게 적절한 피드백을 제공할 수 있습니다.
prefetchInfiniteQuery
예시: 사용자가 특정 페이지나 섹션에 접근하기 전에 데이터를 미리 로드하여, 스크롤링이나 페이지 전환 시 빠른 데이터 표시를 원할 때 사용됩니다. 오류 처리는 직접적으로 이루어지지 않으므로, 데이터 로드 실패 시 대체 UI 등을 별도로 고려해야 합니다.
두 메소드는 각기 다른 시나리오에서 유용하게 활용될 수 있으며, 무한 스크롤 데이터 관리를 위한 효과적인 도구입니다. 사용 목적에 맞게 적절한 메소드를 선택하여 사용하는 것이 중요합니다.
InfiniteQueryObserver
를 활용한 무한 스크롤 구현 🔄InfiniteQueryObserver
는 React Query의 강력한 기능 중 하나로, useInfiniteScroll
훅을 사용하는 것보다 더 세밀한 제어가 가능하게 합니다. 이를 통해 무한 스크롤 기능을 보다 유연하고 효율적으로 구현할 수 있습니다.
import { useEffect } from "react";
import { QueryClient, InfiniteQueryObserver } from "@tanstack/react-query";
import { getVideoList } from "@/api/videoApi";
import type { Video } from "@/types/videoType";
// QueryClient 인스턴스 생성
const queryClient = new QueryClient();
const useInfiniteScroll = () => {
useEffect(() => {
// InfiniteQueryObserver 인스턴스 생성
const observer = new InfiniteQueryObserver(queryClient, {
queryKey: ['videos'],
queryFn: getVideoList, // 데이터 패칭 함수
getNextPageParam: (lastPage) => lastPage.nextCursor, // 다음 페이지 파라미터 결정 로직
});
// 스크롤 이벤트 핸들러
const handleScroll = () => {
const isBottom = window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight;
if (isBottom) {
const state = observer.getCurrentResult();
if (state.hasNextPage) {
observer.fetchNextPage(); // 다음 페이지 데이터 패칭
}
}
};
window.addEventListener("scroll", handleScroll); // 이벤트 리스너 등록
// 클린업 함수에서 리소스 정리
return () => {
window.removeEventListener("scroll", handleScroll); // 이벤트 리스너 제거
observer.destroy(); // Observer 인스턴스 정리
};
}, []);
};
이 코드는 컴포넌트가 마운트될 때 InfiniteQueryObserver
인스턴스를 생성하고, 사용자가 페이지 하단에 도달할 때마다 handleScroll
함수를 통해 추가 데이터를 패칭합니다. 컴포넌트가 언마운트될 때는 이벤트 리스너를 제거하고 InfiniteQueryObserver
인스턴스를 정리하여 메모리 누수를 방지합니다.
제 개인적인 생각으로는 선언형으로 지연이나 에러에 대처가 힘들어보이지만, 간편하게 사용할 수 있는 점이 좋아보입니다.
InfiniteQuery를 살펴보면서 무한 스크롤과 캐싱을 더 효율적으로 구현하는 방법을 알아보았습니다. 이를 통해 사용하는 메소드의 본질과 의미를 다시 한 번 생각해볼 수 있었고, 우연히 발견한 InfiniteQueryObserver에 대한 흥미로운 발견을 할 수 있었습니다.
다음 프로젝트에는 InfiniteQueryObserver를 직접 활용하여 Developer Experience가 어떻게 변화하는지 확인하고, 관련 내용을 블로그에 정리해보려 합니다.
출처 :
https://velog.io/@baby_dev/React-Query-fetchQuery-vs-prefetchQuery
https://tanstack.com/query/latest/docs/framework/react/reference/useSuspenseInfiniteQuery
https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientfetchinfinitequery
https://tanstack.com/query/latest/docs/reference/InfiniteQueryObserver#infinitequeryobserver
https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery