React

reactQuery의 infiniteQuery로 infiniteScroll을 구현해보자 (w/ react-intersection-observer)

썽연 2022. 11. 15. 14:44
728x90

많은 데이터를 확인할 때, 보통 페이지네이션 또는 인피니트스크롤로 구현을 하곤 한다.

저번 프로젝트에서는 페이지네이션을 이용하였는데, 이번에는 인피니트 스크롤을 이용해보았다.

인피니트 스크롤이란?

사용자가 버튼을 누르거나, 특정 지점을 스크롤 할 때, 새로운 데이터를 가져오는 방법이다.

reactQuery를 이용하여, 데이터를 패치하여 인피니트 스크롤을 구현해보겠다.

reactQuery의 포스팅은 여기에서 했으니 궁금하면 먼저 보고오는 것도 좋다.

useQuery

useMutation

무한 사용을 위한 옵션쿼리는 다음을 추가한 useQuery hook과 동일하며 isRefetching에서 차이가 있다.

useInfiniteQuery에 대해서 알아보자

  • data는 infinte query 데이터가 담겨있는 객체
  • data.pages - fetch한 페이지들이 담겨있는 배열
  • data.pageParams - 페이지들을 fetch 하는 데에 필요한 page params가 담겨있는 배열
  • getNextPageParam, getPreviousPageParam - 옵션을 불러올 데이터가 있는지 여부
  • getNextPageParam - undefined가 아닌 다른 값을 반환하면 hasNexrPage는 true이다.
  • getPreviousPageParam - undefined가 아닌 다른 값을 반환하면 hasPreviousPage값이 true이다.
  • isFetchingNextPage, isFetchingPreviousPage - 백그라운드 새로고침 상태인지 추가 로딩 상태인지 구분할 수 있다.

react-intersection-observer의 useInview를 이용하여 데이터 패치하기

react-intersection-observer가 무엇일까?

Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 api이다.

useInview 훅으로 요소를 모니터링 할 수 있으므로, 화면에서 inview로 모니터링 하는 요소가 보이면, 다음의 데이터 패치를 진행한다

<aside> 💡 Scroll Event를 사용해서 구현할 때 사용하는 debounce & throttle을 사용하지 않아도 된다. offsetTop 값을 구할 떄는 정확한 값을 구하기 위해서 매번 layout을 새로 그리는데 이를 Reflow라고 한다. Intersection Observer를 사용하면 Reflow를 하지 않아도된다.

</aside>

react-intersection-observer를 설치하자

yarn add react-intersection-observer
...
import { useInView } from 'react-intersection-observer';

const Guidebook = () => {
	const { ref, inView } = useInView();

	return (
		<>
			<div ref={ref}></div>
		</>
	)
}

위 코드와 같이 useInview의 ref로 현재 보여지고 있는 돔의 위치를 확인할 수 있다.

useInfiniteQuery와 react-intersection-observer을 이용하여 무한스크롤을 구현하자

import { useInfiniteQuery } from '@tanstack/react-query';
import { useRecoilValue } from 'recoil';
import { searchAtom } from '../atoms/searchAtom';
import axios from 'axios';

const useGuidebook = () => {
  const search = useRecoilValue(searchAtom);

  const fetchProjects = async ({ pageParam = 1 }) => {
    const res = await axios(
      `api&page=` + pageParam,
    );
    return res.data;
  };

  const { data, status, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
    queryKey: ['search', search],
    queryFn: fetchProjects,
    getNextPageParam: lastPage => {
      const page = lastPage?.next?.split('?page=');
      const result = page?.[1].split('&')[0];
      return result;
    },
  });

  return {
    data,
    status,
    fetchNextPage,
    isFetchingNextPage,
  };
};

export default useGuidebook;

보통 api의 결과값으로 page의 번호만 나오는 코드들이 많았지만, 나는 next의 api주소 전체가 나왔기 때문에, api전체가 next가 될 수 있도록 해주었다.

다만 쿼리가 조금 잘못되었는지, 뭔가 이상하게 찔리는 것 같아서 페이지 수만 변경을 해주기로 하였다.

이 부분은 조금 더 공부해야겠다.

getNextPageParam은 현재페이지와 모든 페이지를 인수로 받을 수 있다.

현재 페이지의 결과값이 뜰 수 있었기에 나는 next가 링크이므로, 링크 값을 받았다.

여기서 모두 nextCursor으로 예제 코드가 많은데 주의하자!

나의 코드는 page가 1로 시작하여, next가 있으면 다음 페이지를 찌르는 방식으로 구현하였다.

데이터를 불러오는 컴포넌트의 코드는 다음과 같다.

import styled from 'styled-components';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { searchAtom } from '../atoms/searchAtom';
import { ChangeEvent, useEffect, useState } from 'react';
import useGuidebook from '../hooks/useGuidebook';
import { useInView } from 'react-intersection-observer';

const GuideBook = () => {
  const [keyword, setKeyword] = useState('');
  const search = useRecoilValue(searchAtom);
  const setSearch = useSetRecoilState(searchAtom);
  const { data, fetchNextPage, isFetchingNextPage } = useGuidebook();
  const { ref, inView } = useInView();

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    setKeyword(e.target.value);
  };

  const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
      setSearch(keyword);
    }
  };

  useEffect(() => {
    if (inView) fetchNextPage();
  }, [fetchNextPage, inView]);

  return (
    <>
      {search && (
        <>
          <Result>
            <Num>{data?.pages[0].count}</Num>건의 검색 결과가 있습니다.
          </Result>
          {data?.pages.map((page, index) => (
            <Content key={index}>
              {page.results.map((r, i) => (
                <StyledLi key={i}>
                  {r.title}
                  <Date>
                    ({r.tag}/{r.date})
                  </Date>
                </StyledLi>
              ))}
            </Content>
          ))}
          {isFetchingNextPage ? <div>Loading...</div> : <div ref={ref}></div>}
        </>
      )}
    </>
  );
};

export default GuideBook;

inview와 fetchNextPage를 useEffect의 인자로 넘겨주어 값이 바뀌면 fetchNextPage함수가 실행이 되도록 하였다.

즉, 이 말은 맨 끝의 돔이 보이거나, 다음 페이지로 넘길 값이 있으면 다음 페이지로 넘어가는 코드가 실행이되도록 하였다.

 

 

참고 문서

https://tanstack.com/query/v4/docs/reference/useInfiniteQuery

https://velog.io/@sorin44/React-Query-Section3-Infinite-Queries-for-Loading-Data-Just-in-Time동적-데이터-로드를-위한-무한-쿼리

728x90