IntersectionObserver을 이용하여 image lazy loading 구현하기

최정은·2026년 1월 6일

1. 개요: 왜 지연 로딩이 필요했나?

현재 사내 서비스에서는 Jira REST API와 연동하여 등록된 이슈의 상세 내용을 보여주는 기능을 제공하고 있습니다. Jira 이슈의 description에는 텍스트뿐만 아니라 여러 장의 이미지가 포함될 수 있습니다.

하지만 기존 방식에는 두 가지 큰 문제점이 있었습니다.
1. 불필요한 리소스 낭비: 이슈 컴포넌트가 마운트되는 순간, 화면 하단에 있어 당장 보이지 않는 이미지까지 한꺼번에 로드되었습니다.
2. 초기 로딩 속도 저하: 수많은 이미지 요청이 동시에 발생하면서 정작 중요한 이슈 텍스트 데이터를 확인하는 속도가 느려졌습니다.

단순히 loading="lazy" 속성을 쓰면 해결될 것 같았지만, 인증이 필요한 리소스라는 특이점이 있었습니다.


2. 왜 native lazy loading을 쓸 수 없었나?

Jira API를 통해 가져오는 이미지는 보안상 Authorization: Bearer <token> 헤더가 포함된 요청으로만 가져올 수 있는 Private 리소스입니다.

  • <img> 태그의 기본 src 속성이나 HTML 표준 속성인 loading="lazy"커스텀 헤더를 실어서 보낼 수 없습니다.
  • 따라서 fetch API를 통해 이미지 데이터를 blob 형태로 받은 뒤 URL.createObjectURL로 변환하는 과정이 반드시 필요했습니다.

이 과정이 "사용자에게 이미지가 보일 때"만 트리거되도록 제어하기 위해 Intersection Observer API를 선택했습니다.


3. Intersection Observer API란?

MDN에 따르면, Intersection Observer API는 상위 요소 또는 최상위 문서의 viewport와 대상 요소 사이의 변화를 비동기적으로 관찰하는 수단입니다.

핵심 옵션 3가지

  • root: 대상 요소를 감지할 기준이 되는 요소입니다. (기본값: 브라우저 뷰포트)
  • rootMargin: root 주위의 여백입니다. 마치 CSS의 margin처럼 감지 영역을 확장하거나 축소할 수 있어, 이미지가 화면에 보이기 조금 직전에 미리 로드를 시작하게 만들 수 있습니다.
  • threshold: 대상 요소가 얼마나 노출되었을 때 콜백을 실행할지 결정합니다. (0.0 ~ 1.0)

4. 구현 코드

이미지가 뷰포트에 들어왔을 때 인증 요청을 보내고 브라우저에 렌더링하는 로직입니다.

interface LazyImageProps {
  img: HTMLImageElement;
  scrollContainer?: HTMLElement | null;
}

const setupLazyLoading = ({ img, scrollContainer }: LazyImageProps) => {
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach(async (entry) => {
        // 1. 요소가 화면에 감지되었을 때만 실행
        if (entry.isIntersecting) {
          // 2. 한 번 감지되면 관찰 해제 (중복 로드 방지)
          observer.unobserve(img);

          const originalUrl = img.dataset.src;
          if (originalUrl) {
            try {
              // 인증 헤더가 포함된 이미지 fetch 함수 (예시)
              const blobUrl = await getLazyImageUrl(originalUrl);

              img.onload = () => {
                img.dataset.loading = 'false';
              };
              img.src = blobUrl;
            } catch (error) {
              console.error("이미지 로드 실패:", error);
              img.alt = "이미지를 불러올 수 없습니다.";
            }
          }
        }
      });
    },
    {
      root: scrollContainer || null,
      rootMargin: '200px', // 사용자 경험을 위해 200px 정도 미리 로드
      threshold: 0.1,
    }
  );

  observer.observe(img);
};

data-src를 사용하나요?
브라우저는 <img> 태그에 src 속성이 할당되는 즉시 리소스를 다운로드하려고 시도합니다. 이를 막기 위해 실제 URL을 data-src라는 커스텀 속성에 임시로 저장해 두었다가, 감지가 완료된 시점에만 src로 옮겨주는 방식을 사용합니다.

5. 결과 및 성과

Intersection Observer를 적용한 결과 다음과 같은 개선을 이뤄냈습니다.

  • 네트워크 트래픽 최적화: 사용자가 스크롤을 내리지 않으면 하단 이미지 요청을 아예 보내지 않습니다.

  • 성능 향상: 이슈 상세 페이지 진입 시 초기 API 응답 및 렌더링 속도가 눈에 띄게 빨라졌습니다. (예: 이미지 5개 중 화면에 보이는 2개만 우선 로드)

  • 세밀한 제어: rootMargin을 통해 사용자가 이미지를 인식하기 전 미리 로딩을 시작하여, "끊기는 느낌" 없는 자연스러운 지연 로딩을 구현했습니다.

0개의 댓글