useInfiniteQuery 로 무한스크롤 구현하기-(1)

가연·2023년 9월 25일
1

관리자 페이지에서 정보 수정요청과 새 장소 신청 요청 각각 무한스크롤로 정보를 받아와야 했다.

react-query로 비동기 관리를 했기 때문에 useInfiniteQuery를 이용하기로 했다.

무한스크롤 구현하기

useRef 로 바닥 부분을 가져오고, 그 바닥부분과 뷰포트의 교차점을
IntersectionObserver 로 계산하며 useInfiniteQuery로 다음 페이지의 여부를 알아낸 후 bottom 이 루트 요소와 교차 상태로 들어가면서 로딩상태가 아니고, 다음페이지가 있으면 useInfiniteQuery 의 fetchNextPage를 이용해 다음 페이지를 불러오는 방식으로 구현했다.

1. 오프셋 기반 vs 커서 기반

오프셋 방식은 마지막 데이터를 기준으로 이후 값만 조회
커서기반은 사용자에게 응답해준 마지막 데이터의 식별자 값을 Cursor로 사용한다.
useInfiniteQuery 는 커서기반이다.

2. useInfiniteQuery

useInfiniteQuery는 파라미터 값만 변경하여 동일한 useQuery를 무한정 호출할 때 사용된다.

옵션 지정하기

const {
    data: data1,
    isLoading: isLoading1,
    fetchNextPage: fetchNextPage1,
    hasNextPage: hasNextPage1,
    error: error1,
    isError: isError1,
  } = useInfiniteQuery(
    ["basicInfo"],
    ({ pageParam = 0 }) => basicInfoEditRequest(pageParam),
    {
      getNextPageParam: (lastPage, allPages) => {
        const nextPage = allPages.length;
        const totalPage = lastPage?.data?.response?.totalPages;
        return nextPage > totalPage || nextPage == totalPage ? null : nextPage;
      },
    }
  );
  • queryKey, queryFn

    useQuery와 기본적인 기능은 같기 때문에 동일한 형식으로 사용해준다. 이때 pageParam 은 처음 요청할 페이지다.
    우리는 0페이지부터 시작하여 0으로 지정해줬다.

  • getNextPageParam

    getNextPageParam: (lastPage, allPages) => unknown | undefined
    getNextPageParam은 다음 요청페이지의 PageParam 을 리턴해준다. 만약 undefined(or null) 가 리턴된다면 다음 페이지가 없다는 뜻이다.
    총 두개의 파라미터를 가지며
    lastPage는 useInfiniteQuery를 이용해 호출된 가장 마지막에 있는 페이지 데이터 이다.
    allPages는 useInfiniteQuery를 이용해 호출된 모든 페이지 데이터를 의미한다.
    나는 allPages.length 로 다음 페이지의 param 을 구하고(0페이지부터 시작했으므로 불러온 데이터들의 길이가 다음페이지의 파라미터값이 된다.) 만약 다음페이지가 전체페이지수보다 크거나 같다면 null 을 반환하고, 그보다 작으면 다음 페이지를 return했다.

리턴값

  • hasNextPage

    getNextPageParam 의 리턴값으로 boolean 값이 정해진다.
    다음 page가 있으면 true, 리턴값이 null or undefined 라면 false 가 된다.

  • fetchNextPage

    다음 페이지의 데이터를 호출할 때 사용.
    fetchNextPage를 이용해 호출된 데이터는 배열의 가장 우측에 담겨 전달받는다.
    ex) [ '1page', '2page', '3page' ]

3. IntersectionObserver

브라우저 뷰포트(Viewport)와 설정한 요소(Element)의 교차점을 관찰하며, 요소가 뷰포트에 포함되는지 포함되지 않는지 구별하는 기능을 제공.
비동기적 실행 -> 스크롤 같은 이벤트 연속 호출 가능

const io = new IntersectionObserver((entries, observer) => {}, options) // 관찰자 초기화
io.observe(element) // 관찰할 대상(요소) 등록

관찰 할 대상이 등록되거나 가시성에 변화가 생기면 콜백을 실행한다. 콜백은 entries, observer 를 인자로 갖는다.

entries

인스턴스 배열.
boundingClientRect(관찰 대상의 사각형 정보),isIntersecting(관찰 대상의 교차 대상),target(관찰 대상 요소) 등의 읽기 정보를 제공해 준다.

  useEffect(() => {
    const io1 = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting && !isLoading1 && hasNextPage1) {
            fetchNextPage1();
          }
        });
      },
      {
        threshold: 0.5,
      }
    );


intersecing 은 관찰 대상이 루트 요소와 교차 상태로 들어가거나(true) 교차 상태에서 나가는지(false) 여부를 나타내는 값(Boolean)이다.
만약 관찰요소가 교차상태로 들어가고, 로딩 상태가 아니며, 다음페이지가 있으면 다음 페이지를 요청하는 fetchNextPage를 실행한다.

threshold

옵저버가 실행되기 위해 타겟의 가시성이 얼마나 필요한지 백분율로 표시.
기본값은 Array 타입의 [0]이지만 Number 타입의 단일 값으로도 작성할 수 있다.
ex) [0, 0.3, 1]: 타겟의 가시성이 0%, 30%, 100%일 때 모두 옵저버가 실행된다.

	const bottomObserver1 = useRef(null);

...
    

    if (bottomObserver1.current) {
      io1.observe(bottomObserver1.current);
    }

    return () => {
      if (bottomObserver1.current) {
        io1.unobserve(bottomObserver1.current);
      }
    };
  }, [isLoading1, hasNextPage1, fetchNextPage1]);

io.observe : 대상 요소 관찰 시작
io.unobserve : 대상 요소 관찰 중단

useRef 로 bottom 을 인식한 후, 스크롤이 바닥에 닿으면 옵저버가 실행된다(콜백 함수 실행). 그리고, 대상 요소의 관찰을 중단하는 함수를 return 해서 다음페이지를 불러온 후 관찰을 중단할 수 있게 해준다.


레퍼런스
react-query 공식사이트 : https://tanstack.com/query/v4/docs/react/reference/useInfiniteQuery

useInfiniteQuery : https://jforj.tistory.com/246!

IntersectionObserver: https://heropy.blog/2019/10/27/intersection-observer/

0개의 댓글