무한 스크롤과 데이터 캐싱: TanStack Query의 Infinite 메소드 정리

hodu·2024년 2월 28일
5

✅ 개요

최근 tanStack Query를 사용하며, 기존의 useQuery와 infinite 관련 메소드 간의 데이터 유형 차이로 인한 문제를 경험했습니다. 특히, 무한 스크롤 기능을 구현하는 과정에서 이러한 차이가 중요한 트러블슈팅 포인트가 되었습니다. 이 경험을 바탕으로, infinite 관련 메소드들의 특징과 사용법을 집중적으로 정리해보고자 합니다. 본 글에서는 기본적인 useQuery 사용법보다는, 무한 스크롤 구현에 특화된 메소드들에 초점을 맞출 예정입니다.


😎 useInfiniteQuery

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 활용 예시와 전략 🚀

getNextPageParamuseInfiniteQuery에서 다음 페이지의 데이터를 어떻게 요청할지 결정하는 핵심적인 함수입니다. 여기에는 몇 가지 전략이 있으며, 선택한 전략은 API 설계와 데이터 구조에 따라 달라집니다.

파라미터 구성 📌
  • 첫 번째 인자: 마지막 요청에서 받은 페이지 데이터입니다. 이를 통해 다음 페이지 요청 시 필요한 정보를 추출할 수 있습니다.
  • 두 번째 인자: 지금까지 요청된 모든 페이지의 데이터입니다. 이 데이터를 기반으로 복잡한 로직을 수립할 수 있습니다.
  • 세 번째 인자: 이전 페이지 요청에서 사용된 마지막 pageParam입니다.

설계 전략 🛠️

  • ID 기반 전략

    • 마지막 데이터 항목의 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();
  }
});

애플리케이션의 요구 사항에 맞게 최적화된 전략을 선택하는 것이 중요합니다.

🔄 Suspense 연동

  • 무한 스크롤을 Suspense와 함께 사용하고 싶다면, useSuspenseInfiniteQuery를 고려해 볼 수 있습니다. 이를 통해 데이터 로딩 상태를 Suspense 컴포넌트로 쉽게 관리할 수 있습니다.

🔍 데이터 미리 받아오기: fetchInfiniteQuery vs prefetchInfiniteQuery

React Query의 QueryClient는 데이터 캐싱 및 쿼리 관리에 핵심적인 역할을 합니다. 특히, fetchInfiniteQueryprefetchInfiniteQuery 메소드는 무한 스크롤 데이터를 효율적으로 관리하고, 사용자 경험을 향상시키는 데 유용합니다.

📥 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

profile
잘부탁드립니다.

0개의 댓글