javaScript로 Lazy loading를 구현 (쓰로틀링까지)

202210월 24

이번 시간에는 바닐라 JavaScript로 lazy loading을 구현하는 방법에 대해서 알아보겠습니다.

프로그래머스 사이트의 사전과제관이 있는 고양이 검색사이트를 구현하면서 작성 중입니다.


lazy loading이란?

lazy loading이란 페이지 안의 이미지들이 실제로 화면에 그려져야 할 때 img를 그리는 방식을

의미합니다. 즉 초기에 화면을 렌더링할 때 모든 이미지를 그리는 것이 아니라 이미지가 보여야 할

시간에만 이미지를 그리는 방식을 의미합니다. 이 방식을 사용하면 초기 로딩 시 필요한 이미지 수를

줄이기 때문에 즉 이미지 다운로드 bytes를 줄이는 것으로 네트워크를 덜 쓰기 때문에 초기 렌더링

속도를 높일 수가 있습니다. 또한 이미지 다운로드 수가 줄어들기 때문에 통신 비용이 절감됩니다.


img태그를 이용한 방법

lazy loading을 구현하려면 img태그를 사용하는 방법이 가장 간단합니다.

<img class="img" data-lazy="${cat.url}" alt="${cat.name}" />

잘 보시면 data-lazy라는 속성에 실제 이미지 파일 URL이 들어있기 때문에 img의 src값은 비어 있습니다.

즉 현재 img태그는 아무것도 화면에 그리고 있지 않은 상태인 것입니다.

만약 화면에 img태그를 그려야 하는 순간이 오면 data-lazy 속성값을 src에 넣어주면 img를 그려줍니다.


Scroll Event로 간단한 lazy loading을 구현해보자

const images = document.querySelectorAll("img"); // 모든 이미지 파일 선택
window.addEventListener("scroll", (event) => {
  images.forEach((img) => {
    const scrollTop = window.pageYOffset; // 브라우저 상단의 위치
    if (img.offsetTop < window.innerHeight + scrollTop) {
      // 이미지가 그려져야할때
      const src = img.getAttribute("data-lazy");
      if (src) {
        img.setAttribute("src", src); // 이미지를 그려줍니다.
      }
    }
  });
});

위 코드를 보면 pageYOffset, innerHeight라는 메소드가 등장하는데 무엇일까요?

pageYOffset는 현재 화면의 Y축의 상단값을 의미합니다. window.scrollY이라는 메소드를

사용할 수도 있지만 오래된 브라우저일 경우 pageYOffset만 지원할 수 있기 때문에

pageYOffset를 사용하시는 것을 추천해 드립니다. 그렇다면 innerHeight는 무엇일까요?

innerHeight는 현재 보고 있는 브라우저의 y축의 하단 값을 의미합니다.

즉 pageYOffset + innerHeight = 보고 있는 브라우저 화면의 Y축 최하단의 위치 정보

img.offsetTop은 해당하는 img 엘리먼트의 y축 상단 위치 정보를 의미합니다.

img.offsetTop의 값보다 Y축 최하단의 위치 정보가 더 크다면 img가 화면에 살짝이라도

보인다는 뜻이기 때문에 data-lazy 값을 src에 넣어주면 img를 그려주게 되는 겁니다.

하지만 그냥 이렇게 사용하게 되면 맨 처음 화면을 렌더링할 때 스크롤 이벤트가 발생하지 않기 때문에

이런 식으로 이미지를 제대로 불러오지 못합니다 이럴 경우에는 2가지 방법을 사용할 수 있는데

window.scrollTo(0, 1);

이 방법을 사용하면 강제로 1만큼을 움직이기 때문에 화면에 img를 그려주게 됩니다.

하지만 이 방식은 뭔가 스마트 해보이지 않기 때문에 다른 방식을 사용해 보겠습니다.

...
this.$searchResult.innerHTML = this.data
  .map((cat, index) => {
    if (index < 8) {
      return `
              <section class="item">
                <img class="img" src=${cat.url} alt=${cat.name} />
                <span class="cat-name">${cat.name}</span>
              </section>
              `;
    } else {
      return `
            <section class="item">
              <img class="img" data-lazy=${cat.url} alt=${cat.name} />
              <span class="cat-name">${cat.name}</span>
            </section>
            `;
    }
  })
  .join("");
...

제가 만든 고양이 이미지 사이트에서는 초기에 렌더링 될 때 최대 8개의 고양이가 보여야 하기 때문에

초기의 8개의 img태그에는 제대로 src값을 넣어줍니다. 이렇게 할 경우 문제가 해결됩니다.

다 끝난 것 같지만 문제가 있습니다. 그것은 scroll이 발생할 때마다 이벤트가 실행되기 때문에

성능 면에서 매우 많은 부하가 생길 수밖에 없습니다.


쓰로틀링 방식을 추가해서 구현해보자

쓰로틀링 방식은 scroll event에 많이 쓰이는 방식입니다. 쓰로틀링은 몇 초에 한 번, 또는

몇 밀리초에 한 번씩만 실행되게 제한을 두는 것입니다.

document.addEventListener("DOMContentLoaded", function () {
  var lazyloadImages = document.querySelectorAll("img");
  var lazyloadThrottleTimeout;

  function lazyload() {
    if (lazyloadThrottleTimeout) {
      clearTimeout(lazyloadThrottleTimeout); // setTimeout 삭제
    }

    // setTimeout을 이용한 쓰로틀링
    lazyloadThrottleTimeout = setTimeout(function () {
      var scrollTop = window.pageYOffset;

      lazyloadImages.forEach(function (img) {
        if (img.offsetTop < window.innerHeight + scrollTop) {
          const src = img.getAttribute("data-lazy");
          if (src) {
            img.setAttribute("src", src);
          }
        }
      });

      // 이미지 없으면 이벤트 삭제
      if (lazyloadImages.length == 0) {
        document.removeEventListener("scroll", lazyload);
        window.removeEventListener("resize", lazyload);
        window.removeEventListener("orientationChange", lazyload);
      }
    }, 20);
  }

  document.addEventListener("scroll", lazyload);
  window.addEventListener("resize", lazyload); // 화면 크기가 변경될떄
  window.addEventListener("orientationChange", lazyload); // 화면이 가로에서 세로모드로(또는 반대)
});

기본적으로 setTimeout을 사용해서 몇 초에 한 번씩 이벤트를 실행 시킬 건지를 설정할 수 있습니다.

setTimeout을 사용하기 때문에 필수적으로 clearTimeout을 사용해서 이벤트를 삭제해줘야 합니다.

화면 크기가 변경되거나 화면 가로,세로 모드로 변경될 때에는 화면에 보여주는 이미지의 개수가 변경

되기 때문에 resize, orientationChange를 이용해서 강제로 이벤트를 발생시켜야 합니다.

참고로 위의 코드는 이분의 블로그에서 가져왔습니다.

웹 성능 최적화를 위한 Image Lazy Loading 기법

이렇게 쓰로틀링을 적용할 경우 확연하게 이벤트 발생 횟수가 줄어든 것을 볼 수 있습니다.