카테고리 없음

Tanstack Query(React-Query)로 무한 스크롤 구현해보기

Dev.JH 2025. 1. 2. 20:33

Tanstack Query 로고

🔑 무한스크롤 도입 계기

요즘 조금씩 시간을 내서 사이드프로젝트를 진행 중이다! 디자이너분이 제공해주신 목업을 보았을 때, 무한스크롤로 구현하면 좋을 것 같다는 생각이 들었다.

홈 화면에서는 전체 To Do를 보여줘야 하는 요구사항이 존재하였는데, 만약 To Do가 많아지면 분명 성능적인 이슈가 발생할 것이 분명했다. 그렇기에 기획자 분과 UX적으로 어떤 방법이 좋을 지 함께 고민하고 상의한 끝에, 무한 스크롤을 통해 전체 To do를 보여주는 방식을 채택하기로 하였다.

사이드프로젝트 홈 UI

 

프론트엔드 분들과 상태관리 라이브러리를 선택할 때, Jotai와 Tanstack Query를 사용하기로 결정하였기에 Tanstack Query에서 제공하는 useInfiniteQuery 를 통해 무한 스크롤을 구현해야겠다는 생각이 들었다.


🚀 useInfiniteQuery

기존의 데이터에 더 많은 데이터를 추가적으로 로드 하거나 무한 스크롤 할 수 있는 UI Pattern은 매우 흔하다. 이러한 UI Pattern을 좀 더 쉽게 구성할 수 있도록 Tanstack Query는 useQuery의 유용한 버전인 useInfiniteQuery를 지원한다.

다만, useInfiniteQueryuseQuery와 다르게 다양한 옵션이 존재한다.

 

1. 무한 쿼리 데이터를 포함하는 객체, data.

  • data.pages: 가져온 페이지들을 포함하는 배열
  • data.pageParams: 페이지를 가져오는 데 사용된 페이지 파라미터들을 포함하는 배열

2. fetchNextPage 함수

  • fetchNextPage는 필수로 사용되는 옵션

3. initialPageParam 옵션

  • 초기 페이지 파라미터를 지정하기 위해 필수적으로 설정되어야 한다.

4. getNextPageParam 옵션

  • 더 로드할 데이터가 있는지 여부를 확인하고 데이터를 가져오기 위한 정보를 제공하는 데 사용된다.
  • 흔히 마지막 페이지일 경우에는 undefined를 반환하고 hasNextPage의 값을 false로 설정한다.

5. hasNextPage 옵션

  • hasNextPage: getNextPageParam이 null 또는 undefined가 아닌 값을 반환하면 true로 설정된다.

6. isFetchingNextPage

  • isFetchingNextPage: 다음 페이지를 로드하는 상태인지 확인하는 boolean 값

🤔 사용 예제

아래의 예제 코드와 같이 cursor의 index 값을 기반으로 3개의 페이지를 반환하는 API 가 있다고 가정해보겠다.

fetch('/api/projects?cursor=0')
// { data: [...], nextCursor: 3}
fetch('/api/projects?cursor=3')
// { data: [...], nextCursor: 6}
fetch('/api/projects?cursor=6')
// { data: [...], nextCursor: 9}
fetch('/api/projects?cursor=9')
// { data: [...] }

 

그럼 아래와 같이 pagination을 구현할 수도, '더보기' 버튼을 구현하여 다음 페이지를 불러올 수 있도록 만들 수도 있을 것이다.

우리는 아래의 여러 UI 중, 더보기를 구현해보도록 하자!

 

 

import { useInfiniteQuery } from '@tanstack/react-query'

function Projects() {
  const fetchProjects = async ({ pageParam }) => {
    const res = await fetch('/api/projects?cursor=' + pageParam)
    return res.json()
  }

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    initialPageParam: 0,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  })

  return status === 'pending' ? (
    <p>로딩중...</p>
  ) : status === 'error' ? (
    <p>Error: {error.message}</p>
  ) : (
    <>
      {data.pages.map((group, i) => (
        <React.Fragment key={i}>
          {group.data.map((project) => (
            <p key={project.id}>{project.name}</p>
          ))}
        </React.Fragment>
      ))}
      <div>
        <button
          onClick={() => fetchNextPage()}
          disabled={!hasNextPage || isFetchingNextPage}
        >
          {isFetchingNextPage
            ? '불러오는 중...'
            : hasNextPage
              ? '더 보기'
              : '마지막 페이지입니다.'}
        </button>
      </div>
      <div>{isFetching && !isFetchingNextPage ? '불러오는 중...' : null}</div>
    </>
  )
}

위와 같이 useInfiniteQuery를 사용함으로써 해당 UI를 만들 수 있다.


👍 useInfiniteQuery를 이용하여 무한스크롤 구현하기

그러나 우리가 구현하고 싶은 것은 더보기 버튼을 눌러서 다음 페이지를 로드하는 UI가 아니라, 스크롤을 통해 다음 페이지를 로드하는 무한스크롤이다.

우리는 스크롤을 내려서 viewPort에 마지막 요소가 보여지는지를 체크를 해야한다. 우리는 이것을 위해 react-intersection-observeruseInview 훅이 필요하다. 아래와 같이 자신의 패키지매니저에 따라 설치를 하도록 하자. 

npm install react-intersection-observer
// 또는
yarn add react-intersection-observer

 

useInview 훅은 리액트 컴포넌트의 "InView" 상태를 모니터링 해준다. 훅의 이름대로, element가 viewPort에 진입되고, 제외되는 시점을 파악하는데 도움을 주는 훅이라고 생각하면 된다.

viewPort에 보일 때를 체크할 element에 ref 속성을 걸어주고(<div> ref={ref} />), 이 요소가 뷰포트 안에 보였을 때 (inView === true) fetchNextPage를 실행하여 다음 페이지를 가져오면 끝이다.

아래는 무한스크롤을 구현한 예제 코드이다.

 

import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';
import React from 'react';

function Projects() {
  const fetchProjects = async ({ pageParam }) => {
    const res = await fetch('/api/projects?cursor=' + pageParam);
    return res.json();
  };

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });

  const { ref, inView } = useInView({
    threshold: 0.5, // 50% 보일 때 트리거
  });

  // Fetch next page when the observer comes into view
  React.useEffect(() => {
    if (inView && hasNextPage) {
      fetchNextPage();
    }
  }, [inView, hasNextPage, fetchNextPage]);

  return status === 'loading' ? (
    <p>로딩중...</p>
  ) : status === 'error' ? (
    <p>Error: {error.message}</p>
  ) : (
    <>
      {data.pages.map((group, i) => (
        <React.Fragment key={i}>
          {group.data.map((project) => (
            <p key={project.id}>{project.name}</p>
          ))}
        </React.Fragment>
      ))}
      <div ref={ref} style={{ height: '1px' }} />
      {isFetchingNextPage && <p>불러오는 중...</p>}
      {!hasNextPage && <p>마지막 페이지입니다.</p>}
    </>
  );
}

export default Projects;

 

위 코드에서 useInfiniteQuery를 따로 훅으로 만들어서 코드를 작성하는 것이 코드의 가독성을 높일 수 있는 방법이라고 생각한다. 위 예제코드를 활용하여서 여러분들의 입맛대로 커스텀하면 좋을 것 같다.


마무리하며...

시간이 부족하여 아직 무한 스크롤을 직접 사이드프로젝트에 적용하지는 못하였지만... 추후 무한 스크롤을 적용한 UI와 함께 글을 더 보완할 예정이다!!


출처 & 참고 자료

Infinite Queries | TanStack Query React Docs

 

Infinite Queries | TanStack Query React Docs

Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. TanStack Query supports a useful version of useQuery called use...

tanstack.com

useInfiniteQuery로 무한스크롤 구현하기 | 올리브영 테크블로그

 

useInfiniteQuery로 무한스크롤 구현하기 | 올리브영 테크블로그

무한스크롤 구현 방법과 뒤로가기 시 스크롤 유지하는 방법을 소개합니다.

oliveyoung.tech