PUBLISHED
Intersection Observer API
작성일: 2023.12.10

Intersection Observer API는 웹 페이지의 특정 요소가 뷰포트(Viewport) 또는 지정한 컨테이너 요소와 교차(intersect) 하는지를 비동기적으로 감지할 수 있는 방법을 제공한다. 이 API는 스크롤 이벤트를 직접 처리하지 않고도 요소의 가시성 변화를 감지할 수 있도록 설계되었다.
이러한 특성 덕분에 Intersection Observer API는 무한 스크롤, 이미지 지연 로딩(Lazy Loading), 섹션 단위 애니메이션 트리거, 광고 노출 감지와 같은 기능을 구현하는 데 특히 유용하다. 기존의 scroll 이벤트 기반 방식과 비교했을 때, 성능 부담이 적고 코드의 의도가 명확해진다는 장점이 있다.
IntersectionObserver 객체
Intersection Observer API의 사용 흐름은 크게 세 단계로 나뉜다.
첫 번째 단계는 IntersectionObserver 인스턴스를 생성하는 것이다. 이때 교차가 발생했을 때 실행할 콜백 함수와, 교차를 감지하는 기준을 정의하는 옵션 객체를 함께 전달한다.
두 번째 단계는 콜백과 옵션을 통해 관찰 방식과 동작을 정의하는 것이다. 어떤 요소가 무엇과 교차해야 하는지, 얼마나 교차해야 하는지를 이 단계에서 결정한다.
세 번째 단계는 observe 메서드를 사용해 실제 관찰할 노드를 등록하는 것이다.
바닐라 JavaScript에서 일반적으로 사용되는 기본적인 코드는 다음과 같은 형태를 가진다. IntersectionObserver 생성자는 두 개의 인자를 받는다. 첫 번째 인자는 콜백 함수이고, 두 번째 인자는 옵션 객체이다. 콜백 함수는 “교차가 발생했을 때 무엇을 할 것인가”를 정의하고, 옵션 객체는 “어떤 조건에서 교차로 판단할 것인가”를 정의한다.
const target = document.querySelector('#target');
const observer = new IntersectionObserver(callback, options);
observer.observe(target);옵션 객체(options)
옵션 객체는 교차(intersection)를 감지하는 방식을 정의하는 역할을 한다. 옵션 객체는 총 세 가지 속성을 가진다.
const observer = new IntersectionObserver(callback, {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0,
});root
root는 대상 요소의 가시성을 판단하기 위한 기준 요소이다. 반드시 관찰 대상 요소의 상위 요소여야 하며, 값을 지정하지 않거나 null로 설정하면 브라우저의 뷰포트가 기본값으로 사용된다.
일반적인 페이지 스크롤을 기준으로 교차를 감지하는 경우에는 root를 생략하는 것이 일반적이다. 반면, 특정 스크롤 컨테이너 내부에서만 교차 여부를 판단해야 한다면 root를 명시적으로 지정해야 한다.
rootMargin
rootMargin은 교차를 감지하는 영역을 root의 경계를 기준으로 확장하거나 축소하기 위한 마진 값이다. CSS의 margin 속성과 동일한 문법을 사용한다.
예를 들어, "0px 0px -100px 0px"과 같이 설정하면 실제로 요소가 화면에 완전히 들어오기 이전에 교차가 발생한 것으로 판단할 수 있다. 이 옵션은 이미지 지연 로딩이나 사전 데이터 로딩과 같은 시나리오에서 자주 활용된다.
threshold
threshold는 대상 요소가 얼마나 교차되었을 때 콜백을 실행할지를 결정하는 값이다. 0부터 1 사이의 숫자이거나 숫자 배열을 사용할 수 있다.
0은 대상 요소가 단 1픽셀이라도 교차되면 콜백을 실행한다는 의미이다.0.5는 대상 요소의 50%가 보일 때 콜백을 실행한다는 의미이다.[0, 0.25, 0.5, 0.75, 1]과 같이 배열을 사용하면, 해당 비율을 넘길 때마다 콜백이 실행된다.
옵션 객체는 결국 교차를 감지하는 기준을 얼마나 세밀하게 제어할 것인지를 정의하는 도구라고 볼 수 있다.
콜백 함수(callback)
IntersectionObserver의 콜백 함수는 두 개의 인자를 받는다. 첫 번째 인자인 entries는 현재 관찰 중인 모든 요소의 교차 정보를 담고 있는 배열이다. 배열의 각 요소는 IntersectionObserverEntry 객체이다.
두 번째 인자인 observer는 콜백을 실행한 IntersectionObserver 인스턴스 자체이다. 이를 통해 unobserve나 disconnect와 같은 메서드를 호출할 수 있다.
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log('Element has intersected');
}
});
}, {
root: null,
rootMargin: '0px',
threshold: 0.1,
});IntersectionObserverEntry 객체에서 가장 자주 사용되는 속성은 다음과 같다.
isIntersecting: 대상 요소가 교차 영역에 들어와 있는지 여부intersectionRatio: 대상 요소가 얼마나 교차되었는지를 나타내는 비율 값 (0 ~ 1)target: 교차가 발생한 실제 DOM 요소
Intersection Observer의 콜백은 요소가 교차 영역에 들어올 때와 나갈 때 모두 호출된다. 따라서 화면에 들어올 때만 특정 작업을 수행하고 싶다면 isIntersecting 값을 반드시 확인해야 한다.
여러 요소 관찰하기
Intersection Observer API에서 observe 메서드는 하나의 요소만 관찰하기 위한 용도가 아니다. 하나의 IntersectionObserver 인스턴스로 여러 요소를 동시에 관찰할 수 있다. 이 경우 모든 관찰 대상은 콜백 함수의 entries 배열에 담겨 전달된다. 이 구조는 리스트 형태의 UI나 반복되는 섹션을 처리할 때 매우 강력하다.
targets.forEach((target) => observer.observe(target));React에서 Intersection Observer 사용
Intersection Observer는 하나의 observer로 여러 요소를 동시에 관찰할 수 있다. 이 특성은 리스트 렌더링이 빈번한 React 환경에서 특히 유용하다. 문제는 React의 ref가 기본적으로 단일 DOM 노드를 가리키도록 설계되어 있다는 점이다.
이 때문에 여러 DOM 요소를 관찰해야 하는 경우, ref를 배열처럼 다루고 싶어지는 상황이 발생한다. 하지만 useRef<HTMLElement[]>([])에 직접 DOM을 밀어 넣는 방식은 React의 렌더링 흐름과 잘 맞지 않고, 마운트·언마운트 시점을 정확히 제어하기 어렵다.
이럴 때 가장 자연스러운 해결책은 콜백 ref(callback ref) 를 사용하는 것이다. 아래 훅은 하나의 ref 함수로 여러 DOM 요소를 등록하고, 하나의 IntersectionObserver 인스턴스로 이를 모두 관찰한다.
import { useCallback, useEffect, useRef } from "react";
interface UseIntersectionObserverListOptions
extends IntersectionObserverInit {}
export function useIntersectionObserverList(
onIntersect: (entry: IntersectionObserverEntry) => void,
options?: UseIntersectionObserverListOptions
) {
const elementsRef = useRef<HTMLElement[]>([]);
const ref = useCallback((node: HTMLElement | null) => {
if (!node) return;
if (!elementsRef.current.includes(node)) {
elementsRef.current.push(node);
}
}, []);
useEffect(() => {
if (elementsRef.current.length === 0) return;
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
onIntersect(entry);
}
});
}, options);
elementsRef.current.forEach((el) => observer.observe(el));
return () => {
observer.disconnect();
};
}, [onIntersect, options]);
return ref;
}이 훅에서 반환되는 ref는 일반적인 객체 ref가 아니라 콜백 ref이다. React는 DOM 요소가 마운트되거나 언마운트될 때 이 함수를 호출하며, 훅은 이 시점을 이용해 전달된 DOM 노드를 내부 배열에 수집한다. 이 방식은 여러 DOM 요소를 하나의 ref로 처리할 수 있게 해주며, 리스트 렌더링처럼 동적으로 생성되는 요소들과도 자연스럽게 결합된다.
이후 useEffect에서는 하나의 IntersectionObserver 인스턴스를 생성하고, 앞에서 수집한 모든 DOM 요소를 observe 메서드를 통해 등록한다. 교차가 발생하면 콜백으로 전달된 entries 배열을 순회하면서 교차된 요소에 대해 onIntersect 콜백을 실행한다. 이 구조의 핵심은 observer는 하나지만 관찰 대상은 여러 개라는 점이며, 이를 통해 다수의 요소를 효율적으로 감시할 수 있다.