어떤 Element가 화면(viewport)에 노출되었는지를 감지할 수 있는 API이며 이런 유용한 점을 이용해서 우리들은 무한 스크롤(Infinite Scroll)을 만들어볼 수 있습니다.
무한 스크롤을 구현할 때는 Scroll Event를 감지해서 유저가 화면 제일 끝에 도달했을 때 아이템을 더 불러오게끔 만들수도 있는데 굳이 Intersection Observer API를 사용하여 무한 스크롤을 구현하는 이유는 뭘까요?
간단한 Intersection Observer 생성 예제
let observer = new IntersectionObserver(callback, options);
Intersection Observer를 생성할 때는 옵션을 설정할 수 있습니다.
옵션에는 root, rootMargin, threshold가 있는데요,
예전에는 스크롤 이벤트를 구현하기 위해 scroll
, Element.getBoundingClientRect()
등의 메서드를 사용해 하나하나 계산해서 값을 얻어야 했는데 scroll 이벤트의 경우 동기적으로 실행
되기 때문에 이벤트 연속 호출
, 렌더링 성능 저하
등의 문제가 있었다.
그러던 와중에 2016년 구글 개발자 페이지를 통해 IntersectionObserver
가 소개됐다.
IntersectionObserver는 비동기적으로 실행되며 관찰 대상과 뷰포트의 교차점을 관찰하고, 뷰포트 안으로 들어오는 시점에 정보를 제공한다.
$ npm i intersection-observer
인스턴스 생성
const io = new IntersectionObserver(콜백함수, 옵션);
콜백함수 설정
const io = new IntersectionObserver((entries, observer) => {}, 옵션)
관찰 대상의 여러 정보들이 배열로 담겨져 있다.
boundingClientRect
관찰 대상의 사각형 정보(DOMRectReadOnly)
intersectionRect
관찰 대상의 교차한 영역 정보(DOMRectReadOnly)
intersectionRatio
관찰 대상의 교차한 영역 백분율(intersectionRect 영역에서 boundingClientRect 영역까지 비율, Number)
isIntersecting
관찰 대상의 교차 상태(Boolean)
rootBounds
지정한 루트 요소의 사각형 정보(DOMRectReadOnly)
target
관찰 대상 요소(Element)
time
변경이 발생한 시간 정보(DOMHighResTimeStamp)
대상을 지정하면 그 대상을 관찰하는 역할을 한다.
let 옵션 = {
root: document.querySelector('#scrollArea'),
rootMargin: '10px 0 -20px 0', // root가 위로 10px 커지고 아래로 20px 작아진다.
threshold: 0.5 // 대상이 50% 보였을 때 실행
}
let io = new IntersectionObserver(콜백함수, 옵션);
root
뷰포트 대신 사용할 요소를 지정한다. 지정하지 않을 경우 브라우저 뷰포트로 설정된다.
rootMargin
마진을 설정하여 Root에서 지정된 뷰포트의 영역을 확장/축소할 수 있다.
threshold
뷰포트에 관찰 대상이 얼마나 보여야 observer를 실행할 지 설정할 수 있다. 숫자나 숫자 배열로 값을 설정할 수 있다.
25% 단위로 타겟 요소의 보일때마다 콜백함수를 호출하고 싶다면 [0, 0.25,0.5, 0.75, 1]로 설정하면 된다.
const io = new IntersectionObserver((entries, observer) => {});
io.observe(관찰대상);
io.disconnect();
리액트에서 IntersectionObserver 사용 시, 관찰할 DOM 요소는 Ref를 사용해 참조를 만든다.
const observer = () => {
const itemRef = useRef<HTMLDivElement[]>([]);
const io = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
// 관찰 대상이 뷰포트에 들어왔을 경우
if (entry.intersectionRatio > 0) {
entry.target.classList.add("active");
}
// 관찰 대상이 뷰포트에 없을 경우
else {
entry.target.classList.remove("active");
}
});
});
// 관찰 대상 관찰 시작
for (let i = 0; i < itemRef.current.length; i++) {
io.observe(itemRef.current[i]);
}
}