useInfiniteQuery 와 useInfiniteScroll Hook

Potato·2023년 10월 3일
0

react

목록 보기
1/1

무한 스크롤를 구현할때 여러가지 방법이 있겠지만 React-Query 의 useInfiniteQuery 를 자주 사용하는 편이다.

무한 스크롤을 사용하는 페이지가 별로 없었을때는 굳이 재사용할 수 있는 훅에대한 고민을 하지 않았는데 같은 프로젝트에서 무한 스크롤을 사용하는 페이지가 많아지면 같은 코드가 복사, 붙혀넣기 되는 것이 보기 좋지 않아 훅을 작성의 필요성을 느끼에 되었다.

기존

useInfiniteQuery

const {
    data,
    isLoading,
    isFetched,
    isFetching,
    refetch,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ["shopEveryCustomer", searchText],
    queryFn: async ({ pageParam = 1 }) =>
      shopsApi.getCustomerList(pageParam, "asc", searchText),
    getNextPageParam: (lastPage, allPage) => {
      const totalPage = Math.ceil(lastPage.totalCount / 10);
      const nextPage = allPage.length + 1;
      const hasNextPage = nextPage <= totalPage ? nextPage : undefined;
      return hasNextPage;
    },
    refetchOnMount: true,
    refetchOnWindowFocus: true,
  });

getNextPageParam 의 리턴 값이 queryFn 의 pageParam 이 되고 기본 값은 1로 설정해주었다. 무한 스크롤 동작이 수행될때 fetchNextPage를 이용해 다음 페이지를 불러온다.

이제 무한 스크롤 동작을 수행하기 위해서 스크롤 값 계산이 필요하다.

무한 스크롤 동작
scrollHeight, scrollTop, clientHeight 등은 블로그 에 잘 설명되어있다.

// ...
// 실제 스크롤이 수행되는 Div의 Ref
const scrollableCustomerDivRef = useRef<HTMLDivElement>(null);

useEffect(() => {
    const scrollableCustomerDiv = scrollableCustomerDivRef.current;

    const checkScrollPosition = debounce(() => {
      if (!scrollableCustomerDiv) return;

      // scrollHeight 는 원글 처음부터 화면 바깥까지 포함한 글의 전체 높이
      // scrollTop 은 원글 처음부터 현재 화면에 표시되고 있는 글의 top 까지의 높이
      // clientHeight는 현재 화면에서 보이는 height
      const isNearBottom =
        scrollableCustomerDiv.scrollHeight - scrollableCustomerDiv.scrollTop <=
        scrollableCustomerDiv.clientHeight + 100;

      if (isNearBottom) {
        if (isFetchingNextPage || !hasNextPage) return;
        fetchNextPage();
      }
    }, 250);

    if (scrollableCustomerDiv) {
      scrollableCustomerDiv.addEventListener("scroll", checkScrollPosition);
    }

    return () => {
      if (scrollableCustomerDiv) {
        scrollableCustomerDiv.removeEventListener(
          "scroll",
          checkScrollPosition,
        );
      }
    };
  }, []);

스크롤이 수행되는 Div에 페이지의 끝부분에 도달하면 fetchNextPage를 실행시키는 scroll를 추가해주었다.

앞서 말한 것 처럼 기능은 정상적으로 동작하지만 현재 무한 스크롤을 사용하는 각 페이지마다 비슷한 코드가 존재하는 문제가 생긴다.

규모가 작은 프로젝트여서 위로 인해 문제가 발생한 적은 없지만 다른 프로젝트에서 위와 같이 중복된 부분이 너무 많아져서 유지보수가 매우 어려웠던 경험이 있었기에 로직을 재사용 가능하게 수정하기로했다.

커스텀 훅 만들기

useInfiniteQuery는 각 컴포넌트에서 그대로 사용하던대로 두고
useEffect 부분을 훅으로 분리하기로 하였다.

useInfiniteScroll

import { debounce } from "lodash";
import { RefObject } from "react";

const useInfiniteScroll = (
  scrollableRef: RefObject<HTMLDivElement>,
  fetchNextPage: any,
  hasNextPage: boolean,
  isFetchingNextPage: boolean,
) => {
  const scrollableCustomerDiv = scrollableRef.current;

  const checkScrollPosition = debounce(() => {
    if (!scrollableCustomerDiv) return;

    // scrollHeight 는 원글 처음부터 화면 바깥까지 포함한 글의 전체 높이
    // scrollTop 은 원글 처음부터 현재 화면에 표시되고 있는 글의 top 까지의 높이
    // clientHeight는 현재 화면에서 보이는 height
    const isNearBottom =
      scrollableCustomerDiv.scrollHeight - scrollableCustomerDiv.scrollTop <=
      scrollableCustomerDiv.clientHeight + 100;

    if (isNearBottom) {
      if (isFetchingNextPage || !hasNextPage) return;
      fetchNextPage();
    }
  }, 250);

  if (scrollableCustomerDiv) {
    scrollableCustomerDiv.addEventListener("scroll", checkScrollPosition);
  }

  return () => {
    if (scrollableCustomerDiv) {
      scrollableCustomerDiv.removeEventListener("scroll", checkScrollPosition);
    }
  };
};

export default useInfiniteScroll;

useInfiniteQuery 에서 가져오는 값들과 Ref를 인자로 받는 커스텀 훅으로
재사용할 수 있는 훅을 작성하였다.

0개의 댓글