본문 바로가기
Frontend2024년 5월 5일7분 읽기

Intersection Observer 실전 — 무한 스크롤과 지연 로딩

YS
김영삼
조회 138

Intersection Observer란?

Intersection Observer API는 대상 엘리먼트가 뷰포트 또는 지정된 루트 엘리먼트와 교차하는지를 비동기적으로 감시합니다. 기존 scroll 이벤트 리스너 방식과 달리 메인 스레드를 블로킹하지 않아 성능이 훨씬 좋습니다.

기본 사용법

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        console.log('요소가 뷰포트에 진입:', entry.target);
      }
    });
  },
  {
    root: null,
    rootMargin: '0px',
    threshold: [0, 0.5, 1]
  }
);

observer.observe(document.querySelector('#target'));
observer.unobserve(element);
observer.disconnect();

무한 스크롤 구현

목록 하단에 sentinel 요소를 두고, 이 요소가 뷰포트에 진입하면 다음 페이지를 로드합니다.

React에서의 구현

import { useEffect, useRef, useCallback, useState } from 'react';

function useInfiniteScroll(fetchMore) {
  const sentinelRef = useRef(null);
  const [loading, setLoading] = useState(false);

  const handleIntersect = useCallback(async (entries) => {
    const entry = entries[0];
    if (entry.isIntersecting && !loading) {
      setLoading(true);
      await fetchMore();
      setLoading(false);
    }
  }, [fetchMore, loading]);

  useEffect(() => {
    const observer = new IntersectionObserver(handleIntersect, {
      rootMargin: '200px'
    });
    if (sentinelRef.current) {
      observer.observe(sentinelRef.current);
    }
    return () => observer.disconnect();
  }, [handleIntersect]);

  return { sentinelRef, loading };
}

function PostList() {
  const [posts, setPosts] = useState([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);

  const fetchMore = useCallback(async () => {
    const res = await fetch(\`/api/posts?page=\${page}\`);
    const data = await res.json();
    if (data.length === 0) { setHasMore(false); return; }
    setPosts((prev) => [...prev, ...data]);
    setPage((p) => p + 1);
  }, [page]);

  const { sentinelRef, loading } = useInfiniteScroll(fetchMore);

  return (
    
{posts.map((post) => ( ))} {hasMore &&
} {loading && }
); }

이미지 지연 로딩

function lazyLoadImages() {
  const images = document.querySelectorAll('img[data-src]');
  const imageObserver = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          if (img.dataset.srcset) img.srcset = img.dataset.srcset;
          img.classList.add('loaded');
          imageObserver.unobserve(img);
        }
      });
    },
    { rootMargin: '50px 0px' }
  );
  images.forEach((img) => imageObserver.observe(img));
}

스크롤 애니메이션

const animObserver = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add('animate-in');
        animObserver.unobserve(entry.target);
      }
    });
  },
  { threshold: 0.15 }
);

document.querySelectorAll('.animate-on-scroll')
  .forEach((el) => animObserver.observe(el));

scroll 이벤트 대비 성능 비교

항목scroll 이벤트Intersection Observer
메인 스레드 블로킹있음없음
getBoundingClientRect 호출매 프레임불필요
throttle/debounce 필요필수불필요
다중 요소 감시성능 저하효율적
브라우저 지원모든 브라우저IE 제외 전부

실전 팁

  • rootMargin을 활용하면 사용자가 스크롤하기 전에 미리 콘텐츠를 로드할 수 있습니다.
  • threshold를 배열로 지정하면 교차 비율에 따른 세밀한 제어가 가능합니다.
  • 한번만 실행되는 관찰은 콜백 내에서 반드시 unobserve를 호출하세요.
  • 네이티브 loading="lazy" 속성이 있지만, 커스터마이징이 필요하면 Intersection Observer가 유리합니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록