본문 바로가기

웹 개발

Intersection Observer API

Intersection Observer API

  • 타겟 요소와 상위 요소 또는 최상위 document의 viewport 사이의 intersection내의 변화를 비동기적으로 관찰하는 방법.
  • Lazy Loading을 구현할 때, Infinite Scroll을 구현할 때, 광고의 가시성을 확인할 때 주로 사용.
  • 사용자가 보고 있을 때 결과를 표시하거나 애니메이션을 시작하기 위해 사용.
  • 기존에는 Element.getBoundingClientRect()를 사용하거나 해당 요소의 top, bottom, left right와 viewport의 top, bottom, left right를 비교해 해당 요소의 가시성을 판단했다.
  • 기존의 방식은 scroll 또는 resize에 이벤트를 등록해야 하기 때문에 이벤트 호출이 자주 일어나 성능 문제를 일으킬 수 있었다.
  • IntersectionObserver 객체를 생성하고 감시하고자 하는 요소들을 메서드를 사용해 등록해 놓으면 따로 이벤트를 추가할 필요 없이 가시성을 확인할 수 있다.

IntersectionObserver 객체

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

let observer = new IntersectionObserver(callback, options);
  • callback에게 현재 등록 되어있는 entry배열을 인자로 전달한다.
  • 요소의 Node 객체는 entry.target에 저장되어 있다.
  • entry.isIntersecting 값을 사용해 가시성 여부를 확인할 수 있다.
  • callback은 메인 스레드에서 실행되기 때문에 성능을 향상시키기 위해서 연산을 최소화하거나 Window.requestIdleCallback()을 사용하는 것이 좋다.
  • 대상 요소가 직사각형 모양이 아닌경우 대상 요소를 감싸는 가장 작은 직사각형을 bounding box로 정한다.

 

Method

observe(element), unobserve(element)

  • IntersectionObserver 객체가 감시할 요소들을 등록하고 해제한다.
  • LazyLoading을 구현할 때는 이미지를 불러온 후부터는 감시할 필요가 없기 때문에element.onload에서 unobserve하는 방법을 사용한다.

disconnect()

  • IntersectionObserver 객체에 등록된 모든 요소의 감시를 해제한다.

 

Options

root

  • 가시성을 확인 할 viewport를 지정하기 위한 option.
  • 가시성을 확인 할 대상 객체의 조상 요소여야 한다.
  • null 또는 지정되지 않을 경우에는 기본값인 브라우저 viewport를 사용한다.

rootMargin

  • Intersecting 여부를 계산하는 root의 bounding box의 크기를 rootMargin만큼 증가 시킨다.
  • 단위는 px와 %를 사용할 수 있고 기본값은 "0px 0px 0px 0px"이다. 축약형으로도 선언할 수 있다.

threshold

  • callback이 실행되는 intersecting ratio를 지정하는 단일 숫자 또는 숫자 배열.
  • 예를 들어[0, 0.25, 0.5, 0.75, 1]과 같이 설정하면 25% 단위로 callback이 실행된다.
  • 기본값은 0으로 1px이라도 bounding box에 들어오면 callback이 실행된다.
  • threshold 중 하나만 만족하더라도 isIntersectingtrue값을 가진다.

IntersectionObserver Entry 객체

  • IntersectingObserver 객체는 감시하는 요소의 가시성에 변화가 발생했을 때, 변화가 생긴 Entry 객체 배열을 매개변수로 callback을 실행한다.

 

boundingClientRect

  • 해당 Entry 객체의 intersction을 계산하는 bounding box 정보를 담고 있는 객체.
  • { bottom, height, left, right, top, width, x, y }

intersectionRatio

  • Entry의 bounding box 중 어느 정도가 root의 bounding box와 교차하고 있는지 0 ~ 1로 나타낸 비율.

intersectionRect

  • Entry의 bounding box와 root의 bounding box가 교차하고 있는 사각형 정보를 담고 있는 객체.
  • { bottom, height, left, right, top, width, x, y }

isIntersecting

  • Entry의 bounding box과 root의 bounding box와 교차하고 있는지 boolean 값으로 표시.
  • intersectionRatio가 0보다 크면 true.

rootBounds

  • root bounding box의 정보를 담고있는 객체.
  • { bottom, height, left, right, top, width, x, y }

target

  • 해당 요소의 Node 객체.
  • target을 사용해 callback 내에서 해당 요소를 조작할 수 있다.

time

  • 가시성에 변화가 생긴 마지막 시점을 저장한다.
  • perfomance.now()와 비교해 마지막 변화 이후 얼마나 시간이 지났는지 ms단위로 알 수 있다.

예시 코드

Lazy Loading

var observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
        if(entry.isIntersecting) {
            entry.target.src = entry.target.dataset.src; // <img src="" data-src = "이미지 경로"> 
            entry.target.onload = () => { observer.unobserve(entry.target) }; // 이미지 다운로드가 완료되면 감시를 해제.
        }
    })
});

document.querySelectorAll('img').forEach((img) => observer.observe(img));

Infinite Scroll

var lastEl = document.querySelector('tbody tr:last-child'); //목록을 table로 구현했다고 가정.

var observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
        if(entry.isIntersecting) {
            getNextElements(); // 마지막 요소가 화면에 들어오면 table에 다음 요소들을 추가하는 함수를 실행.
            observeLastElement(entry.target); // 추가된 요소의 마지막 요소를 감시하기 시작.
        }
    })
});

function observeLastElement(prevLastEl) {
    var lastEl = document.querySelector('tbdoy tr:last-child');
    observer.undobserve(prevLastEl); // 이전의 마지막 요소를 감시 해제.
    observer.observe(lastEl);
};

observer.observe(lastEl);
  • getNextElements 함수 내에 비동기 처리가 있으면 observeNewLastElement를 callback으로 넘겨주면 된다.
  • 이 코드를 그대로 사용하면, 요소들이 추가된 후에도 마지막 요소가 화면 안에 있는 경우에 스크롤을 내려도 마지막 요소의 가시성 변화가 없기 때문에 스크롤을 올렸다 다시 내려야 하는 상황이 발생한다.
  • 이 상황을 확인하는 함수를 추가하거나, 추가하는 요소의 개수를 늘려 마지막 요소가 화면 밖에서 추가되도록 해야 한다.

궁금한 점

  • 여러 개의 IntersectionObserver 객체를 사용해도 성능 문제가 발생하지 않는지 궁금하다.
  • takeRecords()
    1. Let queue be a copy of this’s internal [[QueuedEntries]] slot.
    2. Clear this’s internal [[QueuedEntries]] slot.
    3. Return queue.
    • IntersectionObserver 객체 Method 중 하나인 takeRecords의 명세에서 QueuedEntries가 어떤 역할을 하는지 모르겠다.
    • 개발자 도구에서 takeRecords를 사용하면 빈 배열만 반환한다.

참고자료