해당 블로그로 이전했습니다.
페이지의 특정 요소가 화면에 보여지고 있는지 확인하기 위해서는 브라우저의 스크롤에 이벤트를 등록하고, 스크롤할 때마다 요소의 위치를 확인하고 해당 위치에 도달하였는지 계산하는 콜백 함수를 등록해야 합니다.
document.addEventListener("scroll", function(){})
스크롤 이벤트를 사용하여 요소의 위치를 관찰하는 방법은 성능상 문제를 발생시킬 수 있습니다. 스크롤 이벤트는 사용자가 스크롤을 할 때마다 호출되며 이벤트에 등록된 콜백 함수도 함께 실행됩니다. 간단한 동작이라면 상관없을 수도 있지만 여러 개의 이벤트가 함께 등록되고 복잡한 연산을 수행하게 된다면 브라우저의 메인 스레드에도 영향을 끼쳐 페이지가 버벅이는 등의 문제가 발생할 수 있습니다.
아래 예시를 통해 스크롤 이벤트의 동작을 살펴보겠습니다.
<div id="container">
<div class="scroll-view"></div>
</div>
const view = document.getElementById("container");
const addElement = () => {
const newView = document.createElement("div");
const colorCode = `#${Math.round(Math.random() * 0xffffff).toString(16)}`;
newView.setAttribute("class", "scroll-view");
newView.style.backgroundColor = colorCode;
view.appendChild(newView);
};
document.addEventListener("scroll", (e) => {
const { scrollTop, clientHeight, scrollHeight } = e.target.scrollingElement;
/*
scrollTop : 요소 최상단에서 얼마나 스크롤 했는지의 값
clientHeight : 브라우저에서 보이는 컨텐츠의 높이
scrollHeight : 스크롤 가능한 범위의 전체 높이
*/
if (scrollTop + clientHeight >= scrollHeight) {
addElement();
}
});
다음은 페이지 로드 시에는 첫번째 스크롤 영역만 보여지고, 스크롤이 끝까지 내려왔을 때 새로운 스크롤 영역을 추가하는 코드입니다. 스크롤 이벤트가 발생하면 현재 높이값을 계산하여 스크롤이 끝나는 위치인지를 확인하고 새로운 요소를 추가시키는 함수를 호출하여 새로운 스크롤 영역을 추가하고 있습니다. 이 때, 유저가 페이지를 스크롤 할때마다 현재 위치값을 계산하는 코드가 실행되어 함수의 중복 호출 문제가 발생합니다.
크롬 개발자 도구의 Performance 탭을 확인해보면 이벤트 발생 시마다 콜백 함수가 호출되어 메인 스레드의 리소스가 사용되는 것을 확인할 수 있습니다.
또한 요소의 위치를 확인하는 과정에서 scrollingElement 속성이 매번 Recalculate Style, Layout(Reflow)와 Paint(Repaint)을 유발한다는 단점도 있습니다.
스크롤 이벤트는 디바운싱과 쓰로틀링 기법을 함께 사용하여 성능을 개선시키는 방법을 고려해볼 수 있습니다.
Intersection Observer API는 타겟 요소와 상위 요소(viewport)의 교차된 영역을 관찰하고 현재 타겟 요소가 상위 요소 내부에 포함되었을 때 타겟 요소의 여러 가지 정보를 제공하는 역할을 합니다.
Intersection Observer API는 스크롤 이벤트와는 다르게 비동기적으로 작동합니다. 그렇기 때문에 이벤트의 연속 호출 없이 등록한 요소가 관찰되는 시점에만 콜백 함수를 실행시킬 수 있습니다.
아래에서 Intersection Observer API의 사용 방법과 사용 시의 성능상 이점을 확인해보도록 하겠습니다.
관찰할 요소 타겟팅
const boxElem = document.querySelectorAll(".box");
const observer = new IntersectionObserver(callback, options);
observer.observe(boxElem);
new 연산자를 사용하여 IntersectionObserver 인스턴스를 생성한 후 관찰할 타겟을 지정합니다.
intersection observer 설정
const boxElem = document.querySelectorAll(".box");
const observer = new IntersectionObserver((entries, observer) => {
// console.log(entries);
// console.log(observer);
}, options);
observer.observe(boxElem);
observer가 타겟팅한 대상에 대한 변경사항을 감지하면 콜백 함수를 실행합니다. 콜백 함수는 다음 2개의 매개변수(entries, observer)를 갖습니다.
entries: IntersectionObserverEntry 객체를 갖는 배열. IntersectionObserverEntry 객체는 다음의 속성을 포함합니다.
Options
let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
observer 생성 시 두번째 인자로 options를 설정할 수 있습니다. options 객체는 observer 콜백이 호출되는 상황을 조작할 수 있습니다.
root: 대상 객체의 가시성을 확인할 때 사용되는 뷰포트 요소. 기본값은 브라우저 뷰포트이며, root값이 null 이거나 지정되지 않을 때 기본값으로 설정됩니다.
rootMargin: root가 가진 여백. root 요소의 사각형을 수축시키거나 증가시킬 수 있습니다.
threshold: observer의 콜백이 실행되기 위한 타겟의 가시성 정도. 기본값은 0이며 1.0은 타겟 요소의 모든 픽셀이 화면에 노출되기 전에는 콜백을 실행시키지 않음을 의미한다.
const view = document.getElementById("container");
const addElement = () => {
const newView = document.createElement("div");
const colorCode = `#${Math.round(Math.random() * 0xffffff).toString(16)}`;
newView.setAttribute("class", "scroll-view");
newView.style.backgroundColor = colorCode;
view.appendChild(newView);
};
const io = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
} else {
addElement();
io.unobserve(entry.target);
io.observe(document.querySelector(".scroll-view:last-child"));
}
});
});
io.observe(view);
앞서 확인해보았던 것처럼 IntersectionObserver의 IntersectionObserverEntry 객체는 관찰 대상으로 등록한 요소에 대한 위치, 크기 정보를 포함하고 있습니다. 따라서 reflow를 발생시키는 별도의 함수 호출 없이도 요소가 타겟 영역 안에 있는지 확인할 수 있습니다.
위의 코드에서는 스크롤할 때마다 함수를 발생시키는 것이 아니라 타겟 요소(마지막 스크롤 영역)와 root(viewport)가 교차하는 시점, 즉 마지막 스크롤 영역이 뷰포트 내에 관찰되었을 때 콜백 함수를 실행합니다.
Performance 탭을 통해 메인 스레드 영역의 변화를 확인해봅시다.
Intersection Observer API를 적용하면 요소가 관찰되는 경우에만 콜백 함수가 호출되는 것을 확인할 수 있습니다. 비동기적으로 실행되므로 메인 스레드에 부담을 주지 않습니다.
또한 함수 호출 이후의 불필요한 reflow, repaint가 발생하지 않는 것도 확인할 수 있었습니다.
Intersection Observer API는 비동기적으로 실행되어 기존 스크롤 이벤트 사용 시의 이벤트 연속 호출, 렌더링 성능 저하 문제를 개선할 수 있었습니다. Intersection Observer는 기본적으로 타겟 요소가 뷰포트 내부에 있는지를 관찰하는 역할을 합니다. getBoundingClientRect 메서드를 대체하고 싶거나 Infinite scroll, Lazy loading을 구현하는 경우에 Intersection Observer API를 활용해보는 것이 좋은 선택지가 될 수 있을 것 같습니다.
참고
https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API
https://heropy.blog/2019/10/27/intersection-observer