IntersectionObserver API 검색하시면 정보 나옵니다

(fetching시 사용하는 useInfinityQuery와는 별개의 유틸기능을 하는 훅)

// useInfiniteScroll.ts

import { useEffect, useRef } from "react";

interface UseInfiniteScrollProps {
  onLoadMore: () => void;
  hasNextPage?: boolean;
  isFetchingNextPage: boolean;
  threshold?: number;
}

/**
 * IntersectionObserver API를 사용하여 특정 요소가 뷰포트에 노출되었을 때 추가 데이터를 로드
 * @param onLoadMore - 추가 데이터를 로드하는 콜백 함수
 * @param hasNextPage - 추가로 로드할 데이터가 있는지 여부
 * @param isFetchingNextPage - 현재 데이터를 로드 중인지 여부
 * @param threshold - 요소가 뷰포트에 노출되어야 하는 비율 (0.0 ~ 1.0, 기본값: 0.1)
 * @returns loadMoreRef - 관찰 대상 요소에 연결할 ref 객체
 */
export const useInfiniteScroll = ({
  onLoadMore,
  hasNextPage = false,
  isFetchingNextPage,
  threshold = 0.1,
}: UseInfiniteScrollProps) => {
  const observerRef = useRef<IntersectionObserver | null>(null);
  const loadMoreRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // IntersectionObserver API를 사용하여 무한 스크롤 구현
    // viewPort 내에 요소가 threshold 비율만큼 노출되었을 때 콜백을 실행
    const observer = new IntersectionObserver(
      entries => {
        const [entry] = entries;
        if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) {
          onLoadMore();
        }
      },
      { threshold }
    );

    observerRef.current = observer;

    // 관찰 대상 등록
    // loadMoreRef에 할당된 요소를 observer가 감지할 수 있도록 observer.observe로 등록
    if (loadMoreRef.current) {
      observer.observe(loadMoreRef.current);
    }

    // 클린업: observer 연결 해제
    return () => {
      if (observerRef.current) {
        observerRef.current.disconnect();
      }
    };
  }, [hasNextPage, isFetchingNextPage, onLoadMore, threshold]);

  return { loadMoreRef };
};


(사용한 컴포넌트 부분 참고)

const SaleCardList: React.FC<MySalesCardsProps> = ({
  salesCards,
  onCardClick,
  onLoadMore,
  hasNextPage,
  isFetchingNextPage,
  className = "",
}) => {
  const { openSnackbar } = useSnackbarStore();
  const { loadMoreRef } = useInfiniteScroll({
    onLoadMore,
    hasNextPage,
    isFetchingNextPage,
  });

  return (
    <div className="relative">
      <div className={className}>
        {salesCards.map((saleCard: MySaleCard) => (
          <MyPhotoCard
            key={saleCard.saleCardId}
            myPhotoCard={convertToMyPhotoCardDto(saleCard)}
            onClick={() => handleCardClick(saleCard)}
          />
        ))}
      </div>

      {/* 무한 스크롤 로딩 인디케이터 */}
      <div ref={loadMoreRef} className="py-4 flex justify-center">
        {isFetchingNextPage && <div className="text-main">데이터를 불러오는 중...</div>}
      </div>
    </div>
  );
};

export default SaleCardList;