
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.

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를 일으키지 않는다고 한다~~