useInfiniteQuery 와 IntersectionObserver를 사용한 무한 스크롤 구현

이동욱·2024년 1월 3일
0

Graduation Project

목록 보기
6/11

Intro

졸업 프로젝트를 진행하며 여행 일정 목록을 보여주는 페이지를 개발했습니다.

사용자가 여행 일정과 함께 사진을 업로드하면, 사진 파일이 포함되어 이미지 렌더링 지연이 발생하고 이는 페이지네이션 기반의 구현에서 병목 현상을 일으킬 수 있고, 이는 사용자 경험이 저하되는 원인으로 작용합니다.

이 문제를 해결하기 위해, 무한 스크롤 방식으로 일정 목록 렌더링을 변경하고, 효율적인 서버 데이터 패칭을 위해 React Query를 활용하기로 했습니다.

순서도

useInfiniteQuery

useInfiniteQuery 는 React Query 라이브러리에서 제공하는 Hook 중 하나로, 데이터를 페이지네이션 없이 끊임없이 로드할 수 있게 해주는 무한 스크롤을 구현할 때 유용합니다.

import { useInfiniteQuery } from 'react-query';

 const { data, fetchNextPage, hasNextPage, isFetching, isError } = 
        useInfiniteQuery(
          ['tripList', token],
          ({ pageParam = 0 }) => getEntireTripList(token, pageParam),
          {
              getNextPageParam: (lastPage, allPages) => {
              const nextPageParam = 
                    lastPage.data.content.length === 0 ? false : allPages.length;
              return nextPageParam;
           
              },
          }
       );

useInfiniteQuery 를 사용하여 "tripList" 쿼리 키와 함께 무한 스크롤을 구현하였습니다.

getNextPageParam 을 통해 다음 페이지를 불러올 조건을 정의하며, 이는 사용자가 페이지의 끝에 도달했을 때 더 이상 로드할 데이터가 없으면 더 이상 데이터를 요청하지 않도록 합니다.


IntersectionObserver

IntersectionObserver는 브라우저에서 제공하는 API로 웹 페이지의 어떤 요소가 뷰포트에 들어오거나 나갈 때 콜백 함수를 실행시킵니다.


useEffect(() => {
    const observer = new IntersectionObserver(observerCallback, {
      root: null,
      rootMargin: '0px',
      threshold: 0.1,
    });
    
    if (intersectionTarget) {
      observer.observe(intersectionTarget);
    }
    
    return () => {
      if (intersectionTarget) {
        observer.unobserve(intersectionTarget);
      }
    };
  }, [intersectionTarget, observerCallback]);

사용자가 특정 DOM 요소 (ex : 페이지 하단)에 도달하였는지를 감지합니다.


전체 코드 보기

MainPage에서 useInfiniteQuery와 IntersectionObserver를 사용하였습니다. 먼저, useInfiniteQuery를 사용하여 서버로부터 여행 일정 데이터를 불러오고, IntersectionObserver를 사용하여 스크롤이 페이지 하단에 도달했을 때 추가 데이터를 불러오는 로직을 구현합니다. 이렇게 함으로써 사용자는 끊김 없는 스크롤 경험을 하며 여행 일정을 탐색할 수 있습니다

import { useState, useEffect, useCallback } from 'react';
...
import { useInfiniteQuery } from 'react-query';
...
import { getEntireTripList } from '@/application/api/main/getEntireTripList';
...

function MainPage() {
  ...
  const [intersectionTarget, setIntersectionTarget] = useState(null);
  
  const { data, fetchNextPage, hasNextPage, isFetching, isError } = 
        useInfiniteQuery(
          ['tripList', token],
          ({ pageParam = 0 }) => getEntireTripList(token, pageParam),
          {
              getNextPageParam: (lastPage, allPages) => {
              const nextPageParam = 
                    lastPage.data.content.length === 0 ? false : allPages.length;
              return nextPageParam;
           
              },
          }
       );
  
  const travelList = data?.pages.flatMap((page) => page.data.content) || [];
  
  ...
  
  const observeTarget = useCallback((node) => {
   setIntersectionTarget(node);
  }, []);
  
  const observerCallback = useCallback(
    async ([entry]) => {
      if(entry.isIntersecting && hasNextPage) {
        try {
          await fetchNextPage();
        } catch (error) {
          console.error(error);
        }
      }
    },
    [fetchNextPage, hasNextPage]
  );
  
  useEffect(() => {
    const observer = new IntersectionObserver(observerCallback, {
      root: null,
      rootMargin: '0px',
      threshold: 0.1,
    });
    
    if (intersectionTarget) {
      observer.observe(intersectionTarget);
    }
    
    return () => {
      if (intersectionTarget) {
        observer.unobserve(intersectionTarget);
      }
    };
  }, [intersectionTarget, observerCallback]);
  
  ...
  
  return(
    <div className={styles.mainContainer}>
  	  <Row>
        ...
        <Col>
          {isError && <div>오류가 발생하였습니다.</div>}
         ...
          <List
            dataSource={travelList}
            renderItem={(
               ...
            )}
          />
          {isFetching && <Spin tip='Loading...' size='large' />}
          <div ref={observeTarget} />
        </Col>
      </Row>
    </div>
  );
}

export default MainPage;
profile
개발 과정을 기록합니다.

0개의 댓글