라이브러리 없이 무한스크롤 구현하기(+디바운싱)

비얌·2024년 4월 12일
0
post-thumbnail

✨ 개요

저번에 '너는, 나는' 프로젝트를 하며 React Query의 useInfiniteQuery를 사용해서 무한스크롤을 구현하고 포스팅했었다.

그리고 최근 한 프로젝트 '트위터 명함'에서는 다른 팀원께서 동일한 라이브러리로 무한스크롤을 구현해주셨다.

이렇게 라이브러리를 사용해서 구현한 무한스크롤을 접하다가 문득, 라이브러리 없이 무한스크롤을 구현한다면 어떻게 구현할 수 있을지 궁금해졌다.

그래서 이번 포스팅에서 라이브러리 없이 무한스크롤 구현하기를 해보기로 했다.

그리고 React Query의 useInfiniteQuery를 쓸 때와 무엇이 다른지도 생각해보기로 했다.



👏 결과 미리보기

디바운싱까지 적용했을 때, 결과는 아래와 같다. 스크롤이 움직일 때마다 스크롤 이벤트를 감지하지 않게 했다. 그리고 스크롤이 바닥에 닿으면 다음 페이지를 불러오도록 했다.



🛫 무한스크롤 구현하기

1. 데이터를 가져오는 함수 만들기

먼저 데이터를 가져오는 함수를 만든다. getCards 함수는 pageNum에 해당하는 페이지의 cards 데이터를 리턴한다.

const [cards, setCards] = useState<CardType[]>([]);

const pageNum = useRef(0);

const getCards = async (pageNum: number) => {
  const res = await axios.get(`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/cards`, {
    params: { page: pageNum },
  });
  setCards((prev) => [...prev, ...res.data.cards]);
};

2. 스크롤이 바닥에 닿았을 때를 감지하여 다음 페이지 데이터 부르기

스크롤이 바닥에 닿았을 때 다음 페이지에 있는 데이터를 가져오는 부분이다.

스크롤이 바닥에 닿았을 때, 다음 pageNum을 1 증가시켜서 이를 getCards에 prop으로 넣어준다. 그리고 이벤트 리스터를 등록하여 스크롤 이벤트가 생길 때마다 handleScroll을 실행시킨다.

useEffect(() => {
  function handleScroll() {
    const { scrollTop, scrollHeight } = document.documentElement;
    if (scrollTop + window.innerHeight >= scrollHeight) {
      pageNum.current += 1;
      getCards(pageNum.current);
    }
  }
  window.addEventListener('scroll', handleScroll);

  return () => {
    window.removeEventListener('scroll', handleScroll);
  };
}, [pageNum]);

참고한 코드: https://github.com/naramzik/twitter-namecard/commit/703c03c50455d45e8d14b660fa49e54c33ef9001

Intersection Observer API를 사용하면 스크롤이 바닥에 닿았는지를 쉽게 알아낼 수 있다: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

맨 처음에 pageNum이 0일 때 데이터 불러오기

초기에 페이지 진입 시 pageNum이 0일 때의 데이터를 가져오기 위해 useEffect 안에서 한번 데이터를 부른다.

개발 환경에서는 useEffect가 두번 실행되어서 그런지 처음에 pageNum이 0인 데이터가 두번 불러와졌었다. strictMode를 꺼서 해결했는데, 이 방법이 맞는지 모르겠다🤔

useEffect(() => {
  getCards(pageNum.current);
}, []);


🧵 디바운싱 적용하기

디바운싱은 라이브러리를 적용했다.

lodash-es 라이브러리 설치 후 window.addEventListener('scroll', handleScroll)window.addEventListener('scroll', debounce(handleScroll, 100));로 변경해 주었다.

기존에는 스크롤이 1px 움직일 때마다 handleScroll 이벤트가 실행됐는데, 이제는 스크롤을 마음껏 움직이다가 멈춘 후, 100ms가 지나면 그제서야 스크롤 이벤트가 실행된다.

import { debounce } from 'lodash-es';

useEffect(() => {
  function handleScroll() {
    const { scrollTop, scrollHeight } = document.documentElement;
    if (scrollTop + window.innerHeight >= scrollHeight) {
      pageNum.current += 1;
      getCards(query, pageNum.current);
    }
  }
  
  // window.addEventListener('scroll', handleScroll);
  window.addEventListener('scroll', debounce(handleScroll, 100));

  return () => {
    window.removeEventListener('scroll', handleScroll);
  };
}, [query, pageNum]);

디바운싱 적용 전

디바운싱 적용 후

✨ 결과

React Query의 useInfiniteQuery를 쓰지 않고 무한 스크롤을 적용하는데 성공했다👍

🔮 마치며

이번 포스팅에서는 라이브러리 없이 무한스크롤을 구현해보았다. React Query의 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,
})

✅ 오류 해결하기

라이브러리 사용 유무에 상관없이, 스크롤이 바닥에 닿았는데도 바닥보다 미세하게 위에 있는 것으로 판단되어 다음 페이지가 불러와지지 않는 오류가 있었다.

그래서 아래와 같이 바닥에 닿기 500px 전을 감지하여 다음 페이지를 불러오도록 했다. 디바이스에 따라 이런 이슈가 생길 수도 있는 것 같다. 모바일이나 다른 데스크탑에서는 이런 오류가 없었는데😱

const gap = 500;
if (scrollTop + window.innerHeight >= scrollHeight - gap) {
  fetchNextPage();
}

PR: https://github.com/naramzik/twitter-namecard/pull/124

profile
🐹강화하고 싶은 기억을 기록하고 공유하자🐹

0개의 댓글