Intersection Observer API
작성일:2023.12.10|조회수:0

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는 하나지만 관찰 대상은 여러 개라는 점이며, 이를 통해 다수의 요소를 효율적으로 감시할 수 있다.
더 읽어보기
2026.04.11
Trie 자료구조
문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다. Trie는 무…
2026.03.19
Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까
느린 네트워크에서 큰 이미지를 열면 가끔 화면이 위에서 아래로 채워진다. 마치 누군가 아주 성실하게 롤러로 이미지를 칠하는 것처럼 보인다. 물론 브라우저 안에 그런 직원은 없다. 있다면 우리보다 야근을 더 많이 하고 있을 것이다. 이 현상은 단순한 시각 효과가 아니라 네트워크 전송, 브…
2026.03.19
Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
다운로드 화면에서 진행 막대가 조금씩 차오르면 이상하게 안심된다. 반대로 스피너만 계속 돌면 파일이 오는 중인지, 서버가 고민 중인지, 내 인생이 잠깐 멈춘 건지 알 수 없다. 사용자는 둘 다 “다운로드 중”이라고 느끼지만, 내부적으로는 꽤 다른 상황일 수 있다. 진행률 계산 자체는 복…
2026.03.13
Streams API 4. 왜 모든 언어에는 Stream API가 존재할까
Streams API를 공부하다 보면 묘한 기시감이 온다. JavaScript에서 ReadableStream, WritableStream, TransformStream을 보고 있는데, Java의 InputStream, Go의 io.Reader, Rust의 Read와 Write가 멀리서…
2026.05.26
차트는 멈췄는데 윈도우가 움직인다
상황 어느날 서비스를 살펴보시던 팀장님께서 이런 말씀을 slack에 남기셨다. 진호님, 예측 차트에서 zoom을 계속하면 어느 순간 라인 차트가 아니라 단일 스캐터 차트처럼 보이는 데, 이거 수정하면 좋을 거 같아요. 어느정도 zoom을 하면 그 이후로는 zoom이 안 되도록 할 수 없…
2026.05.21
피자가게로 이해하는 디자인 패턴
에이든 피자는 처음부터 복잡한 시스템을 만들 생각이 없었다. 처음에는 메뉴 몇 개만 만들면 됐다. 그런데 손님은 커스텀 주문을 넣기 시작했고, 주방은 상태를 나눠야 했고, 결제와 배달앱과 알림이 하나씩 붙었다. 코드도 가게를 닮는다. 장사가 잘될수록 이상하게 더 쉽게 망가진다. 디자인…
2026.05.21
7. Decorator — 토핑 추가할 때마다 클래스를 새로 만들 수 없다
에이든 피자에서 주문서를 객체로 만들자 취소와 재주문은 한결 편해졌다. 그런데 주문이 편해지자 손님들도 한결 편해졌다. 편해진 손님은 더 많은 요구를 한다. "치즈 추가요", "올리브도 추가요", "소스 많이요", "조금 더 바삭하게 구워주세요" 같은 요청이 주문대 위로 쌓이기 시작했다…
2026.05.21
6. Command — 주문서를 객체로 만들면 취소도 재주문도 쉬워진다
에이든 피자의 시범 운영이 어느덧 일주일을 향해 가고 있다. 주방은 이제 제법 능숙하게 피자를 구워내고, 포스기(POS) 시스템도 팩토리 메서드 덕분에 다양한 지점 메뉴를 무리 없이 받아낸다. 하지만 시스템이 안정될수록 요구사항은 더 정교해지기 마련이다. 오늘은 주문 그 자체가 문제가…
댓글
댓글을 불러오는 중...