React

[React] Intersection Observer API : 무한 스크롤 구현

hid1 2022. 7. 3. 17:25

 

 

Intersection Observer API - Web API | MDN

Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.Intersection Observer API는 타겟 요소와 상위 요소 또는

developer.mozilla.org

 

🤔Intersection Observer API란?


Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport사이의 교차점 (intersection) 내의 변화를 비동기적으로 관찰하는 방법이다.

 

즉,  내가 지정한 요소가 화면(뷰포트) 상에 보이고 있는지를 관찰하는 API이다.

 

 

왜 사용할까?


기존의 scroll 같은 이벤트 기반의 요소 관찰에서 발생하는 렌더링 성능이나 이벤트 연속 호출 같은 문제 없이 사용 가능

     - scroll 이벤트는 동기적으로 실행 -> 메인 스레드에 큰 부하를 줄 수 있다.

     - getBoundingClientRect 사용시 reflow 발생  

        reflow : 엘리먼트가 문서내에서 어디에 위치하는지에 대한 정보를 계산해서 다시 렌더링을 하는 현상

Intersection Observer API는 비동기적으로 실행하여 메인 스레드에 영향을 주지 않는다.

IntersectionObserverEntry의 속성을 활용하면, reflow 없이 element의 위치를 감지할 수 있다.

 

어떤 상황에 사용할까?


  • Lazy-loading
  • infinite scrolling
  • 광고 수익 계산을 위한 광고의 가시성 확인
  • 사용자가 결과를 봤는지에 따라 애니메이션 혹은 어떠한 동작을 수행할지 안할지 결정

 

 

 

 어떻게 사용할까?


 intersection observer 생성

const io = new IntersectionObserver(callback, options);
io.observe(element);

new IntersectionObserver()를 통해 생성한 인스턴스(io)로 관찰자(Observer)를 초기화하고 관찰 대상(element) 지정

Methods

io.observe(element)  // => element 관찰 시작
io.unobserve(element) // => element 관찰 중단
io.disconnect() //  => 모든 element 관찰 중단

callback

관찰 대상이 등록되거나 가시성(Visibility, 보이는지 보이지 않는지) 변화가 생기면 콜백 실행

콜백은 2개의 인수(entriesobserver)를 가진다.

entries

const io = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    entry.boundingClientRect // => element.getBoundingClientReac()
    entry.intersectionRect // => element의 감지된 부분의 사각형 정보
    entry.intersectionRatio // => element가 루트 요소와 얼마나 겹치는 지 0~1 사이
    entry.isIntersecting // => element의 교차 상태
    entry.rootBounds // => 루트 요소의 사각형 정보
    entry.target // => element
    entry.time // => element와 루트 요소의 교차 발생시간
  })
}, options)

io.observe(element)

observer

콜백이 실행되는 해당 인스턴스를 참조

options

root => 기본값 null, 관찰 대상을 검사하기 위해 뷰포트 대신 사용할 요소 지정

rootMargin  => 기본값 0px 0px 0px 0px, root의 margin 값 설정

threshold => 기본값 0,  콜백이 실행되기 위한 백분율 지정 0 ~ 1, 혹은 배열

 

 

✨리액트로 무한 스크롤 구현


 const [list, setList] = useState([]);
 const [target, setTarget] = useState(null);
 const [pageNumber, setPageNumber] = useState(0);
 
 // 초기값 렌더링, pageNumber 바뀔 때마다 호출
 useEffect(() => {
 	// ~데이터 가져오는 로직~
 	// setList((list) => list.concat(data))
  }, [pageNumber]);
 

 const callback = useCallback(
    (entry, observer) => {
      if (entry[0].isIntersecting) {
        setPageNumber((prev) => prev = prev + 1)
        observer.unobserve(entry[0].target);
      }
    },
    [setPageNumber]
  );
 
 useEffect(() => {
    if (!target) return;
    const io = new IntersectionObserver(callback);
    io.observe(target);
    return () => observer.disconnect();
  }, [callback, target]);
 
 
 return (
    <>
      <div>
        {list.map((item) => (
          // list item 요소 출력
        ))}
      </div>
      <div ref={setTarget}></div>
    </>
  );

 

clean-up

 - 스크롤을 내려 새로운 목록을 받아오게 되므로 관찰할 대상이 바뀌기 때문에 disconnect()로 관찰요소를 없애고 새로 지정

useCallback

 - 재렌더링 시 함수가 새로 만들어지지 않는다. 함수 안에서 사용하는 state 또는 props가 있다면 deps 배열에 포함

 

custom hook으로 만들기


// useIntersectionObserver.jsx

import { useEffect, useState } from "react";

const useIntersectionObserver = ({ callback }) => {
  const [target, setTarget] = useState(null);

  useEffect(() => {
    if (!target) return;
    const observer = new IntersectionObserver(callback);
    observer.observe(target);
    return () => observer.unobserve(target);
  }, [onIntersect, target]);

  return { setTarget };
};

export default useIntersectionObserver;
// 사용할 곳

const callback = useCallback((entry, observer) => {생략}, [])
 
 const { setTarget } = useIntersectionObserver({ callback });
 
 return (
    <>
      // 생략
      <div ref={setTarget}></div>
    </>
  );

 

 

✍️개인적인 정리


 

+ 렌더링이 두 번 발생


구현하다가 처음에 데이터 호출이 두 번씩 돼서 왜 그런가 했더니 React.StrictMode가 적용되어서 그랬다.

creat-react-app으로 생성하면 잠재적인 문제를 알아내기 위한 도구로 StrictMode가 기본으로 적용된다.

개발 모드에서만 활성화되기 때문에, 프로덕션 빌드에는 영향을 끼치지 않는다고 한다. 지워주면 한 번만 호출이 된다.

ReactDOM.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>,
    document.getElementById("root")
);

 

 

 

반응형