반응형

백엔드는 프리즈마 ORM을 사용했고 커서기반으로 구현했습니다

 

Page

const ItemPage = () => {
  const TAKE = 10;
  const MEMBER_ID = 1; // 1 ~ 5
  const FIRST_CURSOR = 1;

  const { data, fetchNextPage, hasNextPage, isFetching } =
    useInfiniteFindItemsQuery(
      {
        memberId: MEMBER_ID,
        lastCursorId: FIRST_CURSOR,
      },
      {
        getNextPageParam: (lastPage, allPages) => {
          if (lastPage.findItems.length === 0) {
            return undefined;
          } else {
            return {
              memberId: MEMBER_ID,
              lastCursorId:
                lastPage.findItems[lastPage.findItems.length - 1]?.id + 1,
            };
          }
        },
        enabled: false,
        initialPageParam: {
          memberId: MEMBER_ID,
          lastCursorId: FIRST_CURSOR,
        },
      },
    );

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

  const observerRef = useIntersectionObserver({
    isFetching,
    hasNextPage,
    fetchNextPage,
  });

  return (
    <main className="mx-24 my-16">
      <SearchBar />
      {/*<Suspense fallback={ItemPage.Skeleton()}>*/}
      <div className="grid grid-cols-5 gap-x-14">
        {data?.pages.map((page, pageNum) =>
          page.findItems.map((item, index) => (
            <Item index={index + pageNum * TAKE} key={item.id} item={item} />
          )),
        )}
      </div>
      {/*</Suspense>*/}
      <div ref={observerRef} className="h-1" />
    </main>
  );
};

export default ItemPage;

 

 

최하단 엘리먼트 요소 감지 훅 및 다음 페이지 데이터 Fetch

import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
  FetchNextPageOptions,
  InfiniteData,
  InfiniteQueryObserverResult,
} from '@tanstack/react-query';
import { IFindItemsQuery } from '@/gql/generated/graphql';

//hook props interface
interface IuseIntersectionObserverProps<TData = unknown> {
  isFetching: boolean | undefined;
  hasNextPage: boolean | undefined;
  fetchNextPage: (
    options?: FetchNextPageOptions,
  ) => Promise<
    InfiniteQueryObserverResult<InfiniteData<TData, unknown>, unknown>
  >;
}

export const useIntersectionObserver = <TData>({
  isFetching,
  hasNextPage,
  fetchNextPage,
}: IuseIntersectionObserverProps<TData>) => {
  const options = {
    root: null, // 감지할 대상 지정 null일 경우 뷰포트가 대상
    rootMargin: '0px', // root요소가 가장자리에 도달하는 즉시 교차 이벤트 감지
    threshold: 1.0, // 얼마나 많이 root요소와 교차하는지 설정 → 1.0 = 100% 완전히 겹친다
  };

  const handleIntersection = useCallback(
    (entries: IntersectionObserverEntry[]) => {
      const target = entries[0];
      if (target.isIntersecting && hasNextPage && !isFetching) {
        fetchNextPage();
      }
    },
    [hasNextPage, isFetching, fetchNextPage],
  );

  const observerRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(handleIntersection, options);
    if (observerRef.current) {
      observer.observe(observerRef.current);
    }
    return () => {
      observer.disconnect();
    };
  }, [handleIntersection, options]);

  return observerRef;
};

 

반응형