개발 기술/개발 이야기

이미지, 배경이미지의 지연 로드 구현 방법 (with. intersectionObserver API)

by GicoMomg 2024. 5. 30.

1. 들어가기에 앞서

  • 각 서비스에는 랜딩 페이지가 존재하며, 랜딩 페이지들은 대개 고화질의 이미지를 사용한다.
  • 사용자가 페이지에 처음 방문했을 때, 페이지의 처음부터 끝까지 스크롤할 수 있지만, 일부는 첫 화면만 보고 다른 탭으로 이동하기도 한다.
  • 그래서 랜딩 페이지의 모든 고화질 이미지를 한꺼번에 로드한다면, 리소스 낭비가 발생한다.
  • 그럼, 어떻게 해야 리소스 낭비를 줄일 수 있을까? 사용자가 접근하지 않은 이미지는 로드하지 않을 수 없을까? 바로 지연 로드 방식을 사용하면 된다!
  • 지연 로드는 사용자가 이미지를 볼 때까지 이미지 로드를 미루는 기법이다. 이 기법을 사용하면 초기 로드 시간을 단축하고, 불필요한 리소스 사용을 줄일 수 있다.
  • 이번 시리즈에서는 “(1) JavaScript의 IntersectionObserver API로 지연로드를 구현하는 법”, “(2) Vue와 React에서 지연로드를 구현하는 법”을 알아보았다.

시리즈 순서
👉 (1) 이미지, 배경이미지의 지연 로드 구현 방법 (with. intersectionObserver API)
(2) 이미지, 배경이미지의 지연 로드 구현 방법 (with. Vue, React)




2. IntersectionObserver로 지연 로드를 구현해보자!

1) IntersectionObserver란?

  • 이미지 지연로드의 핵심은 “유저가 해당 이미지를 봤을 때, 이미지를 로드한다”이다. 여기서 중요한 건 “화면에 요소가 나타났는지 어떻게 감지하는지”이다.
  • 스크롤 길이를 비교해서 감지할 수도 있지만, 더 간단한 방법은 바로 IntersectionObserver API 를 사용하는 것이다.
  • IntersectionObserver는 뷰포트(Viewport)나 특정 부모 요소를 기준으로, 대상 요소가 나타날 때 콜백 함수를 호출하는 API이다.
  • 이 옵저버는 주로 무한 스크롤 이나 이미지 지연 로드을 구현할 때 쓰인다.
예시 Observer 사용 전 Observer 사용 후
무한 스크롤 스크롤 위치가 페이지 하단에 도달했는지를 매번 계산하여 확인.
스크롤 이벤트는 매우 빈번하게 발생하므로, Throttling 처리가 필요.
요소가 뷰포트에 진입할 때만 콜백이 실행되므로, 불필요한 계산을 줄일 수 있음.
스크롤 위치를 수동으로 계산하는 대신, Intersection Observer의 콜백 함수 내에서 필요한 작업만 수행 가능.
이미지 지연 로드 페이지 로드 시점에서 모든 이미지가 즉시 서버에서 로드함.
초기 페이지 로드 시간이 길어짐.
사용자가 실제로 보지 않을 이미지도 로드되므로, 불필요한 네트워크 트래픽이 발생함.
이미지가 뷰포트에 진입할 때만 실제 이미지를 로드.
페이지 로드 시점에서 이미지를 모두 로드하지 않기 때문에 초기 로드 시간이 단축.
필요한 이미지가 뷰포트에 들어올 때만 로드되어, 사용자에게 더 빠른 반응성을 제공.

(1) 사용 방법

  • 사용 방법은 간단한데, 먼저 new IntersectionObserver 생성자를 사용해 observer 객체를 만든다.
  • 생성자의 인자로는, 요소가 화면에 나타났을 때 실행할 callback 함수를 넘겨준다.
const observer = new IntersectionObserver(callback);
function callback(entries) {
  // entries는 IntersectionObserverEntry 객체의 배열임
    if (entries[0].intersectionRatio <= 0) return;

  console.log("감시하는 요소가 화면에 나타났다!");
}

  • 두 번째 단계로, 감지하고 싶은 요소를 observer에 등록한다.
  • 이렇게 하면 해당 요소가 화면에 나타날 때, 미리 등록한 callback 함수가 실행된다.
const targetElement = document.querySelector(".target");

intersectionObserver.observe(targetElement); // 감지 시작!

  • 아래 예시는 네 번째 박스가 화면에 나타났을 때 알림창을 띄우는 코드이다.
  • 스크롤로 네 번째 박스가 있는 영역에 도달하면, 알림창이 나타난다.




(2) 어떻게 이미지의 lazyload 처리를 할 수 있을까?

😉 html, js로 여러 개의 이미지를 지연 로드하는 방법을 알아보자!

  • 먼저, 총 7개의 이미지 태그를 추가한다.
  • 각 이미지 태그에는 class="lazy"data-src 속성을 추가한다.
  • data-src에는 이미지의 경로를 지정하고, src 속성은 비워둔다.
<div class="image-container">
  <img class="lazy" data-src="https://img1.daum..." alt="Lazy Image 1">
</div>
<div class="image-container">
  <img class="lazy" data-src="https://img1.daum..." alt="Lazy Image 2">
</div>
<div class="image-container">
  <img class="lazy" data-src="https://img1.daum..." alt="Lazy Image 3">
</div>
<div class="image-container">
  <img class="lazy" data-src="https://img1.daum..." alt="Lazy Image 4">
</div>
<div class="image-container">
  <img class="lazy" data-src="https://img1.daum..." alt="Lazy Image 5">
</div>
<div class="image-container">
  <img class="lazy" data-src="https://img1.daum..." alt="Lazy Image 6">
</div>
<div class="image-container">
  <img class="lazy" data-src="https://img1.daum..." alt="Lazy Image 7">
</div>

  • 그럼 아래와 같은 형태로 화면이 구성된다. 현재는 src에 이미지가 지정되지 않아 이미지 박스만 보인다.




😉 이제 intersection Observer를 사용해, 지연 로드을 구현해보자

document.addEventListener('DOMContentLoaded', registerObserver);
  • 우선 화면이 처음 렌더링될 때 Intersection Observer를 등록해야한다.
  • DOMContentLoaded 이벤트는 HTML 문서의 구조가 완전히 로드되고 파싱되었을 때 발생한다.
  • DOMContentLoaded이벤트가 발생할 때, 옵저버를 등록하는 registerObserver 함수를 호출한다.

function registerObserver() {
  const options = {
    root: null,        
    rootMargin: '0px',
    threshold: 0.5    
  };
  ...
}
  • registerObserver 함수은 옵저버를 등록하고 관리하는 함수이다.
  • options은 옵저버의 설정값으로, root, rootMargin, threshold을 지정할 수 있다.
  • root는 관찰의 기준이 되는 요소를 지정하는데, null이면 뷰포트가 기준이 된다.
  • rootMargin은 관찰 대상의 여백을 지정할 수 있고, threshold의 경우 0.5로 설정하면 50% 이상 보일 때 콜백을 호출한다.

function registerObserver() {
  ...
  function callback(entries, observer) { // (A), (B)
    entries.forEach(entry => {
      if (entry.isIntersecting) {   // (C)
        const img = entry.target;   
        img.src = img.dataset.src;  // (D)
        img.classList.add('loaded');// (E)

        observer.unobserve(img);    // (F)
      }
    });
  };
 ...
}
  • callback 함수는 Intersection Observer가 감지한 변화가 있을 때 호출된다.
코드번호 설명
(A) entries는 관찰 대상의 상태를 나타내는 객체 배열이다.
(B) 현재 Intersection Observer 객체이다.
(C) entry.isIntersecting를 사용해 대상 요소가 화면에 나타났는지 확인한다.
(D) 만약 대상 요소가 화면에 나타났다면, 이미지를 로드하는 과정을 거친다.
우선, img.srcdata-src 속성의 값을 할당하여 실제 이미지를 로드한다.
(E) img.classList.add('loaded')로 loaded 클래스를 추가한다.
loaded 클래스가 적용되었을 때, 지연 로드 스타일을 적용할 수 있게 된다.
(F) 이미지가 로드된 요소는 더이상 관찰할 필요가 없다.
observer.unobserve(img)를 호출하여 해당 이미지 요소의 관찰을 중지한다.

function registerObserver() {
  ...
  const observer = new IntersectionObserver(callback, options) // (G)
  const lazyImages = document.querySelectorAll('.lazy'); // (H)

  lazyImages.forEach(img => observer.observe(img)); // (I)
}
  • 마지막으로 앞서 선언한 콜백과 옵션을 intersectionObserver에 전달한다.
코드번호 설명
(G) new IntersectionObserver(callback, options)로 Intersection Observer 객체를 생성하고, callback 함수와 options 설정을 전달한다.
(H) 그리고 지연로드 대상이 되는 요소(.lazy)들에 접근한다.
(I) observer.observe(img) 를 호출해 지연 로드 대상을 관찰하면 완성이다!

앞서 설명한 스크립트 전체 코드는 아래와 같다.

document.addEventListener('DOMContentLoaded', registerObserver);

function registerObserver() {
  const opiton = {
    root: null,
    rootMargin: '0px',
    threshold: 0.5
  };
  function callback(entries, observer) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        img.classList.add('loaded');

        observer.unobserve(img); 
      }
    });
  };

  const observer = new IntersectionObserver(callback, opiton);
  const lazyImages = document.querySelectorAll('.lazy');

  lazyImages.forEach(img => observer.observe(img));
}

  • playground를 보면 이미지가 화면에 나타날 때만 이미지가 로드되는 걸 볼 수 있다 🙂



그럼 실제로 초기 로드 시간, 이미지 다운로드양이 줄어들었을까?

  • 크롬의 [네트워크 탭]에서 지연로드 전 후를 비교했을 때, 이미지 리소스 로드시간, 로드양이 감소한 걸 볼 수 있다. (지연로드 전 코드)




(3) 배경이미지의 lazyload 처리는 어떻게 할 수 있을까?

앞서 우리는 이미지를 지연 로드하는 방법을 알아보았다.
그렇다면, 배경 이미지를 지연 로드하는 방법은 없을까? 물론 있다!

  • 먼저, 배경 이미지를 설정할 컨테이너(.background-container)를 생성한다.
  • data-bg 속성에 실제 배경 이미지 경로를 지정한다.
  • 여기서 주의할 점은, 초기에는 background-image: none로 설정해 배경 이미지를 로드하지 않도록 해야한다.
<div class="background-container lazy-background" data-bg="url('https://img1.daum...)">
  Background 1
</div>
<div class="background-container lazy-background" data-bg="url('https://img1.daum...)">
  Background 2
</div>
<div class="background-container lazy-background" data-bg="url('https://img1.daum...)">
  Background 3
</div>
<div class="background-container lazy-background" data-bg="url('https://img1.daum...)">
  Background 4
</div>
<div class="background-container lazy-background" data-bg="url('https://img1.daum...)">
  Background 5
</div>

  • 그럼 아래와 같은 형태로 화면이 구성된다. background-image: none 이 설정되어 있어 background-color만 보인다.



자! 이제 intersection Observer를 사용해, 배경 이미지 지연 로드을 구현해보자
대략적인 과정은 이미지 지연 로드과 같다.

document.addEventListener('DOMContentLoaded', registerObserver);
  • DOMContentLoaded 이벤트가 발생할 때, 옵저버를 등록하는 registerObserver 함수를 호출한다.

function registerObserver() {
  const options = {
    root: null,         // 관찰 기준이 되는 요소를 지정
    rootMargin: '0px',  // 관찰 대상의 여백을 지정
    threshold: 0.5      // 50% 이상 보일 때 콜백을 호출
  };
  ...
}
  • registerObserver 함수은 옵저버를 등록 관리하는 함수이다.

function registerObserver() {
  ...
  function callback(entries, observer) { // (A), (B)
    entries.forEach(entry => {
      if (entry.isIntersecting) {   // (C)
        const container = entry.target;
        container.style.backgroundImage = container.dataset.bg; // (D)
        container.classList.add('loaded'); // (E)
        observer.unobserve(container); // (F)
      }
    });
  };
 ...
}
  • callback 함수는 Intersection Observer가 감지한 변화가 있을 때 호출된다.
코드번호 설명
(A) entries는 관찰 대상의 상태를 나타내는 객체 배열이다.
(B) 현재 Intersection Observer 객체이다.
(C) entry.isIntersecting를 사용해 대상 요소가 화면에 나타났는지 확인한다.
(D) 만약 대상 요소가 화면에 나타났다면, 배경 이미지를 로드하는 과정을 거친다.
먼저, backgroundImagedataset.bg 속성값을 할당하여 배경 이미지를 로드한다.
(E) img.classList.add('loaded')를 적용한다.
(F) 배경 이미지가 로드된 요소는 더이상 관찰할 필요가 없다.
observer.unobserve(container)를 호출하여 이미지 컨테이너 요소의 관찰을 중지한다.

function registerObserver() {
  ...
  const observer = new IntersectionObserver(callback, options);
  const lazyBackgrounds = document.querySelectorAll('.lazy-background');

  lazyBackgrounds.forEach(container => observer.observe(container));
}
  • 이제 마지막으로 콜백과 옵션을 intersectionObserver에 전달해주자.
코드번호 설명
(G) new IntersectionObserver(callback, options)로 Intersection Observer 객체를 생성하고, callback 함수와 options 설정을 전달한다.
(H) 그리고 지연로드 대상이 되는 요소(.lazy-container)들에 접근한다.
(I) observer.observe(container) 를 호출해 지연 로드 대상을 관찰하면 끝난다.

앞서 설명한 스크립트 전체 코드는 아래와 같다.

document.addEventListener('DOMContentLoaded', registerObserver);

function registerObserver() {
  const options = {
    root: null,
    rootMargin: '0px',
    threshold: 0.5 // 50% 이상 보일 때 콜백 호출
  };

  function callback(entries, observer) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const container = entry.target;
        container.style.backgroundImage = container.dataset.bg;
        container.classList.add('loaded');
        observer.unobserve(container); 
      }
    });
  }

  const observer = new IntersectionObserver(callback, options);
  const lazyBackgrounds = document.querySelectorAll('.lazy-background');

  lazyBackgrounds.forEach(container => observer.observe(container));
}

  • playground를 보면 이미지 컨테이너가 화면에 나타날 때만 배경 이미지가 로드되는 걸 볼 수 있다 🙂





3. 마치며…

이번 시간에는 intersectionObserver의 개념, 사용법 그리고 observer를 사용해 이미지, 배경 이미지의 지연로드를 구현하는 법에 대해 알아보았다.
지연 로드는 웹 페이지의 성능을 최적화하고 사용자 경험을 향상시키는 방법이다.
이 기능을 구현하는 방법 중 하나로 IntersectionObserver API를 이용할 수 있다. IntersectionObserver는 요소가 화면에 나타날 때 특정 작업을 수행할 수 있다.

먼저 이미지의 지연 로드는 data-src 속성에 이미지 경로를 저장하고, 요소가 화면에 나타났을 때 src 속성에 data-src값을 전달해 이미지를 로드했다.

배경 이미지의 지연 로드도 비슷한 방식으로 구현할 수 있었다.
배경 이미지의 경우, 요소의 data-bg 속성에 배경 이미지 경로를 저장하고, 요소가 화면에 나타날 때 이 경로를 backgroundImage 스타일 속성에 할당했다.

이처럼 IntersectionObserver를 활용하면 이미지와 배경 이미지 모두 지연 로드를 통해 효율적으로 관리할 수 있다.
다음 시간에는 Vue의 커스텀 디렉티브, React의 커스텀 훅으로 지연로드를 구현하는 방법에 대해 알아보겠다.
만약 포스팅 예정인 Vue, React 코드를 먼저 보고 싶다면 아래 링크의 레포에서 볼 수 있다.


반응형

댓글