👁️

Intersection Observer — 스크롤 이벤트 없이 요소 가시성 감지

lazy loading, 무한 스크롤, 애니메이션 트리거의 성능 좋은 구현 방법

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('visible');  // 뷰포트에 들어옴
      observer.unobserve(entry.target);       // 한 번만 실행
    }
  });
}, { threshold: 0.1 });  // 10% 보이면 트리거

document.querySelectorAll('.lazy').forEach(el => observer.observe(el));

왜 scroll 이벤트보다 좋은가

scroll 이벤트는 초당 수십~수백 번 발생한다. 매번 getBoundingClientRect()를 호출하면 reflow가 발생해서 성능이 떨어진다.

Intersection Observer는:

  • 브라우저가 최적화된 타이밍에 실행 (메인 스레드 idle 시)

  • reflow 없이 가시성을 판단

  • requestAnimationFrame보다도 효율적

옵션

{
  root: null,           // null = 뷰포트, DOM 요소 = 해당 요소 기준
  rootMargin: '100px',  // 뷰포트를 100px 확장 (미리 로드)
  threshold: [0, 0.5, 1] // 0%, 50%, 100% 보일 때 각각 콜백
}

rootMargin: '100px'이면 뷰포트 아래 100px까지 미리 감지한다. 이미지 lazy loading에서 "스크롤하기 전에 미리 로드 시작"을 구현할 때 쓴다.

실전 패턴

이미지 lazy loading: <img loading="lazy">가 네이티브로 있지만, 세밀한 제어가 필요하면 IO를 쓴다.

무한 스크롤: 마지막 아이템을 observe하고, 보이면 다음 페이지를 로드.

스크롤 애니메이션: 섹션이 보이기 시작하면 fade-in 클래스 추가.

핵심 포인트

1

new IntersectionObserver(callback, options)로 옵저버 생성

2

observer.observe(element)로 감시 대상 등록

3

entry.isIntersecting으로 뷰포트 진입 여부 확인

4

rootMargin으로 감지 영역 확장 — 미리 로드/애니메이션 시작

사용 사례

이미지 lazy loading — 뷰포트에 가까워지면 src를 설정 무한 스크롤 — 마지막 아이템이 보이면 다음 페이지 fetch