무한스크롤 구현

걸음걸음·2023년 7월 6일
0

Project

목록 보기
6/6

프로젝트를 진행하면서 useInfinityQuery 을 사용하여 무한스크롤을 구현했다. 구현했다곤 하지만 사실상 메인페이지 담당 팀원이 먼저 구현하신 부분을 코드 동일성과 시간적 여유 등을 이유로 거의 가져다 쓴 것에 가까워서 내 것으로 만들기 위해 다시 정리한다.

useInfiniteQuery

// 공식페이지 예시 코드
const {
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
  isFetchingNextPage,
  isFetchingPreviousPage,
  ...result
} = useInfiniteQuery({
  queryKey,
  queryFn: ({ pageParam = 1 }) => fetchPage(pageParam),
  ...options,
  getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
  getPrevio
// 실제 작성한 코드
import { useInfiniteQuery } from '@tanstack/react-query';
import { GetUserPortfolioPage, PageParam } from '../types';
import { UserPortfolioAPI } from '../api/client';
import { AxiosError } from 'axios';

const { getUserPortfolio } = UserPortfolioAPI;

export const useGetUserPortfolios = (userId: number, size: string, sortOption: string) => {
  const {
    data: UserPortfolios,
    isError: getUserPortfoliosError,
    isFetching: getUserPortfoliosFetching,
    error: ErrorInfo,
    status: userPortfoliosStatus,
    fetchNextPage: fetchNextUserPortfolios,
    hasNextPage: hasNextUserPortfolios,
    isLoading: getUserPortfolioLoading,
  } = useInfiniteQuery<GetUserPortfolioPage, AxiosError>({
    queryKey: ['userPortfolios', userId, sortOption],
    queryFn: ({ pageParam = 1 }: PageParam) =>
      getUserPortfolio(userId, sortOption, pageParam, size),
    getNextPageParam: (lastPage) => {
      if (lastPage.currentPage == lastPage.pageInfo.totalPages) {
        return undefined;
      } else {
        return lastPage.currentPage + 1;
      }
    },
    retry: false,
  });
  return {
    UserPortfolios,
    getUserPortfoliosError,
    getUserPortfoliosFetching,
    ErrorInfo,
    userPortfoliosStatus,
    fetchNextUserPortfolios,
    hasNextUserPortfolios,
    getUserPortfolioLoading,
  };
};

getNextPageParam

다음 pageParam을 가져오는 옵션
lastPage를 인자로 받으며, 이 lastPage는 직전 호출한 페이지 조회 함수의 리턴값

hasNextPage

다음 페이지가 있는지를 true/false값으로 알려주는 옵션
getNextPageParam에서 리턴한 값이 유효한 값이 아니라면 false를 return
해당 프로젝트에서는 데이터를 요청할 때 totalPages 값을 함께 요청하여 getNextPageParam에서 값을 비교하였다.

Intersection Observer

화면에 내가 지정한 Target Element가 노출되었는지 여부를 확인할 수 있는 API

  • Lazy-loading of images or other content as a page is scrolled.
    스크롤에 따라 이미지나 다른 컨텐츠를 lazy-loading할 때
  • Implementing "infinite scrolling" websites, where more and more content is loaded and rendered as you scroll, so that the user doesn't have to flip through pages.
    Infinite scrolling을 통해 점점 더 많은 컨텐츠를 로드하고 렌더할 때
  • Reporting of visibility of advertisements in order to calculate ad revenues.
    광고 수익 계산을 위해 광고의 가시성을 측정할 때
  • Deciding whether or not to perform tasks or animation processes based on whether or not the user will see the result.
    사용자가 화면을 볼 수 있는지 여부에 따라 작업 또는 애니메이션 프로세스를 수행할 때
    출처 : MDN
new IntersectionObserver(callback, options)
// callback : 가시성의 변화가 생겼을 때 호출
// options : 코랩ㄱ이 호출되는 상황 정의

Callback

타겟 요소의 관찰이 시작되거나 가시성에 변화가 감지되면 실행

let callback = (entries, observer) => {
  entries.forEach((entry) => {
    // Each entry describes an intersection change for one observed
    // target element:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });
};

entries

IntersectionObserverEntry 객체 리스트를 배열 형식으로 반환

observer

콜백이 호출되는 IntersectionObserver

Options

let options = {
  root: document.querySelector("#scrollArea"),
  rootMargin: "0px",
  threshold: 1.0,
};

let observer = new IntersectionObserver(callback, options);

root

어떤 요소를 기준으로 target이 들어오고 나가는 것을 확인할 것인지 지정
기본값 : null

rootMargin

root의 범위를 확장하거나 축소 가능
기본값 : 0px

threshold

target이 뷰포트에 얼마나 진입해야(root와 얼마나 교차되어야) callback을 호출할 것인지를 표시
기본값 : 0
(0 : target이 영역에 진입 시작, 1 : target 전체가 진입)

적용

프로젝트에는 이러한 과정을 useHook으로 만들어서 진행

export function useInfiniteScroll({
  targetRef,
  isError,
  isFetching,
  hasNext,
  fetchNext,
  // targetRef와 useInfiniteQuery의 값을 전달
}) {
  useEffect(() => {
    const options = {
      root: null,
      rootMargin: '100px',
      threshold: 0.3,
    };

    const handleIntersect: IntersectionObserverCallback = (
      entries: IntersectionObserverEntry[],
    ) => {
      entries.forEach((entry) => {
        // 에러가 일어나지 않고, 다음 페이지가 있을 경우 => 다음 페이지를 불러오는 api 실행
        if (entry.isIntersecting && !isError && hasNext) {
          fetchNext();
        }
      });
    };

    const observer = new IntersectionObserver(handleIntersect, options);

    if (targetRef.current) {
      observer.observe(targetRef.current);
    }

    return () => {
      if (targetRef.current) {
        observer.unobserve(targetRef.current);
      }
    };
  }, [fetchNext, hasNext, isFetching]);
}
  • 해당 API 에 대해서 노마드코더에서 올린 유툽 영상도 발견.
profile
꾸준히 나아가는 개발자입니다.

0개의 댓글