웹 페이지에서 스크롤에 맞춰 이미지를 늦게 불러오는 지연 로딩(Lazy Loading)이나, 페이지 하단에 도달했을 때 다음 데이터를 자동으로 불러오는 무한 스크롤을 구현해본 적이 있을 것이다.
다만 기존의 window.addEventListener('scroll', ...) 방식이 스크롤할 때마다 수십, 수백 번씩 호출되어 브라우저의 렌더링 성능을 심각하게 떨어뜨린다는 사실을 모르는 채 그대로 사용하는 경우가 많다.
이번에는 Intersection Observer API가 정확히 무엇이고, 왜 스크롤 이벤트 대체제로 필수적인지, 그리고 실무에서 바로 쓸 수 있는 실전 예제로 어떻게 적용하는지 완벽하게 정리해서 소개하겠다.

 

1. Intersection Observer API란 무엇인가?

Intersection Observer API는 타겟 요소(Element)가 관찰 대상 영역(Viewport 또는 특정 부모 요소)과 교차(Intersection)하는지 여부를 비동기적으로 감지하는 자바스크립트 내장 API다.
과거에는 특정 요소가 화면에 보이는지 확인하기 위해 스크롤 이벤트 내에서 getBoundingClientRect() 메서드를 호출하곤 했다. 하지만 이 메서드는 리플로우(Reflow)를 유발하여 브라우저의 메인 스레드를 점유하는 치명적인 성능 저하 문제를 일으킨다.
Intersection Observer는 브라우저가 최적화된 방식으로 요소의 가시성을 계산하므로, 메인 스레드를 낭비하지 않고 훨씬 부드러운 성능을 제공한다.

 

비교 항목기존 scroll 이벤트 방식Intersection Observer API
동작 방식동기적 (스크롤 시 연쇄 실행)비동기적 (브라우저 최적화)
성능 영향리플로우 유발, 메인 스레드 부하 위험메인 스레드 비점유, 부드러운 프레임 유지
코드 복잡도좌표 계산 공식 및 쓰로틀링(Throttle) 필요선언적 API 제공으로 간결함
지원 브라우저모든 브라우저모든 현대 브라우저 (IE 제외)

 

2. 기본 사용법과 옵션 이해하기

Intersection Observer는 인스턴스를 생성할 때 콜백 함수와 옵션 객체를 전달받는다.
기본적인 구조와 설정값의 역할을 파악해두면 실무 상황에 맞춰 유연하게 응용할 수 있다.

 

핵심 옵션 3가지

1) root: 타겟 요소의 가시성을 검사할 기준이 되는 부모 요소다. 기본값은 null이며 브라우저의 뷰포트(Viewport)가 된다.
2) rootMargin: root의 범위를 확장하거나 축소할 수 있는 여백 값이다. CSS margin 속성과 유사하게 '0px 0px 200px 0px' 형태로 지정해 화면에 들어오기 200px 전에 미리 로딩을 시작하도록 만들 수 있다.
3) threshold: 타겟 요소가 root에 얼마나 노출되었을 때 콜백을 실행할지 결정하는 비율이다. 0.0은 1px이라도 보일 때, 1.0은 요소 전체가 보일 때 실행된다.

 

3. 실전 예제: 이미지 지연 로딩(Lazy Loading)

페이지에 대량의 고화질 이미지가 있을 때 모든 이미지를 한 번에 불러오면 초기 로딩 속도가 느려진다. 교차 시점에 맞춰 src 속성을 주입해주는 지연 로딩 코드를 작성해보자.

 

✗ 비효율적인 기존 스크롤 리스너 방식 (성능 저하 유발)

// 잘못된 예: 스크롤할 때마다 모든 이미지의 위치를 계속 계산함
window.addEventListener('scroll', function() {
  const images = document.querySelectorAll('img.lazy');
  images.forEach(img => {
    const rect = img.getBoundingClientRect();
    if (rect.top < window.innerHeight && rect.bottom >= 0) {
      img.src = img.dataset.src;
      img.classList.remove('lazy');
    }
  });
});

위 방식은 스크롤이 움직일 때마다 수백 번씩 호출되며 getBoundingClientRect()로 인해 심각한 프레임 드랍이 발생한다.

 

✓ Intersection Observer를 활용한 성능 최적화 방식

// 올바른 예: 브라우저가 스스로 감지하여 필요한 순간에만 실행
document.addEventListener('DOMContentLoaded', () => {
  const lazyImages = document.querySelectorAll('img.lazy');

  const imageObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const image = entry.target;
        image.src = image.dataset.src; // 실제 이미지 URL 적용
        image.classList.remove('lazy');
        observer.unobserve(image); // 관찰 해제하여 메모리 절약
      }
    });
  }, {
    root: null,
    rootMargin: '0px 0px 100px 0px', // 화면 하단 100px 전에 미리 로드
    threshold: 0.1
  });

  lazyImages.forEach(img => imageObserver.observe(img));
});

결과: 스크롤을 내릴 때 화면에 접근하는 이미지에만 비동기로 HTTP 요청이 발생하며, 한 번 전환된 이미지는 unobserve()로 감지를 중단하여 불필요한 연산을 완전히 차단한다.

 

4. 주의사항과 자주 하는 실수

실무에서 Intersection Observer를 적용할 때 간과하기 쉬운 두 가지 핵심 포인트를 점검해두어야 한다.

 

1) 관찰 해제(unobserve / disconnect) 누락

단발성 이벤트(이미지 로딩 등)가 완료된 후에도 계속 관찰 대상으로 남겨두면 내부 메모리 누수가 발생한다. 처리 완료 시점에 반드시 observer.unobserve(target)를 호출하거나, 컴포넌트 언마운트 시점에 observer.disconnect()를 실행해야 한다.

 

2) Layout Shift(누적 레이아웃 이동) 방지

src가 비어있는 상태의 <img> 태그에 widthheight 또는 CSS 비율(aspect-ratio)을 지정해두지 않으면, 이미지가 로드되는 순간 주변 레이아웃이 미끄러지듯 밀려나는 현상이 발생한다. 지연 로딩 대상 요소에는 항상 플레이스홀더 크기를 CSS로 확보해두는 것이 올바른 방법이다.

 

5. 마무리

Intersection Observer API는 현대 웹 애플리케이션의 스크롤 성능 최적화를 위한 필수 도구다. 불필요한 스크롤 리스너 호출을 줄이는 작은 최적화가 모여 쾌적한 사용자 경험이라는 큰 효과를 만든다는 점을 잊지 말자. 이 글의 실전 예제 코드를 참고해 기존 scroll 이벤트를 교체해보면, 눈에 띄게 부드러워진 스크롤 성능을 얻을 수 있을 것이다.