woolta

Vue 사용자디렉티브 + IntersectionObserver 를 활용한 이미지 지연로딩 (lazy loading)

wooltaUserImgvue | 2019-07-21

이미지 지연로딩

지연로딩이 없게 된다면 시작되자마자 모든 이미지들을 한번에 로딩하게 됩니다. 이미지의 수가 적다면 상관이 없을수 있으나 이미지의 개수가 많거나 용량이 큰 경우 네트워크 비용의 증가서비스 속도 부분에 큰 단점이 됩니다. 때문에 보이는 영역에 대한 이미지만 로딩하고 그 이후로는 사용자의 액션(드래그) 를 통해 다음 이미지가 필요할 때마다 해당 이미지만 보여주는 방식의 지연로딩을 사용하면 됩니다.

https://image.woolta.com/3fde657da7dbe456.png

다음과 같이 지연로딩을 사용하게 되면 한번에 보이지 않는 영역의 이미지는 최초에 불러오지 않습니다.!!

지연로딩 커스텀 디렉티브

vue 에서는 v-for, v-if 와 같이 간편하게 사용가능한 디렉티브를 제공합니다. 그리고 제공된것 이외에 추가로 만들수 있는 사용자 디렉티브를 제공하는데 이러한 사용자 디렉티브와 교차영역을 감지하는 IntersectionObserver API 를 사용하면 다음과 같이 디렉티브 사용만으로 이미지를 지연로딩 시킬 수 있습니다.

커스텀 디렉티브를 통한 이미지 지연로딩

<img v-lazyload :data-url="imageUrl"/>

다음과 같이 디렉티브를 설정하고 dataset 속성에 이미지 url 를 지정해주기만 하면 이미지가 지연로딩 될 수 있습니다. 코드 한줄만으로 가능하다니 놀랍습니다.!! :) 우선 사용할 사용자 디렉티브IntersectionObserver API 를 알아보고 이를 활용한 디렉티브 지연로딩을 만들어 보도록 하겠습니다.

vue 사용자 디렉티브

사용자 디렉티브v-show 와 같은 엘리먼트에 대한 하위 수준에 DOM 접근이 필요한 경우 사용할 수 있습니다. 간단한 예제로 focus 에 대한 디렉티브를 만들어 보도록 하겠습니다.

Vue.directive('focus', { // 바인딩 된 엘리먼트가 부모 노드에 삽입 되었을 때 호출 inserted(el) { el.focus() } }) <p v-focus="doSomeThing"></p>

이런 식으로 DOM 에 대한 접근을 쉽고 빠르게 만들어서 사용할 수 있습니다. 이때 훅은 inserted 말고도 bind, update, componentUpdated, unbind 가 있으나 해당 포스트에서는 사용하지 않기때문에 빠르게 넘어가도록 하겠습니다. 자세한 내용은 훅함수 를 참조하시면 됩니다. :)

IntersectionObserver API란

IntersectionObserver 란 구글이 2016년 4월에 구글이 소개한 API 로 Chrome 51 버전 부터 지원 가능한 API 입니다. IntersectionObserver 를 간단하게 말해보자면 다음과 같이 설명할 수 있습니다.

타겟 엘리먼트가 조상 엘리먼트 orviewport 의 교차영역에서 발생하는 변화를 비동기로 관찰하는 방법 제공

IntersectionObserver 를 사용하게 되면 본인이 설정한 앨리먼트가 실제 사용자들이 보는 viewport 영역에서 보이는지 아닌지를 알 수 있습니다. 이를 활용해서 여러가지를 할수 있지만 대표적으로는 다음과 같은것들을 할 수 있습니다.

대표적 활용 예시

  • image lazy loading
  • infinite scroll
  • 배너 or 광고 노출 여부 확인

IntersectionObserver 를 사용하기전에는 이러한 교차를 감지하기 위해 window속성의 innerHeight 혹은 getBoundingClientRect를 사용해서 구현해야 하였습니다. 이러한 방식들은 다음과 같은 단점들이 존재합니다.

  • 구현하기 까다로움.
  • getBoundingClientRect 의 경우 호출할때마다 문서의 일부 or 전체를 다시 그리는 리플로우(reflow) 현상이 발생
  • iframe 을 통한 광고 지연 로딩 or 노출 여부 확인 불가

이러한 단점들을 IntersectionObserver 를 사용하면 간편하게 해결 할 수 있습니다. :)

브라우저 호환성

아쉽게도 IntersectionObserver 는 IE에서는 지원히지 않습니다... safari 의 경우 이전에는 지원하지않았으나 macOS 10.14.4 beta, iOS 12.2 beta. 버전 이상부터 지원이 가능합니다. 호환을 위한 폴리필도 존재하기는 하지만 아직 완벽하게 정상적으로 동작하지는 않습니다.

API 사용방법

const io = new IntersectionObserver(callback, options);

IntersectionObserver 는 다음과 같이 2개의 파라미터를 통해 생성 할 수 있습니다. 이때 첫번째 속성인 callback은 감지하기로 설정한 엘리먼트가 뷰포트에 감지되었을때 호출되는 함수 속성이고 두번째 속성은 감지할 엘리먼트에 대한 각종 설정을 하는 속성 입니다.

options

const io = new IntersectionObserver(callback, { root: null, rootMargin: 0 threshold: 0 })
  • root : 브라우저 viewport에 교차하는 영역의 기준이 될 루트 엘리먼트를 지정합니다. 지정하지 않는경우 null 로 설정되고 이는 브라우저의 viewport가 지정되는것을 의미합니다.

  • rootMargin : root 옵션으로 정한 엘리먼트의 마진값을 설정합니다. 마진 설정을 통해 교차하는 영역의 확장, 축소가 가능합니다. 지정할때는 css 로 margin을 지정하는것처럼 축약 선언도 가능하고 %도 지원합니다. 설정을 안하게 된다면 기본 값은 0px 0px 0px 0px 로 설정됩니다.

  • threshold : 0~1 사이의 숫자로 지정하게 되면 해당 비율만큼 root로 지정한 엘리먼트가 교차했을때 observer를 실행시켜 주게 됩니다. 예를 들어 0.5로 세팅 할 경우 root로 지정한 엘리먼트가 50% 이상 viewport 안에 진입해야 observer를 실행시켜 주게 됩니다. 설정을 안하게 된다면 기본 값은 0으로 설정됩니다.

callback

const io = new IntersectionObserver((entries, observer) => {}), options...)

callback 은 위의 root 에 설정한 엘리먼트가 교차되었을때 실행(당연히 rootMarginthreshold 설정까지 반영되어 감지합니다.)되는 함수 입니다. 이때 Element 배열인 entries자기 자신인 observer 2개가 반환됩니다.

사용 예시

const io = new IntersectionObserver((entries, observer) => {}), options...) const images = document.querySelectorAll('.io-img'); images.forEach((el) => io.observe(el));

다음과 같이 생성된 IntersectionObserverobserve 메소드를 사용하여 타겟 엘리먼트에 등록할 수 있습니다. 이와 반대로 등록한 것을 멈추고 싶다면 unobserve 를 사용하여 교차영역에 대한 관찰을 중지할 수 있습니다.

이미지 지연 로딩 예시

<html> <style> .io-img { width: 80%; height: 80%; margin-bottom: 2rem; } </style> <body> <div> <img class="io-img" data-src="https://picsum.photos/420/320?image=0"/> <img class="io-img" data-src="https://lorempixel.com/420/320/abstract/1/Sample"/> <img class="io-img" data-src="https://imgplaceholder.com/420x320/ff7f7f/333333/fa-image"/> <img class="io-img" data-src="https://via.placeholder.com/420x320/ff7f7f/333333?text=Sample"/> <img class="io-img" data-src="https://placeimg.com/420/320/tech/grayscale"/> <img class="io-img" data-src="https://loremflickr.com/420/320?lock=1"/> <img class="io-img" data-src="https://placekitten.com/420/320?image=2"/> <img class="io-img" data-src="https://placebear.com/420/320?image=2"/> </div> </body> <script> const io = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { // 감지대상이 교차영역에 진입 할 경우 entry.target.src = entry.target.dataset.src; observer.unobserve(entry.target); // 이미지 로딩 이후론 관찰할 필요 x } }) }) const images = document.querySelectorAll('.io-img'); images.forEach((el) => io.observe(el)); </script> </html>

위의 예제를 간단하게 설명하면 처음부터 url을 src 에 넣지 않고 dataset 속성에 할당합니다. 이렇게 하면 바로 이미지를 한번에 불러오지 않습니다. 이후 IntersectionObserver 를 통해 교차영역에 들어온 엘리먼트만 dataset 속성에 작성한 urlsrc에 넣어주어서 지연된 로딩을 처리할 수 있습니다. 이때 이미 로딩된 엘리먼트들은 더이상 감지를 할 필요가 없으므로 unobserve 를 통하여 해제해 주도록 합니다. 이렇게 작성하면 다음과 같이 이미지가 교차영역에 감지될때마다 하나씩 불러오는것을 확인하실수 있습니다.

https://image.woolta.com/jslazy.gif

지연로딩 디렉티브 제작

이제 사용자 디렉티브와 IntersectionObserver API 를 사용하여 지연로딩 디렉티브를 제작하도록 해보겠습니다.

지연로딩 디렉티브

import Vue from "vue"; const lazyLoad = { inserted: el => { function loadImage() { const isImg = el.nodeName === "IMG"; // 이미지 태그일 경우만 url 입력 로딩 if (isImg) { el.src = el.dataset.url; } } function createObserver() { const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { // 감지대상이 교차영역에 진입 할 경우 loadImage(); // 교차영역 들어올경우 이미지 로딩 observer.unobserve(el); // 이미지 로딩 이후론 관찰할 필요 x } }); }); observer.observe(el); } // 지원하지 않는 브라우저는 바로 이미지 로딩을 시켜주도록 호환성 체크 window["IntersectionObserver"] ? createObserver() : loadImage(); } }; Vue.directive("lazyload", lazyLoad);

돔이 부모노드에 들어가는 순간 loadImage, createObserver 2가지 함수를 만들고 IntersectionObserver API 의 지원유무에 따라 지연로딩을 할것인지를 판단하게 됩니다.

// 지원하지 않는 브라우저는 바로 이미지 로딩을 시켜주도록 호환성 체크 window["IntersectionObserver"] ? createObserver() : loadImage();

각 함수들의 역활은 각각 다음과 같습니다.

  1. loadImage : 이미지 엘리먼트 인 경우 dataset의 속성의 이미지 경로를 url에 주입
  2. createObserver : 디렉티브로 지정한 엘리먼트를 감지하도록 생성

이제 디렉티브를 설정하였으니 이미지태그에 디렉티브를 사용해서 정상적으로 동작하는지 확인해 보도록 하겠습니다.

디렉티브를 이용한 지연로딩 확인예제

<template> <div> <h3>이미지 지연 로딩</h3> <div class="image-container" v-for="image in images"> <img v-lazyload :data-url="image"/> </div> </div> </template> <script> export default { name: "image", data() { return { images: [ 'https://picsum.photos/420/320?image=0', 'https://lorempixel.com/420/320/abstract/1/Sample', 'https://imgplaceholder.com/420x320/ff7f7f/333333/fa-image', 'https://via.placeholder.com/420x320/ff7f7f/333333?text=Sample', 'https://placeimg.com/420/320/tech/grayscale', 'https://loremflickr.com/420/320?lock=1', 'https://placekitten.com/420/320?image=2', 'https://placebear.com/420/320?image=2', 'https://www.fillmurray.com/420/320' ] } }, } </script> <style scoped> .image-container { display: flex; justify-content: center; align-items: center; width: 80%; height: 20rem; margin: 2rem 0; } </style>

이제 브라우저를 들어가 확인해보면 다음과 같이 디렉티브 설정만으로 이미지를 지연로딩하는것을 확인할 수 있습니다.

https://image.woolta.com/vuelazy.gif

다음과 같이 성공적으로 불러오는것을 확인 할 수 있습니다. 물론 지연로딩뿐만 아니라 위의 디렉티브내에서 클래스를 추가하는 등의 방식으로 로딩되는 동안 로딩 이미지를 보여주게 하는등의 커스텀 기능도 만들 수 있습니다.!!

크롬 자체 지원 레이지 로딩..

크롬에서는 이러한 지연로딩을 **Chrome 75 **버전부터 img, iframe 태그에 대해 지원 한다고 합니다.

<img src="cat.jpg" loading="lazy" /> <iframe src="..." loading="lazy" />

위의 예시와 같이 loading 이라는 속성을 통해 지연로딩을 지원합니다. 다만 **Chrome 75 ** 부터 지원된다고 하니 나중에는 이러한 추가작업 없이 더욱 편하게 지연로딩을 사용할 수 있습니다.!!!!

참고