일단 퍼블 단계에서는 Axios로 첫번째 페이지만 가져와서 뿌려준 상태였다. 또한 Query에서 상태관리도 안되구... 

근데 알아보니까 React Query에 페이징에 특화된 훅이 있었다. 

useInfiniteQuery...

 

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

 

useInfiniteQuery | TanStack Query Docs

const { fetchNextPage,

tanstack.com

useInfiniteQuery frame

일단 기본적인 반환값들은 useQuery와 겹치는게 많지만 추가로 위에 저 반환 값과 옵션들이 새로 생긴거라고 볼 수 있는데, 

fetchNextPage, feetchPreviousPage : 다음, 이전 페이지를 적용해서 fetch를 해줌
hasNextPage, hasPrevious : 다음, 이전 페이지 존재 유뮤 
isFetchingNextPage, isFetchingPreviousPage : 현재 페칭들이 진행중인지
getNextPageParam : 이 쿼리가 새 데이터를 받으면 마지막 페이지와 모든 페이지의 전체 배열을 모두 받는데, 단일 변수로 반                                         환을 해줘야합니다. 

무한 스크롤이랑 페이지네이션에 써먹기에 정말 좋아보인다.. (츄릅)


내 코드에 일단 적용해 보았다. 일단 Axios 코드부터 손 봤다.

export const getProduct = async ({ pageParam = null }) => {
  let response;
  if (!pageParam) response = await axios.get('/product/');
  else response = await axios.get(`/product/?cursor=${pageParam}`);
  return response.data;
};

useInfiniteQuery 훅을 통해서 pageParam이 넘어 올건데 맨처음에 fetch할때는 안넘어올테니까 저렇게 null로 초기화를 해줬다. 그리고 우리 API는 저렇게 cursor에 cursor string을 쿼리로 넣어서 보내줘야지 다음 페이지를 보내준다. 그래서 저렇게 넣어줬다. 

 

const { data, 
        fetchNextPage, 
        hasNextPage, 
        isFetching, 
        isFetchingNextPage 
       } = useInfiniteQuery(['ProductList'], getProduct, {
      getNextPageParam: (lastPage, pages) => lastPage.cursor,
    });

 

내가 useInfiniteQuery 코드다. 일단 당장 딱 필요한 반환값이랑 옵션만 넣었다. 이렇게 data를 반환받으면 

이렇게 뱉어준다. 첫 페이지라서 pageParamssms 없고 pages안에 내가 원하는 값들이 들어있다. 

그 다음에는 이렇게 뱉어준다. 데이터가 계속 쌓이는 걸 볼 수 있다.  버튼에 Button에 fetchNextPage를 달아서 동작을 시켜보자 

 

                          잘 된다.. 이제 무한스크롤을 적용시켜봐야하는데... 역시 라이브러리보단 지원해주는 API를 쓰는게 맞지 않겠나


Intersection Observer API

https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API

 

Intersection Observer API - Web API | MDN

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

developer.mozilla.org

여기서 나는 2번째 경우 라고할 수 있겠다. 어차피 맨아래 요소가 보이면 그냥 바로 fetchNextPage를 하면 되니까...

 

그래서 마지막 요소에 대해서 ref로 타겟해줘야한다.

// 타겟 엘리먼트
  const observer = useRef<IntersectionObserver>();
  
{data?.pages.map((page, i) => (
          <React.Fragment key={i}>
            {page.results.map((result: any, i: number) => {
              // 마지막 요소라면 ref를 넣어준다. 
              if (page.results.length === i + 1) {
                return (
                  <Box key={result.id} ref={lastElementRef}>
                    <ProductCard product={result} onOpen={onOpen} />
                  </Box>
                );
              } else {
                return (
                  <Box key={result.id}>
                    <ProductCard
                      key={result.id}
                      product={result}
                      onOpen={onOpen}
                    />
                  </Box>
                );
              }
            })}
          </React.Fragment>
))}

그리고 lastElementRef가 관찰이 되면 Callback이 될 수 있게 Callback 함수를 아래와 같이 짜주었다. 

 const lastElementRef = useCallback(
    (node) => {
      //** 이미 fetch 중이라면  return*/
      if (isLoading) return;

      // 타겟 엘리먼트의 가시성 변화를 더 이상 감지하지 않는다.
      if (observer.current) observer.current.disconnect();

      // observer.current를 Intersection Observer 인스턴스를 생성
      observer.current = new IntersectionObserver((entries) => {
        // callback으로부터 받은 entries 배열에서 isIntersecting 노출 여부와
        // 다음 페이지가 있는지 확인을 하면 다음 페이지 fetchNextPage
        if (entries[0].isIntersecting && hasNextPage) {
          fetchNextPage();
        }
      });

      // 타겟 엘리먼트 다시 관찰 시작
      // 기존 타겟 엘리먼트는 disconnect되었고,
      // 새로 뿌려진 마지막 요소를 다시 타겟으로 삼게됨.
      if (node) observer.current.observe(node);
    },
    [isLoading, hasNextPage, fetchNextPage],
  );

 

여기서 Intersection Observer가 콜백되면 뱉어주는 entries라는 배열이 있는데 흥미로웠다. 

여기서 나는 isIntersecting만 있으면 되니까 알고만 넘어갔다.