웹 애플리케이션 성능 저하의 주범, 그리고 Lazy Loading의 필요성

현대의 웹 애플리케이션에서 이미지는 사용자 경험(UX)과 웹 성능을 좌우하는 핵심 요소 중 하나입니다. 고해상도 이미지 사용 증가와 더불어, 페이지 내 이미지 콘텐츠의 비중이 커지면서 웹사이트 로딩 속도 저하는 피할 수 없는 문제가 되었습니다. 모든 이미지를 초기 로딩 시점에 한꺼번에 불러오는 방식은 다음과 같은 치명적인 성능 병목 현상을 야기합니다.

  • 초기 로딩 시간 증가 (FCP, LCP 지연): 뷰포트 밖에 있는 이미지까지 다운로드함으로써, 사용자가 실제로 콘텐츠를 볼 수 있게 되는 시점(First Contentful Paint)과 가장 큰 콘텐츠 요소가 로드되는 시점(Largest Contentful Paint)이 크게 지연됩니다. 이는 Core Web Vitals 지표에도 악영향을 미칩니다.

  • 불필요한 대역폭 소모: 사용자가 스크롤하여 보지 않을 이미지에 대해서도 네트워크 리소스를 낭비하게 되어 모바일 환경에서는 데이터 요금 부담을 가중시키고, 데스크톱 환경에서도 불필요한 네트워크 요청으로 다른 중요한 리소스 로딩을 방해합니다.

  • 서버 부하 증가: 모든 이미지에 대한 요청이 동시에 발생하여 서버에 과도한 부하를 주며, 이는 서비스 안정성 저하로 이어질 수 있습니다.

이러한 문제들을 해결하기 위한 가장 효과적인 전략 중 하나가 바로 이미지 Lazy Loading입니다. Lazy Loading은 웹 페이지의 초기 로딩 시점에 모든 이미지를 불러오는 대신, 사용자의 뷰포트(viewport) 내에 들어올 때 또는 임계치에 도달했을 때 해당 이미지를 비동기적으로 로드하는 기술입니다.

이미지 Lazy Loading의 작동 원리 및 적용 방법

Lazy Loading을 적용하는 주요 방식은 크게 두 가지로 나눌 수 있으며, 각각의 장단점을 이해하고 프로젝트의 요구사항에 맞춰 선택해야 합니다.

1. 브라우저 Native Lazy Loading (loading="lazy")

가장 간단하면서도 강력한 방법으로, HTML <img> 태그에 loading="lazy" 속성을 추가하는 것만으로 구현할 수 있습니다. 최신 브라우저 대부분에서 지원하며, 브라우저 자체적으로 최적화된 방식으로 Lazy Loading을 처리합니다.

<img src="placeholder.jpg"
     data-src="actual-image.jpg"
     alt="Description of image"
     loading="lazy"
     width="500"
     height="300">
  • 장점:

    • 구현 용이성: 별도의 JavaScript 코드 없이 HTML 속성 추가만으로 적용 가능합니다.

    • 성능 최적화: 브라우저 엔진 레벨에서 구현되므로, JavaScript 기반 솔루션보다 더 효율적이고 빠르게 동작할 수 있습니다.

    • 개발자 부담 감소: 복잡한 Intersection Observer API나 이벤트 리스너 관리 없이 브라우저가 알아서 처리합니다.

  • 단점:

    • 브라우저 지원: 모든 레거시 브라우저에서 완벽하게 지원되지 않을 수 있습니다. (그러나 대부분의 현대 브라우저는 지원)

    • 세밀한 제어 부족: 로딩 임계값(threshold)이나 로딩 전략을 개발자가 직접 세밀하게 제어하기 어렵습니다.

권장 사항: 특별한 제어 요구사항이 없다면, Native Lazy Loading을 우선적으로 적용하는 것이 좋습니다.

2. JavaScript 기반 Lazy Loading (Intersection Observer API 활용)

보다 세밀한 제어가 필요하거나, loading="lazy" 속성을 지원하지 않는 브라우저에 대한 폴백(fallback)이 필요할 때 JavaScript 기반 Lazy Loading을 구현할 수 있습니다. 과거에는 스크롤 이벤트 리스너를 활용하는 방식이 주로 사용되었으나, 성능 이슈로 인해 현재는 Intersection Observer API를 활용하는 것이 표준으로 자리 잡았습니다.

기본적인 구현 로직:

  1. 초기에는 이미지의 src 속성에 Placeholder 이미지를 지정하거나, data-src 속성에 실제 이미지 URL을 저장합니다.

  2. Intersection Observer 인스턴스를 생성하고, 이미지 요소들을 감시(observe)합니다.

  3. 이미지 요소가 뷰포트에 진입하거나, 정의된 임계값에 도달하면 Observer 콜백 함수가 실행됩니다.

  4. 콜백 함수 내에서 data-src에 저장된 실제 이미지 URL을 src 속성으로 이동시켜 이미지를 로드합니다.

  5. 이미지 로딩이 완료되면 해당 이미지 요소에 대한 관찰을 중단(unobserve)합니다.

// HTML (예시)
// <img class="lazyload" data-src="actual-image.jpg" alt="Description">

// JavaScript
document.addEventListener("DOMContentLoaded", function() {
    const lazyImages = [].slice.call(document.querySelectorAll("img.lazyload"));

    if ("IntersectionObserver" in window) {
        let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
            entries.forEach(function(entry) {
                if (entry.isIntersecting) {
                    let lazyImage = entry.target;
                    lazyImage.src = lazyImage.dataset.src;
                    // lazyImage.srcset = lazyImage.dataset.srcset; // srcset도 필요하다면
                    lazyImage.classList.remove("lazyload");
                    lazyImageObserver.unobserve(lazyImage);
                }
            });
        }, {
            // Root Margin: 뷰포트 가장자리로부터 이미지를 미리 로드할 영역 (px 또는 %)
            // 예: "0px 0px 200px 0px" 는 하단으로 200px 미리 로드
            rootMargin: "0px 0px 200px 0px"
        });

        lazyImages.forEach(function(lazyImage) {
            lazyImageObserver.observe(lazyImage);
        });
    } else {
        // IntersectionObserver를 지원하지 않는 브라우저를 위한 폴백
        // (예: 스크롤 이벤트 리스너를 사용하거나, 모든 이미지를 즉시 로드)
        // 이 부분은 성능상 권장되지 않으므로, 주요 대상 브라우저를 고려하여 판단
        lazyImages.forEach(function(lazyImage) {
            lazyImage.src = lazyImage.dataset.src;
        });
    }
});
  • 인기 있는 JavaScript Lazy Loading 라이브러리:

    • lazysizes: 매우 가볍고, SEO 친화적이며, srcsetpicture 요소를 포함한 다양한 이미지 형식을 지원합니다. Native Lazy Loading의 폴백으로도 활용될 수 있습니다.

    • lozad.js: 작은 번들 크기와 높은 성능을 자랑합니다.

Lazy Loading 적용 시 고려사항 및 최적화 팁

Lazy Loading은 강력한 성능 최적화 기술이지만, 모든 이미지에 무분별하게 적용하는 것은 오히려 사용자 경험을 해칠 수 있습니다.

  1. 'Above the Fold' 이미지 관리: 초기 화면(뷰포트)에 즉시 보이는 이미지(Above the Fold)에는 Lazy Loading을 적용해서는 안 됩니다. 이 이미지들은 페이지의 Largest Contentful Paint(LCP)에 직접적인 영향을 미치므로, loading="eager"를 명시하거나 Lazy Loading에서 제외하여 즉시 로드되도록 해야 합니다.
<img src="logo.png" alt="Company Logo" width="100" height="50" loading="eager">

LCP breakdown analysis

  1. widthheight 속성 명시: 이미지의 widthheight 속성을 HTML에 명시하여 레이아웃 쉬프트(Cumulative Layout Shift, CLS)를 방지해야 합니다. 이미지가 로드되기 전에 브라우저가 이미지의 공간을 미리 확보할 수 있도록 하여, 로딩 중 레이아웃이 흔들리는 현상을 막습니다. 이는 Core Web Vitals 점수 향상에 필수적입니다.

  2. Placeholder 이미지 사용: Lazy Loading되는 이미지가 로드되기 전까지 사용자에게 시각적인 공백을 줄여주기 위해 흐릿한 저해상도 이미지나 단색 배경의 Placeholder를 사용하는 것이 좋습니다. 이는 사용자에게 페이지가 로드되고 있음을 알리는 역할을 합니다.

  3. srcset<picture> 태그 활용: 반응형 이미지(srcset)와 아트 디렉션(picture 태그)을 Lazy Loading과 함께 사용하여 다양한 화면 크기와 해상도에 최적화된 이미지를 제공하고, 불필요한 이미지 다운로드를 더욱 줄일 수 있습니다.

<picture>
    <source srcset="image-large.webp" type="image/webp" media="(min-width: 1200px)" loading="lazy">
    <source srcset="image-medium.webp" type="image/webp" media="(min-width: 768px)" loading="lazy">
    <img src="placeholder.jpg" data-src="image-small.jpg" alt="Description" loading="lazy">
</picture>

Lighthouse performance score

결론: 성능과 사용자 경험, 두 마리 토끼를 잡는 Lazy Loading

이미지 Lazy Loading은 단순히 이미지 로딩을 지연시키는 것을 넘어, 웹 페이지의 초기 로딩 성능을 획기적으로 개선하고, 대역폭 사용을 효율화하며, 서버 부하를 줄여주는 필수적인 웹 최적화 기법입니다. 특히 Core Web Vitals와 같은 웹 성능 지표의 중요성이 증대되면서, Lazy Loading의 전략적인 적용은 개발자가 반드시 고려해야 할 사항이 되었습니다.

Native Lazy Loading을 우선적으로 고려하되, 세밀한 제어가 필요하거나 특정 브라우저 환경을 지원해야 할 경우 Intersection Observer API 기반의 JavaScript 구현 또는 라이브러리 활용을 검토하십시오. 또한, 'Above the Fold' 이미지 관리, width/height 명시, Placeholder 사용 등의 최적화 팁을 함께 적용하여 사용자에게 빠르고 쾌적한 웹 경험을 제공할 수 있을 것입니다.