본문 바로가기
frontend

[팁] IntersectionObserver에 대해 알아보자!

by marble25 2023. 1. 10.

React로 프로그래밍을 하다 보니 화면 밖으로 벗어날 때 처리가 필요했다.

내 경우에는 scroll로 화면에 벗어날 때 / 화면으로 들어올 때를 감지하는 것이 필요했다.

관련된 API를 찾아보다가, IntersectionObserver가 Web 기본 스펙에 포함되어 있어서 소개해 보고자 한다.

https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API

사용 예시

  • 페이지가 스크롤 되는 도중에 발생하는 이미지나 다른 컨텐츠의 지연 로딩.
  • 스크롤 시에, 더 많은 컨텐츠가 로드 및 렌더링되어 사용자가 페이지를 이동하지 않아도 되게 하는 infinite-scroll 을 구현.
  • 광고 수익을 계산하기 위한 용도로 광고의 가시성 보고.
  • 사용자에게 결과가 표시되는 여부에 따라 작업이나 애니메이션을 수행할 지 여부를 결정.

과거에는 Intersection 감지를 구현하기 위해 Element.getBoundingClientRect() 등을 사용했는데, 문제는 이를 계산하는 비용이 크다는 데 있다. 웹페이지에는 수많은 element가 있고, element 각각이 intersection 감지를 원한다면 메인 스레드 위에서 동작하는 다음 코드가 문제가 될 수 있다.

IntersectionObserver는 event 방식으로 동작하므로, 성능 면에서 이점이 될 수 있다. 다만, ‘정확히’ 몇 픽셀이 겹쳐졌고, 어떤 픽셀이 겹쳐졌는지는 알 수 없다.

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

// callback으로 들어오는 element는 배열!
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
  });
};

let observer = new IntersectionObserver(callback, options);

let target = document.querySelector('#listItem');
observer.observe(target); // observer.unobserve(target) 으로 해제

IntersectionObserver로 줄 수 있는 옵션은 3가지가 있다.

  • root: 기본값은 브라우저 뷰포트로, 대상 객체의 가시성을 확인할 때 사용하는 요소이다.
  • rootMargin: root 요소의 bouding box를 수축, 증가 가능하다. CSS의 margin처럼 사용할 수 있다. (ex: “10px 20px 30px 40px ”)
  • threshold: 콜백이 실행될 가시성 퍼센테이지를 의미하는 숫자, 숫자 배열이다. 25% 단위로 가시성이 변경되었을 때 실행되게 하려면 [0, 0.25, 0.5, 0.75, 1] 와 같은 배열로 설정하면 된다.

여기서 Intersection은 항상 직사각형으로 계산되는 점에 유의해야 한다. 직사각형이 아닌 모양의 경우 가장 작은 직사각형으로 변형되어 계산된다.

내 경우에는 dropdown을 custom으로 만드는데 사용했다.

  1. Dropdown의 selectable한 옵션을 기존에는 상위 컴포넌트인 dropdown에 relative + 하위 컴포넌트인 옵션에 absolute를 주어 해결했다. 다만 이 경우에는 모달에 표시하는 경우 아래로 스크롤이 생겨버렸다.
  2. 의도된 동작은 항상 화면 위로 표시하는 것이었기에 position을 fixed로 주어야 했다. 하지만 이 경우에도 스크롤을 움직였을 경우 옵션이 dropdown을 따라다니지 못하는 문제가 있었다.
  3. 그래서 position을 fixed로 주되, scroll, resize 이벤트가 발동시 getBoundingClientRect를 계산해서 따라다니도록 설정했다. 그러나 dropdown이 화면에서 벗어난 경우에도 option은 화면에 남아있는 경우가 있었다.
  4. position fixed + scroll, resize 발생시 getBoundingClientRect 계산 + IntersectionObserver로 화면 벗어난 경우 option을 absolute로 변경해서 해결!
useEffect(() => {
    const anchorEl = refAnchor.current ?? null; // dropdown anchor
    if (!anchorEl) {
      return;
    }

    // anchor를 움직일 수 있는 모든 이벤트에 대해서 rect를 다시 계산
    ["scroll", "resize"].forEach(event => {
      window.addEventListener(event, updateRect, true);
    });

    // anchor가 scroll 등의 이유로 화면 밖으로 사라지면 anchor hidden 값을 true로 세팅
    // optionListPlaceToTop가 true라도 anchor가 화면상에 존재하지 않으면 fixed가 아닌 absolute 처럼 동작
    const observer = new IntersectionObserver((entries, _) => {
      entries.forEach(entry => {
        setIsAnchorHidden(!entry.isIntersecting); // isAnchorHidden으로 fixed / absolute 값 결정
      });
    });
    observer.observe(anchorEl);

    // destructor
    return () => {
      observer.unobserve(anchorEl);
      ["scroll", "resize"].forEach(event => {
        window.removeEventListener(event, updateRect, true);
      });
    };
  }, []);

간단한 Dropdown 같아 보이지만, 생각보다 알아야 할 지식이 많다 😅