[React] Custom 하게 infinte scroll 설계 구현하기

김현수·2023년 12월 9일
0

React

목록 보기
10/29


🖍️ Infinite Scroll 설계 구현하기


나의 코드

  • throttle

    • 일정 주기마다 이벤트를 모아서 이벤트가 한 번만 발생하도록 하는 기술
    • 스크롤과 같은 빠르거나 연속적인 이벤트 중에 이벤트 처리기 호출 수를 제한

    • 성능을 최적화하고 불필요한 부하를 줄이는 데 도움
    • 특히 DOM 조작 또는 API 호출과 같은 리소스 집약적인 작업이 있는 시나리오에서 중요
export const throttle = (handler, timeout = 500) => {
  let invokedTime;
  let timer;
  return function (...args) {
    if (!invokedTime) {
      handler.apply(this, args);
      invokedTime = Date.now();
    } else {
      clearTimeout(timer);
      timer = window.setTimeout(() => {
        if (Date.now() - invokedTime >= timeout) {
          handler.apply(this, args);
          invokedTime = Date.now();
        }
      }, Math.max(timeout - (Date.now() - invokedTime), 0));
    }
  };
};
- 기본값은 500밀리초, 실행 속도를 제한하여 handler 실행

- 프로세스 
  - 첫 번째 스크롤 이벤트는 핸들러를 즉시 트리거

  - 500밀리초 내에 후속 스크롤을 할 때마다 타이머가 재설정

  - 사용자가 스크롤을 중지하고 최소 500밀리초 동안 일시 중지되면
    최종 스크롤 위치를 반영하여 핸들러가 다시 실행

- 예외 상황 처리
   - Math.max 를 통해 제한 지연이 음수가 되지 않도록 설정

- apply 란?
   - 제한된 함수를 올바른 컨텍스트 및 인수로 호출 가능
   - 다양한 시나리오에 적용하도록 설정 가능

  • useInfinteScroll

    • 데이터 최적화를 위해 필요한 페이지만 데이터 패칭
    • scroll 을 통해 무한 패치

    • 사용자가 페이지를 아래로 스크롤할 때 데이터를 가져오고 관련 상태를 관리
import { useState, useEffect, useCallback } from "react";
import { initParams } from "../services/local";
import { getBackOptions } from "../api/getBackOptions";

import { throttle } from "../utils/throttle";

const useInfiniteScroll = (fetcher, { size, onSuccess, onError }) => {
  const [page, setPage] = useState(1);
  const [totalCounts, setTotalCounts] = useState(-1);
  const [data, setData] = useState([]);
  const [isFetching, setFetching] = useState(false);
  const [hasNextPage, setNextPage] = useState(true);
  const [backParams, setBackParams] = useState(getBackOptions(initParams()));

  // 패치되는 데이터 설정 콜백
  const executeFetch = useCallback(async () => {
    try {
      const data = await fetcher({ page, ...backParams });
      setData((prev) => prev.concat(data.contents));
      setTotalCounts(data.totalCounts);
      setPage(data.pageNumber + 1);
      setNextPage(!data.isLastPage);
      setFetching(false);
      onSuccess?.();
    } catch (err) {
      onError?.(err);
    }
  }, [page]);

  // scroll event 발생할 시 비동기 fecth event 
  useEffect(() => {
    const handleScroll = throttle(() => {
      const { scrollTop, offsetHeight } = document.documentElement;
      if (window.innerHeight + scrollTop >= offsetHeight * 0.9) {
        setFetching(true);
      }
    });
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, []);

  // fetch 해야하는데 hasNestPage 가 true 면 동작
  useEffect(() => {
    if (isFetching && hasNextPage) {
      executeFetch();
    } else if (!hasNextPage) setFetching(false);
  }, [isFetching]);

  // 초기화
  useEffect(() => {
    setPage(1);
    setData([]);
    setNextPage(true);
    setFetching(true);
  }, [backParams]);

  return {
    hasNextPage,
    data,
    totalCounts,
    isFetching,
    setFetching,
    setBackParams,
  };
};

export default useInfiniteScroll;
  • 실행 코드

 const scrollRef = useRef(null);
 const {
    data,
    isFetching,
    totalCounts,
    hasNextPage,
    setFetching,
    setBackParams,
  } = useInfiniteScroll(getOptionHouses, { size: 24 });
...

 // reload, scroll 조건부 이벤트 발생할 때 
 useEffect(() => {
    // if (isLastPage) return;
    const handleScroll = () => {
      const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;

      if (scrollTop + clientHeight >= scrollHeight * 0.9) {
        setFetching(true);
      }
    };

    const throttleScrollHandler = throttle(handleScroll);

    scrollRef.current.addEventListener("scroll", throttleScrollHandler);
    scrollRef.current.addEventListener("beforeunload", () => {
      return () =>
        scrollRef.current.removeEventListener("scroll", throttleScrollHandler);
    });
  }, [data]);

  // loading set
  useEffect(() => {
    if (hasNextPage) {
      setLoading(isFetching);
    }
  }, [isFetching]);

...

 /// useRef 를 Grid Layout 에 설정
return (
  ...
 		 <Grid
            w={"100vw"}
            pl="5vw"
            pr="5vw"
            gridTemplateColumns={{
              sm: "1fr",
              md: "1fr 1fr",
              lg: "repeat(3, 1fr)",
              xl: "repeat(4, 1fr)",
            }}
            ref={scrollRef}
            overflowY={"scroll"}
            h="100%"
            sx={scrollbarStyle}
          >
            {data?.map((item, idx) => {
              return (
                <GridItem key={idx}>
                  <HouseCard {...item} />
                </GridItem>
              );
            })}
          </Grid>
  ...
)
profile
일단 한다

0개의 댓글