React · 2026-04-30

React로 가상화와 레이지 로딩 이미지 갤러리 만들기

React에서 가상화(virtualization)와 레이지 로딩을 결합해 대규모 이미지 갤러리를 효율적으로 렌더링하는 방법과 구현 예제

작성일 : 2026-04-30 ㆍ 작성자 : 관리자
post
목차

개요

대량의 이미지를 한 번에 렌더링하면 성능 문제가 발생한다. 브라우저 메모리와 렌더링 비용이 빠르게 늘어나기 때문이다. 이 문제를 해결하려면 가상화(virtualization)로 보이는 영역만 렌더하고, 레이지 로딩으로 실제 이미지 로드를 지연시키는 방식이 효과적이다. 본문에서는 react-window 기반의 그리드 가상화와 IntersectionObserver를 이용한 레이지 로딩을 결합한 실전 예제를 단계별로 설명한다.

핵심 개념

가상화(virtualization)

가상화는 화면에 보이는 요소만 DOM에 렌더링한다. 스크롤 위치에 따라 보이는 영역을 계산해 필요한 아이템만 그린다. 대량 리스트에서 렌더링 비용을 크게 줄일 수 있다.

레이지 로딩(lazy loading)

레이지 로딩은 이미지가 뷰포트에 들어올 때 실제 src를 설정해 로드한다. 초기 로드 시점의 네트워크 부담을 낮추고 LCP(최대 콘텐츠 페인트) 개선에 도움이 된다.

설계 방향

  • react-window로 그리드 가상화 적용
  • IntersectionObserver로 이미지 레이지 로드 처리
  • 작은 플레이스홀더 사용으로 레이아웃 안정성 확보
  • 이미지 크기와 비율이 다른 경우 aspect-box로 고정

구현 예제 설명

아래 예제는 react-window의 FixedSizeGrid를 사용한다. 각 셀은 LazyImage 컴포넌트를 포함하며, IntersectionObserver로 뷰포트 진입 시 실제 이미지를 로드한다. 이미지 배열은 간단한 URL 리스트로 가정한다.

코드

필요 패키지

  • react
  • react-dom
  • react-window
const LazyImage = ({ src, alt }) => {
  const ref = React.useRef(null);
  const [visible, setVisible] = React.useState(false);

  React.useEffect(() => {
    const node = ref.current;
    if (!node) return;
    const obs = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          setVisible(true);
          obs.disconnect();
        }
      });
    }, { rootMargin: '200px' });

    obs.observe(node);
    return () => obs.disconnect();
  }, []);

  return (
    <div ref={ref} style={{ width: '100%', height: '100%', background: '#eee' }}>
      {visible ? <img src={src} alt={alt} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : null}
    </div>
  );
};

const Gallery = ({ images, columnCount, columnWidth, rowHeight }) => {
  const rowCount = Math.ceil(images.length / columnCount);
  return (
    <FixedSizeGrid
      columnCount={columnCount}
      columnWidth={columnWidth}
      height={600}
      rowCount={rowCount}
      rowHeight={rowHeight}
      width={columnCount * columnWidth}
    >
      {({ columnIndex, rowIndex, style }) => {
        const index = rowIndex * columnCount + columnIndex;
        if (index >= images.length) return null;
        const img = images[index];
        return (
          <div style={{ ...style, padding: 4 }}>
            <LazyImage src={img} alt={`image-${index}`} />
          </div>
        );
      }}
    </FixedSizeGrid>
  );
};

세부 설명과 주의점

  • IntersectionObserver의 rootMargin을 조절해 미리 로드할 시점을 제어한다.
  • react-window는 고정된 셀 크기에 유리하다. 가변 높이일 경우 VariableSizeList나 다른 전략을 검토한다.
  • 이미지 로딩 실패 처리(placeholder, retry)를 구현하면 사용자 경험이 개선된다.
  • 서버에서 가능한 한 적절한 사이즈의 이미지를 제공해 네트워크 낭비를 줄인다.

성능 최적화 팁

  • 이미지에 width/height 또는 aspect 비율을 주어 레이아웃 변동을 줄인다.
  • 브라우저 캐시와 CDN을 적극 활용한다.
  • 리스트 데이터가 자주 바뀌면 키를 안정적으로 관리해 불필요한 리렌더를 줄인다.

결론

가상화와 레이지 로딩을 함께 사용하면 대규모 이미지 갤러리의 퍼포먼스를 크게 개선할 수 있다. react-window로 보이는 영역만 렌더하고, IntersectionObserver로 이미지를 필요 시점에 로드하면 초기 렌더 비용과 네트워크 부담을 줄인다. 위 예제를 기반으로 캐싱, 에러 처리, 반응형 레이아웃을 추가하면 실무 환경에서도 안정적인 갤러리를 구현할 수 있다.

react 이미지 갤러리 예제 virtualized gallery react lazy gallery react react 이미지 최적화 react-window IntersectionObserver 이미지 레이지로드 가상화 그리드