React의 intersection observer

서성원·2025년 4월 25일

리액트

목록 보기
27/27
post-thumbnail

프로젝트에서 옆으로 넘어가는 카드 슬라이드를 구현하였습니다.
슬라이드는 보통 옆으로 쭉 늘어뜨려 카드를 배열해 놓고 슬라이드 할 때마다 현재 카드만 보이도록 옆쪽에 위치한 카드를 의도적으로 가립니다.

즉, 현재 보이는 카드 부분만 사용자에게 보여주면 된다는 것입니다.
만약 모든 카드를 렌더링마다 불러오게 된다면, 사용자에게 보이지 않는 카드들 모두 가져오게 됩니다. 이는 불필요한 이미지 렌더링이 일어나게 되고 성능에도 문제가 발생합니다.

그래서 현재 카드 부분이 보일 때만 카드를 렌더링 하도록 하고 싶었습니다. 자바스크립트에서는IntersectionObserver라는 API를 제공합니다. MDN

해당 API로 이미지가 뷰포트에 들어왔을 시에만 이미지를 지연로딩하는 컴포넌트를 제작하였습니다. 참고로 리액트 + 타입스크립트 조합입니다.

이미지 Props

interface LazyImageProps {
  src: string;        // 로드할 이미지 주소
  alt: string;        // 이미지 설명 (접근성용)
  onError?: () => void; // 이미지 로드 실패 시 콜백
  index?: number;     // 리스트일 경우 지연 순서 지정
  delayMs?: number;   // 각 이미지 로딩 간의 지연 시간
}

상태 정의

const [shouldLoad, setShouldLoad] = useState(false);
const [isVisible, setIsVisible] = useState(false);
  • shouldLoad : 이미지 로드를 시작할지 여부
  • isVisible : 이미지 렌더링 여부

ref

const imgRef = useRef<HTMLImageElement | null>(null);
  • dom 요소 추적으로 IntersectionObserver가 관찰할 수 있도록 함

뷰포트 감지

useEffect(() => {
  const observer = new IntersectionObserver(([entry]) => {
    if (entry.isIntersecting) {
      const delay = index * delayMs;
      const timeout = setTimeout(() => {
        setShouldLoad(true);
      }, delay);
      observer.disconnect();

      return () => clearTimeout(timeout);
    }
  }, { threshold: 0.1 });

  if (imgRef.current) {
    observer.observe(imgRef.current);
  }

  return () => observer.disconnect();
}, [index, delayMs]);
  • 이미지가 뷰포트에 10% 이상 들어오면 -> threshold: 0.1
  • index * delayMs만큼 기다렸다가 setShouldLoad(true)
  • 이는 로딩이 시작되었다는 뜻입니다. 그렇다면 요소를 더 이상 관찰할 필요가 없기에 observer.disconnect()로 중지합니다.
    (disconnect는 모든 요소, unobserve()는 특정요소입니다.)

이미지 로딩

useEffect(() => {
  if (shouldLoad) {
    setIsVisible(true);
  }
}, [shouldLoad]);

렌더링

return isVisible ? (
  <img ref={imgRef} src={src} alt={alt} onError={onError} />
) : (
  <div ref={imgRef} ... /> // placeholder
);

이미지가 아직 안 보이면 placeholder를 렌더링합니다.
로드가 시작되면 진짜 이미지로 교체합니다.

마치며

제가 만든 컴포넌트는 뷰포트에 하나의 카드 + 두 번째 카드의 10분의 1의 정도 보이는 구조였습니다. 만약 뷰포트에 거의 하나의 요소만 들어간다면 index * delayMs를 추가하는 것은 필수는 아니라는 생각이 드네요.

결과적으로 카드 인덱스가 낮은 순으로 순차적 렌더링이 됨을 확인할 수 있었습니다. 원한다면 onError 함수에 fallback 이미지로 대체하여 에러 방지도 할 수 있습니다.

profile
FrontEnd Developer

0개의 댓글