Intersection Observer API로 이미지 지연 로딩 성능 최적화하기
Intersection Observer API를 활용하여 이미지 지연 로딩을 구현하고 웹 페이지의 초기 로딩 속도와 네트워크 트래픽을 최적화하는 방법을 알아봅니다. `loading="lazy"` 속성과 비교하며 각 기법의 장단점을 설명합니다.
Intersection Observer API로 이미지 지연 로딩 성능 최적화하기
웹 페이지에서 수많은 이미지를 한 번에 불러오면 초기 로딩 속도가 느려지고 불필요한 네트워크 트래픽이 발생합니다. 특히 스크롤을 내려야 보이는 하단 콘텐츠의 이미지까지 즉시 로딩하는 것은 사용자 경험을 저해하는 주요 원인 중 하나입니다. 이 문제를 해결하기 위해 Intersection Observer API를 활용한 지연 로딩(Lazy Loading) 기법을 적용하여 성능을 최적화하는 방법을 알아보겠습니다.
왜 지연 로딩이 필요한가?
초기 페이지 로딩 시 브라우저는 HTML, CSS, JavaScript 파일을 파싱하고, 이 과정에서 이미지를 포함한 모든 리소스의 다운로드를 시작합니다. 만약 페이지에 수십, 수백 개의 이미지가 있다면, 사용자가 실제로 볼 가능성이 낮은 이미지까지 모두 다운로드하느라 페이지 렌더링이 지연될 수 있습니다. 이는 사용자가 콘텐츠를 보기 전에 기다림을 유발하며, 특히 모바일 환경에서는 데이터 소모와 배터리 사용량 증가로 이어집니다.
지연 로딩은 이미지가 사용자 화면에 보일 때 비로소 해당 이미지의 원본 소스를 요청하여 다운로드하는 방식입니다. 이를 통해 초기 로딩 속도를 획기적으로 개선하고 불필요한 리소스 사용을 줄일 수 있습니다.
Intersection Observer API 소개
Intersection Observer API는 특정 DOM 요소가 뷰포트(viewport)나 다른 요소와 교차하는 시점을 비동기적으로 감지하는 JavaScript API입니다. 콜백 함수를 통해 요소의 가시성 변화를 감지하므로, 스크롤 이벤트 리스너를 직접 사용하는 것보다 훨씬 효율적입니다.
기존의 스크롤 이벤트 기반 지연 로딩은 scroll 이벤트 핸들러 내에서 getBoundingClientRect() 등을 사용하여 요소의 위치를 계속 계산해야 했습니다. 이는 스크롤이 빈번할 때 메인 스레드를 많이 사용하게 되어 성능 저하를 유발할 수 있습니다. Intersection Observer는 이러한 부담을 덜어줍니다.
Intersection Observer API 사용법
Intersection Observer를 사용하려면 IntersectionObserver 생성자를 호출하여 인스턴스를 생성합니다. 이 생성자는 두 개의 인자를 받습니다.
callback: 옵저버 대상 요소의 가시성 변화가 감지될 때 실행될 함수입니다.options: 옵저버 동작을 설정하는 객체입니다.
callback 함수는 두 개의 인자를 받습니다.
entries: 관찰 중인 요소들의 Intersection 변화 정보를 담은IntersectionObserverEntry객체의 배열입니다.observer: 콜백을 트리거한IntersectionObserver인스턴스 자체입니다.
IntersectionObserverEntry 객체는 다음과 같은 유용한 속성을 제공합니다.
isIntersecting: 관찰 대상 요소가 뷰포트와 교차하고 있는지 여부를 boolean 값으로 반환합니다.intersectionRatio: 관찰 대상 요소가 뷰포트와 교차하는 비율을 0.0과 1.0 사이의 값으로 반환합니다.target: 콜백을 트리거한 관찰 대상 DOM 요소를 가리킵니다.
options 객체는 다음과 같은 속성을 가집니다.
root: 교차를 감지할 부모 요소입니다. 기본값은 브라우저 뷰포트입니다.rootMargin:root요소의 경계로부터 얼마나 떨어진 지점에서 교차를 감지할지 설정합니다. CSSmargin과 유사한 형식으로 지정합니다 (예:'10px','0px 0px -50px 0px').threshold:root요소 대비 관찰 대상 요소의 교차 비율을 나타내는 숫자 또는 숫자 배열입니다. 예를 들어0.5는 대상 요소의 50%가 보일 때 콜백을 실행하도록 합니다.0은 조금이라도 보일 때,1은 완전히 보일 때를 의미합니다.
지연 로딩 구현 예제
다음은 Intersection Observer API를 사용하여 이미지 지연 로딩을 구현하는 예제입니다.
HTML (index.html)
1<!DOCTYPE html> 2<html lang="ko"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Lazy Loading with Intersection Observer</title> 7 <style> 8 body { 9 font-family: sans-serif; 10 margin: 0; 11 } 12 .image-container { 13 height: 500px; /* 이미지 높이를 미리 확보하여 스크롤 시 레이아웃 변경 방지 */ 14 background-color: #eee; 15 margin-bottom: 20px; 16 display: flex; 17 justify-content: center; 18 align-items: center; 19 font-size: 24px; 20 color: #888; 21 } 22 img { 23 max-width: 100%; 24 display: block; 25 } 26 </style> 27</head> 28<body> 29 <h1>Intersection Observer Lazy Loading</h1> 30 <div class="image-container">Placeholder 1</div> 31 <img class="lazy" src="" data-src="https://via.placeholder.com/800x600/FF5733/FFFFFF?text=Image+1" alt="Lazy Loaded Image 1" loading="lazy"> 32 33 <div class="image-container">Placeholder 2</div> 34 <img class="lazy" src="" data-src="https://via.placeholder.com/800x600/33FF57/FFFFFF?text=Image+2" alt="Lazy Loaded Image 2" loading="lazy"> 35 36 <div class="image-container">Placeholder 3</div> 37 <img class="lazy" src="" data-src="https://via.placeholder.com/800x600/3357FF/FFFFFF?text=Image+3" alt="Lazy Loaded Image 3" loading="lazy"> 38 39 <div class="image-container">Placeholder 4</div> 40 <img class="lazy" src="" data-src="https://via.placeholder.com/800x600/F5FF33/000000?text=Image+4" alt="Lazy Loaded Image 4" loading="lazy"> 41 42 <div class="image-container">Placeholder 5</div> 43 <img class="lazy" src="" data-src="https://via.placeholder.com/800x600/FF33F5/FFFFFF?text=Image+5" alt="Lazy Loaded Image 5" loading="lazy"> 44 45 <script src="script.js"></script> 46</body> 47</html>
HTML에서는 <img> 태그의 src 속성을 비워두고, 실제 이미지 경로를 data-src 속성에 저장합니다. 또한, 이미지가 로딩되기 전까지 공간을 확보하기 위해 image-container와 같은 플레이스홀더 요소를 사용하거나, CSS height 또는 aspect-ratio 속성을 활용하여 이미지 영역의 크기를 미리 지정하는 것이 좋습니다.
JavaScript (script.js)
1document.addEventListener('DOMContentLoaded', () => { 2 const lazyImages = document.querySelectorAll('.lazy'); 3 4 const observerOptions = { 5 root: null, // 뷰포트를 기준으로 관찰 6 rootMargin: '0px', // root margin 없음 7 threshold: 0.1 // 대상 요소가 10% 보이면 콜백 실행 8 }; 9 10 const imageObserver = new IntersectionObserver((entries, observer) => { 11 entries.forEach(entry => { 12 // 뷰포트와 교차하는 경우 (화면에 보이기 시작하는 경우) 13 if (entry.isIntersecting) { 14 const lazyImage = entry.target; 15 const dataSrc = lazyImage.dataset.src; 16 17 // data-src 속성에 값이 있다면, src 속성으로 복사하여 이미지 로드 18 if (dataSrc) { 19 lazyImage.src = dataSrc; 20 // 로딩이 완료되면 lazy 클래스 제거 (선택 사항) 21 lazyImage.onload = () => { 22 lazyImage.classList.remove('lazy'); 23 // 더 이상 관찰할 필요 없으므로 옵저버 중지 24 observer.unobserve(lazyImage); 25 }; 26 lazyImage.onerror = () => { 27 console.error('Image failed to load:', dataSrc); 28 // 에러 발생 시에도 관찰 중지 29 observer.unobserve(lazyImage); 30 }; 31 } else { 32 console.warn('Image has no data-src attribute:', lazyImage); 33 observer.unobserve(lazyImage); 34 } 35 } 36 }); 37 }, observerOptions); 38 39 // 각 lazy 이미지에 대해 관찰 시작 40 lazyImages.forEach(image => { 41 imageObserver.observe(image); 42 }); 43});
위 JavaScript 코드에서는:
DOMContentLoaded이벤트가 발생하면 실행됩니다.document.querySelectorAll('.lazy')를 사용하여 지연 로딩할 모든 이미지 요소를 선택합니다.IntersectionObserver인스턴스를 생성합니다.threshold: 0.1은 이미지의 10%가 화면에 보이면 콜백 함수를 실행하라는 의미입니다.- 콜백 함수에서는
entry.isIntersecting이true일 때, 즉 이미지가 화면에 보이기 시작하면data-src의 값을src속성으로 복사하여 이미지 로딩을 트리거합니다. - 이미지 로딩이 완료되면(
onload이벤트), 더 이상 해당 이미지를 관찰할 필요가 없으므로observer.unobserve(lazyImage)를 호출하여 관찰을 중지합니다. 이는 불필요한 옵저버 호출을 줄여 성능에 도움이 됩니다. - 각 이미지에 대해
imageObserver.observe(image)를 호출하여 관찰을 시작합니다.
loading="lazy" 속성과의 비교
최신 브라우저들은 <img> 태그에 loading="lazy" 속성을 지원합니다. 이 속성은 브라우저가 자체적으로 지연 로딩을 처리하도록 지시하는 선언적 방법입니다. 개발자가 직접 JavaScript 코드를 작성할 필요 없이 사용 가능하여 매우 편리합니다.
1<img src="image.jpg" loading="lazy" alt="...">
loading="lazy"는 브라우저가 리소스의 우선순위를 결정하여, 뷰포트에서 멀리 떨어진 이미지는 나중에 로드하도록 합니다. 이는 Intersection Observer API를 직접 사용하는 것보다 구현이 간단하다는 장점이 있습니다.
그러나 몇 가지 차이점이 있습니다:
- 제어력: Intersection Observer API는
rootMargin,threshold등을 세밀하게 제어할 수 있어, 특정 조건에 따라 로딩 시점을 더 정확하게 맞출 수 있습니다.loading="lazy"는 브라우저의 판단에 맡겨야 합니다. - 호환성:
loading="lazy"는 비교적 최신 기능으로, 오래된 브라우저에서는 지원되지 않을 수 있습니다. 반면 Intersection Observer API는 Polyfill을 사용하거나, 대체 로직(예: 스크롤 이벤트)을 구현하여 더 넓은 범위의 브라우저를 지원할 수 있습니다. (현재는loading="lazy"의 지원 범위가 상당히 넓어졌습니다.) - 플레이스홀더:
loading="lazy"는src에 이미지를 바로 넣으므로, 초기 로딩 시 빈 화면이 보일 수 있습니다. Intersection Observer 방식에서는src를 비워두고data-src를 사용하므로,src에 빈 이미지나 플레이스홀더 이미지를 지정하여 초기 레이아웃을 안정화하는 것이 용이합니다.
따라서 가장 간단하고 최신 브라우저를 타겟으로 한다면 loading="lazy" 속성을 사용하는 것이 좋습니다. 하지만 더 세밀한 제어가 필요하거나, 오래된 브라우저 지원이 중요하다면 Intersection Observer API를 고려해볼 수 있습니다.
성능 최적화 관점
Intersection Observer API를 활용한 지연 로딩은 다음과 같은 성능 최적화 효과를 가져옵니다.
- 초기 로딩 속도 개선: 초기 뷰포트에 보이는 콘텐츠에 집중하여 페이지 렌더링 시간을 단축합니다.
- 네트워크 트래픽 감소: 사용자가 보지 않는 이미지는 다운로드하지 않으므로, 불필요한 데이터 사용량을 줄입니다.
- CPU/GPU 사용량 감소: 이미지를 디코딩하고 렌더링하는 작업이 화면에 보이는 이미지에 한정되므로, 브라우저의 전반적인 리소스 사용량이 줄어듭니다.
- 배터리 절약: 모바일 기기에서 불필요한 데이터 통신 및 렌더링 작업을 줄여 배터리 소모를 절감합니다.
추가 팁:
- 플레이스홀더: 이미지가 로딩되는 동안 뼈대(skeleton) UI나 낮은 해상도의 미리보기 이미지(LQIP, Low-Quality Image Placeholders)를 표시하여 사용자 경험을 향상시킬 수 있습니다.
rootMargin활용:threshold값과 함께rootMargin을 조절하여 이미지가 화면에 완전히 나타나기 전에 미리 로딩을 시작하도록 설정하면, 스크롤 시 이미지가 나타나는 딜레이를 더욱 줄일 수 있습니다. 예를 들어,rootMargin: '200px'는 뷰포트 하단에서 200px 아래에 있을 때부터 이미지를 로딩하도록 합니다.observer.unobserve()활용: 이미지가 로드되거나 로드에 실패하면 해당 요소에 대한 관찰을 중지하여, 옵저버가 불필요한 작업을 수행하지 않도록 합니다.
결론
Intersection Observer API는 웹 페이지의 성능을 최적화하는 강력한 도구이며, 특히 이미지와 같은 무거운 리소스를 효율적으로 로드하는 지연 로딩 구현에 매우 유용합니다. loading="lazy" 속성으로도 충분한 경우가 많지만, 더 세밀한 제어가 필요할 때 Intersection Observer API는 개발자에게 더 많은 통제권을 제공합니다. 이를 통해 사용자에게 더 빠르고 쾌적한 웹 경험을 선사할 수 있습니다.
참고 자료: