무한스크롤을 만들어보자 (useInfiniteQuery + IntersectionObserver)

Soyeon·2025년 6월 15일
3
post-thumbnail

현재 진행하고 있는 프로젝트에 하객들이 실시간으로 📸사진과 💬축하 메시지를 남길 수 있는 포토톡 기능이 있다.
포토톡 데이터를 보여줄 때 React QueryIntersection Observer API를 사용하여 무한스크롤을 구현했는데, 왜 사용했는지 그리고 어떻게 구현했는지 기록하고자 한다.

페이지네이션 vs 무한스크롤

사용자에게 많은 양의 정보를 보여줄 수 있는 방법에는 크게 2가지가 있다. 각 기법을 어떤 상황에 사용해야 하는지부터 알아보자.

페이지네이션 (Pagination)

특정 기준으로 정렬된 콘텐츠를 여러 페이지로 나눠서 페이지별로 제공하는 UX다.

  • 목적에 맞게 (최신순, 인기순, ...) 정렬된 상태로 접근
  • 콘텐츠의 전체 양을 파악할 수 있어 통제감을 느끼며 탐색
  • 콘텐츠의 정확한 위치 파악
  • When? 사용자가 처음부터 목적을 가지고 콘텐츠를 탐색할 때

무한스크롤 (Infinite Scroll)

사용자가 스크롤을 내릴때 다음 데이터를 자동으로 불러오는 UX다.

  • 페이지 이동 없이 스크롤만으로 정보를 볼 수 있어 몰입감 있는 경험
  • 모바일에서도 빠르고 쉽고 직관적
  • When? 시각 자료 중심의 콘텐츠를 대량으로 탐색할 때

포토톡은 (1)이미지 기반이고, (2)하객들이 부담 없이 스크롤하며 보는 UI가 목적이므로 무한스크롤을 적용하기로 했다.



먼저 무한스크롤이 동작하는 원리를 살펴보고, 직접 구현한 예제로 설명하려고 한다.

무한스크롤의 동작 원리

무한스크롤은 다음과 같이 3가지 과정을 반복한다.

(1) 스크롤 위치 감지 
(2) 조건을 만족하면, 다음 페이지 요청 
(3) 기존 리스트에 추가 렌더링

여기서 조건이란, 한 페이지에 보여주는 콘텐츠 갯수를 말한다.

이를 위해, React에서는 보통 IntersectionObserverReact Query를 함께 사용한다.

(1) Intersection Oberver API (mdn)

브라우저에서 제공하는 API로, 어떤 요소가 뷰포트에 보이는 순간을 감지할 수 있다.

(2) React Query (TanStack Query)

React Application에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트 하는 작업을 도와주는 라이브러리이다.

React Query는 다양한 UI에 유연하게 적용할 수 있도록 useQueries, useInfiniteQuery와 같은 Hook들을 제공하는데, useInfiniteQuery는 무한스크롤을 구현할 때 사용할 수 있다.

useInfiniteQuery 기본 구조 (공식 문서)

const {
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
  isFetchingNextPage,
  isFetchingPreviousPage,
  ...result
} = useInfiniteQuery({
  queryKey,
  queryFn: ({ pageParam = 1 }) => fetchPage(pageParam),
  ...options,
  getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})


직접 구현한 예제로 살펴보자

useInfiniteQuery 커스텀 훅

export const useInfinitePhototalkByQuery = <T, ItemType>({
  queryFn, // 서버에서 데이터를 가져오는 함수
  extractItems, // 가져온 데이터에서 실제 항목만 뽑는 함수
  getHasMore, // 다음 페이지가 있는지 판단하는 함수
  initialPage = 1,
  enabled = true,
}: useInfinitePhototalkByQueryProps<T, ItemType>) => { 
  ...
};
  • 상태 관리
    • page: 지금 몇 번째 페이지인지
    • items: 지금까지 불러온 전체 데이터 목록
    • hasMore: 더 가져올 데이터가 있는지 여부
    • observeRef: 스크롤 감지용 div를 가리키는 변수

데이터 요청 함수들

1. loadMore()

다음 페이지 데이터를 불러오고 기존 리스트에 추가하는 함수다.

  • 스크롤이 끝에 도달했을 때 호출
  • 서버에 현재 페이지 번호를 전달하여 데이터를 요청합
  • 가져온 데이터를 기존 목록 뒤에 추가
  • hasMore 값을 갱신해서 더 불러올 게 있는지 판단
  • 다음 페이지를 위해 page 값을 증가
  • 중복 요청 방지를 위해 isFetching 상태를 사용
const loadMore = async () => {
  if (!hasMore) return;
  const res = await queryFn(page);
  const newItems = extractItems(res);
  setItems([...items, ...newItems]);
  setPage(page + 1);
};

2. refetch()

처음부터 데이터를 다시 불러올 때 사용된다.

  • 페이지가 변경되었거나, 새로고침이 필요할 때 호출
  • 초기 페이지 번호부터 데이터를 요청하고, 상태를 초기화
const refetch = async () => {
  const res = await queryFn(1);
  setItems(extractItems(res));
  setPage(2);
};

3. fetchUnfilFull()

첫 로딩 시 화면이 너무 짧아 빈 공간이 생기는 걸 방지한다.

  • refetch()로 초기화한 후
  • 반복해서 loadMore()를 호출하며 데이터를 더 불러온다
  • 데이터가 더 이상 없거나 추가 데이터가 없으면 중지
const fetchUntilFull = async () => {
  await refetch();
  while (hasMore) {
    const addedLength = await loadMore();
    if (addedLength === 0) break;
  }
};

스크롤 감지하기

useEffect(() => {
  const target = observeRef.current;
  const observer = new IntersectionObserver(([entry]) => {
    if (entry.isIntersecting) loadMore();
  }, { threshold: 0.2 });

  if (target) observer.observe(target);
  return () => observer.unobserve(target);
}, []);




(참고)
카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유
React-Query(tanstack query v5)로 만들어보는 무한스크롤
📲 React-query 사용하는 이유, useInfiniteQuery 로 무한 스크롤 구현하기 (+ react-infinite-scroller)

profile
탄탄한 개발자로 살아남기🗿

0개의 댓글