실무에서 컨텐츠의 가시성을 기반으로 작업을 해야 할 일이 많았다. 특정 요소가 화면에 보이는 순간 애니메이션을 동작시켜야 할 일도 있었고, 스크롤을 내릴 때 스크롤이 최하단으로 내려오면 데이터를 추가로 불러와야 하는 상황도 있었다. (무한스크롤)
추가적으로 스크롤 위치에 따라서 상단에 고정을 해야하는 컨텐츠가 생길 때도 있었는데 Intersection Observer API
가 지금 이 모든 상황들을 해결해줄 수 있을 지는 모르겠지만 컨텐츠의 가시성을 처리하기 위해 스크롤 이벤트에서 getBoundingClientRect
를 계속 호출하는 것은 웹 페이지 성능에 악영향을 줄 수 있는것은 분명하다. (테스트를 하다가 화면이 느려지는 경우도 많았다.)
콜백 큐에 콜백이 무한정 쌓이고 그걸 처리하는 쓰레드에도 큰 부하가 갔을 텐데 문제는 그 뿐만이 아니였다. getBoundingClientRect
은 reflow 를 발생시키는데 이 경우 지속적인 DOM 트리와 CSSOM 트리의 재계산으로 성능이 악화가 되어 부정적인 사용자 경험을 제공할 수 있다.
이를 개선하기 위한 방안으로 감사하게도 Intersection Observer API
가 2016년 4월 구글 개발자 페이지를 통해 소개되었다. Intersection Observer API
을 사용하면서 reflow
현상을 피할 수 있을 뿐만 아니라 구현하기 어려웠던 복잡한 과정들을 조금 더 편리하고 쉽게 구현할 수 있을거라는 생각이 든다.
reflow
를 발생시키지 않는다.let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
// options에 따라 인스턴스 생성
let observer = new IntersectionObserver(callback, options);
// 타겟 요소 관찰 시작
let target = document.querySelector('#listItem');
observer.observe(target);
new
키워드를 통해 인스턴스를 생성합니다. callback , options 2개의 파라미터를 받습니다. callback 은 가시성의 변화가 생겼을 때 호출되는 콜백 로직입니다. options 는 만들어질 인스턴스에서 콜백이 호출되는 상황을 정의합니다.
타겟 요소의 가시성을 확인할 때 사용되는 루트 요소입니다. 이것은 타겟 요소보다 상위 요소, 즉 요소의 조상 요소이어야 합니다. 설정하지 않거나 root 값을 null 로 주었을 때 기본 값으로 브라우저 뷰포트가 설정됩니다.
margin 을 주어 루트 요소의 범위를 확장할 수 있습니다. 즉 확장된 영역 안에 타겟 요소가 들어가면 가시성에 변화가 생깁니다. CSS 의 margin 과 유사하게 top, right, bottom, left 의 margin 정도롤 각각 설정할 수 있습니다. 기본 값은 0이며 따로 설정 시 단위를 꼭 입력해야합니다.
콜백이 실행될 타겟 요소의 가시성 퍼센티지를 나타내는 단일 숫자 및 숫자 배열이 들어갈 수 있습니다. 즉, 요소의 top, bottom 이 노출된 순간만 콜백을 실행할 수 있는 것이 아니라 어느정도 타겟 요소가 보여졌는 지에 따라서도 콜백을 호출할 수 있습니다. 예를 들어 요소가 50%만큼 보여졌을 때 탐지하고 싶다면 단일 숫자 값 0.5 를 설정하면 됩니다. 혹은 25% 단위로 가시성이 변경될 때마다 콜백이 실행되게 하고 싶다면 [0, 0.25, 0.5, 0.75, 1] 을 설정하면 됩니다.
// 타겟 요소가 50% 가시성이 확인되었을 때
let observer1 = new IntersectionObserver(callback, {
threshold: 0.5
});
// 타겟 요소가 25% 단위로 가시성이 확인되었을 때
let observer1 = new IntersectionObserver(callback, {
threshold: [0, 0.25, 0.5, 0.75, 1]
});
타겟 요소의 관찰이 시작되거나, 가시성에 변화가 감지되면(threshold 와 만나면) 등록된 callback 이 실행된다.
let callback = (entries, observer) => {
entries.forEach(entry => {
// 각 entry는 가시성 변화가 감지될 때마다 발생하고 그 context를 나타낸다.
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
이 콜백은 메인스레드에서 처리되고 파라미터로 entries 와 observer 를 받게 된다.
entries 는 IntersectionObserverEntry 인스턴스를 담은 배열이다. 일반적으로 callback 에 파라미터로 전달이 되고 후술할 Intersection Observer.takeRecords() 를 통해 반환받을 수도 있다.
IntersectionObserverEntry 는 루트요소와 타겟요소의 교차(threshold 와 만났을 때)의 상황을 묘사한다. 포함된 프로퍼티들은 모두 읽기전용(read only)이다.
타겟 요소의 사각형 정보(DOMRectReadOnly)를 반환한다. getBoundingClientRect() 호출과는 다르게 reflow 를 발생시키진 않는다.
타겟 요소의 가시성이 감지된 부분의 정보(DOMRectReadOnly)를 반환한다.
타겟 요소의 intersectionRect 이 boundingClientRect 와 어느정도로 교차(겹치는 지) 비율(0.0 ~ 1.0)을 반환한다. 바꿔 말하면 타겟 요소가 루트 요소와 얼마나 교차하는지의 정도와 같다.
타겟 요소와 루트 요소가 전혀 교차하지 않았음에도 타겟 요소의 관찰이 시작되면 콜백 또한 바로 호출된다. 이는 Intersection Observer 의 기본동작이며 이를 예외처리 하기 위해서 intersectionRatio 가 사용된다.
let callback = (entries, observer) => {
entries.forEach(entry => {
// 타겟 요소가 루트 요소와 교차하는 점이 없으면 콜백을 호출했으되, 조기에 탈출한다.
if (entry.intersectionRatio <= 0) return
// 혹은 isIntersecting을 사용할 수 있다.
if (!entry.isIntersecting) return
// ... 콜백 로직
});
};
해당 entry 에 타겟 요소가 루트 요소와 교차하는 지 여부를 Boolean 값으로 반환한다.
루트 요소의 사각형 정보(DOMRectReadOnly)를 반환한다. 이 정보는 rootMargin 옵션 설정에 영향을 받는다.
타겟 요소를 반환한다.
문서(Document)가 만들어진 표준 시간(time origin)을 기준으로 타겟 요소와 루트 요소의 교차가 발생한 시간(DOMHighResTimeStamp)을 반환한다.
타겟 요소에 대한 관찰을 시작합니다.
타겟 요소에 대한 관찰을 중지합니다. 관찰의 목적이 이루어져 굳이 계속 관찰을 할 필요가 없는 경우 사용합니다.
인스턴스의 타겟 요소들에 대한 모든 관찰을 중지합니다.
IntersectionObserverEntry 인스턴스들의 배열을 리턴합니다.
Intersection Observer API
가 웹 성능을 개선해주고 보다 간단하게 컨텐츠의 가시성을 기반으로 작업을 하게 도와주는것은 분명한 사실이지만 기존getBoundingClientRect
로 할 수 있는 모든 것들을 다 실현가능한가에 대해서는 미지수이다. 따라서 Intersection Observer API
과 getBoundingClientRect
을 모두 사용하는 방법을 익히고 적절한 상황에 사용할 필요가 있다고 생각한다.