Intersection Observer Deep Dive

Intersection Observer Deep Dive

목차

  • 특정 위치에 도달했을 때 어떤 행위를 해야한다면?
    • 어떻게 구현할까?
    • 구현 (scroll)
    • 문제점
  • intersection Observer란
    • 구현
    • 사용법
    • 참고

회사 프로젝트에서 Intersection Observer를 활용할 일이 생겼다. 오랜만에 블로그 포스팅을 위해 Intersection Observer 발표 자료를 살짝 다듬었다.

특정 위치에 도달했을 때 어떤 행위를 해야한다면?

위 Codepen을 실행해보자~ 화면에 상자가 보일 때 애니메이션이 실행된다.

위와 같이 구현하려면 어떻게 해야할까? 고민해보자

어떻게 구현할까?

우선 떠오르는 건 Scroll 이벤트로 구현하는 것이다.

Scroll 이벤트로 구현

HTML

<div class="example">
    <h1 class="title">Scroll Down <span class="arrow">👇</span></h1>
    <div class="box"></div>
    <div class="box"></div>
    <div class="box"></div>
</div>

JS

// 해당 요소가 viewport 내에 있는지 확인하는 함수
function isElementInViewport(el) {
  var rect = el.getBoundingClientRect();

  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <=
      (window.innerHeight || document.documentElement.clientHeight) &&
    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  );
}

// scroll 이벤트를 추가하고, 해당 element에 callback 함수를 등록하는 함수
const addEventToEl = (elList) => {
  document.addEventListener("scroll", () => {
    elList.forEach((el) => {
      if (isElementInViewport(el)) {
        el.classList.add("tada");
      } else {
        el.classList.remove("tada");
      }
    });
  });
};

// 동작시킬 elements리스트에 이벤트를 등록
const boxElList = document.querySelectorAll(".box");
addEventToEl(boxElList);

간단하게 설명하면 스크롤 이벤트가 발생했을 때, class=”box” 를 가진 Element가 viewport 내에 있는지 확인하고 있다면 tada 라는 class를 추가한다.

문제점

하지만 위 방식은 불필요한 리플로우와 스크롤 이벤트로 인해 비효율이 발생할 수 있다.

불필요한 리플로우

리플로우 는 문서내의 요소들에 대해서 위치와 좌표를 다시 계산하는 웹 브라우저의 프로세스이다. 이는 문서의 일부분 혹은 전부를 다시 렌더링 할 때 사용된다. - 참고

리플로우가 어떤 것인지 가물가물하다면 아래를 토글하자!

위 JS 코드에서 불필요한 리플로우를 일으키는 함수는 getBoundingClientRect() 이다.

이 함수가 하는 일은 단순히 요소의 크기와 뷰포트에 상대적인 위치에 대한 정보를 가져오기만 할 뿐인데 Reflow가 일어난다.. Performance 체크도 해보면서 진짜로 Reflow(Recalculate Style)가 일어나는 지 확인해봤다.

Performance

스크롤 이벤트

  • 이벤트는 단시간에 수백번, 수천번 호출될 수 있기 때문에 Main Thread를 고생시킨다
  • 한 페이지 내에 여러 scroll이벤트(무한 스크롤, 광고 배너, 애니메이션 등)가 등록되어 있을 경우, 각 엘리먼트마다 이벤트가 등록되어 있기 때문에 사용자가 스크롤할 때마다 이를 감지하는 이벤트가 끊임없이 호출된다. (Debounce, Throttle로 어느 정도 해결가능하긴 함) MDN - scroll Event

Intersection Observer란?

여태까지 특정 상황을 Scroll 이벤트로 어떻게 구현할 수 있으며 그렇게 했을 때 어떤 문제가 있을 수 있는 지 알아봤다. 그럼 드디어 Intersection Observer에 대해 알아보자.

배경 (참고)

  • 웹이 발전하면서 위 예시와 같은 요구사항 증가
  • getBoundingClientRect() 를 활용한 Scroll 이벤트는 비효율적

방식

  • intersection Observer API(WEB API)는 특정 요소가 보이는지에 대한 판단을 브라우저에게 넘긴다.
  • Target이 상위 Element 또는 View Port와 교차하는 지 비동기적으로 관찰

이벤트 방식과 Intersection Observer 비교

// Event handler
// Target Element에 특정한 이벤트가 발생 시, callback를 실행한다.
targetElement.addEventListener('event', callback);

// Intersection Observer
// Target Element가 관찰 될 때, callback를 실행한다.
const observer = new IntersectionObserver(callback); // Observer 생성
observer.observe(targetElement); // Target element 관찰 시작
  • 크롬 51버전부터 사용가능 (현재 103.x 버전)
  • can i use
  • Intersection Observer가 활용되는 Point
    • 스크롤 시 이미지 Lazy-loading
    • infinite scroll
    • view port에 광고가 보여질 때 어떠한 동작

Intersection Observer로 구현

JS

// IntersectionObserver 를 등록한다.
const io = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    // 관찰 대상이 viewport 안에 들어온 경우 'tada' 클래스를 추가
    if (entry.intersectionRatio > 0) {
      entry.target.classList.add('tada');
    }
    // 그 외의 경우 'tada' 클래스 제거
    else {
      entry.target.classList.remove('tada');
    }
  })
})

// 관찰할 대상을 선언하고, 해당 속성을 관찰시킨다.
const boxElList = document.querySelectorAll('.box');
boxElList.forEach((el) => {
  io.observe(el);
})

Performance

애니메이션이 실행되며 당연히 Reflow가 발생했지만, getBoundingClientRect() 처럼 비효율적으로 발생한 건 없다.

사용법

// observer 생성
// options를 설정하지 않으면 viewport가 지정된다.
const observer = new IntersectionObserver(callback[, options]);
// target element 관찰 시작
observer.observe(targetElement);

// target element 관찰 중지
observer.unobserve(targetElement);

// observer 해제
observer.disconnect();

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

const options = {
  root: null, // root element = observer가 될 element를 선택(null(default) => viewport)
  rootMargin: '0px', // root element의 범위를 확장하거나 축소
  threshold: 1.0, // target element가 몇 퍼센트 보일 때, callback을 실행 할지 설정 (0(0%) ~ 1.0(100%))
};

root

rootMargin

threshold

  • 옵저버가 실행되기 위해 타겟의 가시성이 얼마나 필요한지 백분율
  • 배열도 가능

callback

const callback = (entries, observer) => {
  // observer에 target element가 들어올 때, 나갈 때 총 두 번 callback이 실행된다.

  // entries = observer에 감지된 target elements
  // 하나의 observer가 여러개의 target element를 관찰하도록 설정할 수 있기 때문에,
  // entries는 array로 받아진다.

  if (!entries[0].isIntersecting) return; // isIntersecting = true(들어올 때), false(나갈 때)
  console.log(`${entries[0].target} is intersecting!`);
};

boundingClientRect

IntersectionObserverEntry: boundingClientRect property - Web APIs | MDN

The IntersectionObserverEntry interface's read-only boundingClientRect property returns a DOMRectReadOnly which in essence describes a rectangle describing the smallest rectangle that contains the entire target element.

https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/boundingClientRect
IntersectionObserverEntry: boundingClientRect property - Web APIs | MDN
function isElementInViewport(entry) {
  var rect = entry.boundingClientRect(); //Reflow X

  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <=
      (window.innerHeight || document.documentElement.clientHeight) &&
    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  );
}

추가로, IntersectionObserver에서 boundingClientRect라는 API를 사용할 수 있는데 이 함수는 Reflow를 일으키지 않는다고 한다~~