무한 스크롤 (라이브러리 없이 구현하기)

bloom74·2024년 10월 16일

무한 스크롤 페이지 만들기

  • 먼저, 무한 스크롤을 하기 위해서는 ?
    - 너무 빠르게 모든 데이터를 받아올 수 없기 때문에, throttle, debounce를 걸어서 일정 시간이 지난 후에 요청이 수행되도록 만든다.
    - throttle, debounce 차이점
    - throttle -> 정해둔 기간동안 한번의 요청만 실행된다 -> 500ms 동안 한번의 이벤트만
    - debounce -> 정해둔 기간동안 한번의 요청만 실행. 같은 요청이 다시 들어오면, 이전 요청을 취소하고 최신 요청을 기억 및 실행
    - 무한 스크롤에는 debounce보다, throttle이 더 적합.
    - 사용자가 스크롤을 멈추기를 기다리기 보다는, 스크롤이 어느지점에 다다르면, 데이터를 먼저 display하는 것이 좋다. -> 이벤트를 실행하는 방식. eventListener

하지만, 스크롤 이벤트로 무한 스크롤을 구현한다면, 리플로우에 의해 좋지 않은 렌더링 성능과 상황에 따라 기대한 대로 동작하지 않을 수 있다. 이를 해결하기 위해 나온것이 Intersection Observer API이다.

스크롤 이벤트로 무한 스크롤을 구현하면 안좋은 점
영향을 받는 모든 요소에 대해 필요한 정보를 축적하기 위해 이벤트 처리기와 getBoundingClientRect()와 같은 메서드를 호출하는 루프를 호출하면, 이는 모두 메인 스레드에서 실행되기 때문에 이 중 하나라도 성능 문제를 일으킬 수 있다.

Intersection Observer

  • Intersection Observer API는 상위 요소 또는 최상위 문서의 viewport와 대상 요소 사이의 변화를 비동기적으로 관찰할 수 있는 수단을 제공한다. 아래와 같은 이유로 Intersection Observer가 필요했다.
    - 페이지가 스크롤 될 때 이미지의 도는 다른 컨텐츠의 지연 로딩을 위해
    - 무한 스크롤 사이트를 구현함으로써 사용자 페이지를 넘길 필요가 없다.
    - 광고 수익 산정을 위해 광고 가시성을 보고한다. -> 사용자가 몇분동안 해당 광고를 봤는지 알 수 있다.
    - 사용자가 결과를 볼 수 있을지 여부에 따라 작업 또는 애니메이션 프로세스를 수행할지 여부를 결정한다.

Intersection Observer를 이용해서 무한 스크롤을 구현하기 위해서는 root, observer, 대상이 필요하다.

  • root: 관찰될 범위 -> null로 보통 지정하면 viewport를 관찰한다.
  • observer: 최초로 대상 요소를 관찰하라고 요청 받는 시점을 말한다.

관찰자를 생성하기 위해서는 생성자를 호출하고, 교차될때 실행할 callback 함수를 전달하려 생성해야 한다.

내가 정리한 이용 방법은 아래와 같다.
1. 생성자를 통해 콜백 함수와 options를 넣은 하나의 관찰 함수를 생성한다.
2. 관찰할 요소를 observer.observe()로 지정한다.
3. 지정된 요소가 root와 교차하면, callback 함수가 실행된다.
4. callback함수에 교차 상태를 알려주는 entry 객체 목록이 들어온다.
5. entry 각 객체에서 isIntersecting의 불리언 유무를 통해 교차하고 있으면, 필요한 데이터를 가져오는 함수를 실행시킨다.

interface DataMock {
  isEnd: boolean;
  data: MockData[];
}

function App() {
  const [page, setPage] = useState<number>(0);
  const [currentData, setCurrentData] = useState<MockData[]>([]);
  const [isEnd, setIsEnd] = useState(false);
  const observerRef = useRef(null);

  const getData = useCallback(
  // 3. 콜백 함수의 인자로 entries 객체  array를 받음. 각 entry는 현재 교차 상태인지 알려주는 값들을 담고 있음. 
    async (entries: IntersectionObserverEntry[]) => {
      if (isEnd) return;

      if (entries[0].isIntersecting && !isEnd) {
        const result = async () => await getMockData(page);

        const { data, isEnd }: DataMock = await result();

        if (data.length) {
          setPage((prev) => prev + 1);
          setCurrentData((prev) => [...prev, ...data]);
        }

        if (isEnd) setIsEnd(true);
      }
    },
    [page, isEnd]
  );

  useEffect(() => {
    const options = {
      root: null,
      rootMargin: "0px",
      threshold: 0.5,
    };

    const observer = new IntersectionObserver(getData, options); // 1. 생성자를 통해 관찰자 생성 

    if (observerRef.current) observer.observe(observerRef.current); // 2. ref값이 생기면, 바로 관찰 시작. 

    return () => {
      if (observerRef.current) {
        observer.unobserve(observerRef.current);
      }
    };
  }, [getData, isEnd]);

  const totalNum = useMemo(() => {
    return currentData.reduce((acc, cur) => {
      return (acc += cur.price);
    }, 0);
  }, [currentData]);

  return (
    <>
      <div className="totalNum">{totalNum}</div>
      <ul className="box">
        {currentData.length &&
          currentData.map((el) => {
            return (
              <li className="boxElement" key={el.productId}>
                {el.productName}
              </li>
            );
          })}
      </ul>
      <div ref={observerRef}>observer</div>
    </>
  );
}

export default App;

0개의 댓글