본문 바로가기
> 레벨업의 테크노트

[프론트엔드]Image Lazy Load 디렉티브 구현 & 사용법

by 쫑's 2022. 4. 29.

안녕하세요!

빅픽처인터렉티브(주)에서 프론트엔드 개발자로 재직 중인 김종완입니다.

이번 글에서는 이미지를 브라우저 뷰포트에 들어왔을때 lazy load 할 수 있게 해 주도록 만든 imglazyLoad 디렉티브에 대한 구현 방법과 사용 설명에 대해 간략히 적어보려고 합니다.

이번에 작성하는 글은 다른 프론트엔드 팀원분들도 아주 쉽게 이해를 할 수 있을 거라 판단하여 정말 간략히 만 설명드리고자 하여 글이 조금 짧을 수도 있습니다 ^^;;


구현 배경

페이지가 나타날때 스크롤이 어떻든 보통 페이지 전체를 랜더링을 하게 되며 이미지 또한 전부 가져오게 됩니다.

하지만 스크롤이 긴 페이지의 경우 즉, 리스트가 굉장히 많은 페이지의 경우 그리고 그 리스트에 용량이 클 수도 있는 이미지들이 섞여 있다면 한 번에 많은 이미지를 로드하기 위해 그만큼 네트워크 요청을 하게 되고 이는 트래픽이 높아져 서버에 부하를 줄 수 있게 됩니다.

 

 

그렇게 차이가 있을까?

그렇습니다 커머스가 아니라면 그리고 이미지 용량이 크지만 않다면 딱히 큰 차이가 없을 수도 있지만 꼭 이것만을 위해 구현한것은 아닙니다.

lazy load를 구현하면서 추가적으로 처리를 해주고 싶었던 것은 이미지를 제대로 가져왔는지 아니면 가져오는데에 실패를 하였는지 보다 손쉽게 확인하고 커스텀하게 처리할 수 있도록 하고자 하는 이유도 있었습니다.

 

구현하면서 저희 목표는 이러했습니다.

 

1. viewport에 진입하였을 때 lazy load가 되도록 한다.
2. image뿐만 아니라 background-image도 lazy-load할 수 있어야 한다. 
3. 로드/에러 여부를 손쉽게 확인할 수 있어야한다.​
4. 로드/에러시 style에서도 바로 확인이 가능하며 style(css, less..)만으로 UI를 커스텀하게 분기 처리할 수 있어야 한다.
5. 추가적으로 로드/에러시 이벤트를 받아 script단에서도 추가로 커스텀이 가능하여야 한다.
6. 쓰기 쉽도록 만든다.

 


구현 방법

viewport에 들어왔는지 아닌지를 확인하기 위해 IntersectionObserver API를 쓰기로 하였고 이미지가 큰 경우 viewport에 진입하고 나서 이미지를 로드하게 되면 조금 부자연스러울 수도 있다고 판단하여 100px 정도의 여유를 주도록 구현을 하여야 했습니다.

 

const imgLazyLoad = {
  inserted: (el, binding, vnode) => {
    if (!window) return;

    if (window.IntersectionObserver) {
      if (!Vue.$imgLazyLoadIo) {
        const options = {
          root: null,
          rootMargin: '100px 0px',
        };

        Vue.$imgLazyLoadIo = new IntersectionObserver((entries, observer) => {
          entries.forEach(entry => {
            const { target } = entry;

            if (entry.intersectionRatio > 0) {
              imageLoader(vnode, target);
              observer.unobserve(target);
            }
          });
        }, options);
      }

      Vue.$imgLazyLoadIo.observe(el);
    } else {
      imageLoader(vnode, el);
    }
  },
};

 

 

바인딩된 엘리먼트가 DOM에 삽입되었을 때 IntersectionObserver API를 사용할 수 있는 브라우저 환경인지 확인한 후 지원이 안 되는 브라우저 환경일 경우 lazy load를 하지 않고 이미지를 로드합니다. 하지만 지원을 하는 경우 IntersectionObserver객체가 아직 생성되지 않았다면 Vue 객체에 $imgLazyLoadIo라는 객체에 초기화해주고 해당 엘리먼트가 observe가 되도록 해줍니다.

그 후 entry가 viewport에 조금이라도 진입되면 이미지를 로드하고 다시는 관찰하지 않도록 unobserve를 해줍니다.

 

그럼 imageLoader 함수를 보도록 하겠습니다.

 

const imageLoader = (vnode, el) => {
  const src = el.getAttribute('data-src');
  const bgSrc = el.getAttribute('data-bg-src');

  const imageEl = bgSrc ? document.createElement('img') : el;

  el.setAttribute('lazy', 'loading');

  if(src) el.setAttribute('src', src);
  else if(bgSrc) {
    imageEl.setAttribute('src', bgSrc);
    el.style.backgroundImage = `url(${bgSrc})`;
  }
  else {
    el.setAttribute('lazy', 'error');
  }

  imageEl.onload = () => {
    el.removeAttribute('data-src');
    el.removeAttribute('data-bg-src');
    el.removeAttribute('data-error');

    el.setAttribute('lazy', 'loaded');

    emitEvent(vnode, 'lazy-load', { el });
  };

  imageEl.onerror = () => {
    const errorImg = el.getAttribute('data-error');
    el.removeAttribute('data-error');
    if (errorImg) {
      if(src) el.setAttribute('src', errorImg);
      else if(bgSrc) el.style.backgroundImage = `url(${bgSrc})`;
    } else {
      el.setAttribute('lazy', 'error');

      emitEvent(vnode, 'lazy-error', { el });
    }
  };
};

 

imageLoader함수는 말 그대로 이미지를 불러오는 부분을 담당하는 함수입니다.

엘리먼트의 dataset 중 src와 bg-src를 가져오고 로드해오고자 하는 이미지가 백그라운드 이미지인지 확인을 합니다.

 

백그라운드 이미지를 로드해오고자 한다면 태그가 img가 아닌 다른 태그일 수도 있기 때문에 onload & onerror 이벤트가 존재하지 않을 수 있습니다. 그렇기 때문에 백그라운드 이미지를 로드해오고자 할 시 img 엘리먼트를 만들어 주고 onload & onerror 이벤트를 감지할 수 있도록 합니다.

 

소스를 보다 보면 엘리먼트에 attribute를 set 해주는 부분을 중간 중간 보실 수 있으실 겁니다. 여기서 lazy라는 attribute를 set해주는 경우가 있는데 이 부분이 바로 style에서도 이미지가 제대로 로드가 되었는지 아니면 에러가 났는지 로드 중인지 확인할 수 있게 하는 부분입니다. 

 

>img{ .hidden;
    &[lazy=loaded]{ .visible; }
    &[lazy=error]{ background-color:red; }
    &[lazy=loading] { background-color:gray; }
  }

 

이렇게 할 경우 저희는 더 이상 불필요한 script를 작성하지 않고 오직 style부분만 커스텀하여 더 나은 UI를 만들어낼 수 있게 되겠죠?

 

또한 이미지 로드에 실패한 경우나 로드가 된 경우 directive를 사용한 엘리먼트가 이벤트를 emit 해주도록 되어 있어 script 적으로도 추가적으로 대처할 수 있도록 하였습니다.

 

...

<img v-img-lazy-load v-if="imgLoad" :data-src="imgSrc" :alt="alt" @load="onLoad" @error="onError">

...

<script>
import imgLazyLoad from '@/directives/imgLazyLoad';

export default {
  ...
  
  data() {
    return {
      imgLoad: false,
    };
  },
  
  ...
  
  methods: {
    onLoad() {
      this.imgLoad = true;
    },
    onError() {
      console.log('에러 발생!!!');
    },
  },
};
</script>

 

다른 부분은 소스를 보시면 이해가 쉽게 되겠지만 추가적으로 말씀드릴 부분은 onerror 부분입니다. 

이미지 로드에 실패했을 경우 대체 이미지를 보여주고자 할 때 ImgLazyLoad directive에서는 error라는 dataset을 받도록 되어있습니다.

 

<img v-img-lazy-load :data-src="imgSrc" :alt="alt" data-error="error.jpg">

 

근데 대체 이미지 또한 에러가 난다면? 아마.. 무한루프가 발생하겠죠.

그렇기 때문에 한번 이미지를 로드하는데 실패하였을 경우 data-error의 이미지를 다시 불러오는데 불러오기 전 error dataset을 지워줍니다. 그렇게 되면 또다시 이미지 로드에 실패할 경우 대체 이미지가 없다고 판단하고 error이벤트를 emit 해주게 되며 더 이상 이미지를 로드해오지 않습니다.

 

이벤트를 emit 해주는 emitEvent 함수는 아래와 같으며 별다른 설명이 없다고 판단하여 소스만 공유하도록 하겠습니다.

 

const emitEvent = (vnode, eventName, args = undefined) => {
  if (vnode.componentInstance) {
    vnode.componentInstance.$emit('lazy-error', args);
  } else {
    vnode.elm.dispatchEvent(new CustomEvent('lazy-error', args));
  }
};

 


 

사용법

 

1. 이미지 lazy load

<img v-img-lazy-load data-src="default.jpg" data-error="error.jpg" :alt="alt" @load="onLoad" @error="onError">

...


methods: {
    onLoad() {
      this.imgLoad = true;
    },
    onError(el) { // element를 인자로 넘겨받는다.
      console.log('에러 발생!!!');
    },
  },
  
  
 ...
 
 
 <style lang="scss">
    img{ visiblity: hidden;
        &[lazy=loaded]{ visiblity: visible; }
        &[lazy=error]{ background-color:red; }
        &[lazy=loading] { background-color:gray; }
    }
</style>

 

1. 백그라운드 이미지 lazy load

<div v-img-lazy-load data-bg-src="default.jpg" data-error="error.jpg" :alt="alt" @load="onLoad" @error="onError">

...


methods: {
    onLoad() {
      this.imgLoad = true;
    },
    onError(el) { // element를 인자로 넘겨받는다.
      console.log('에러 발생!!!');
    },
  },
  
  
 ...
 
 
 <style lang="scss">
    > div{ visiblity: hidden;
        &[lazy=loaded]{ visiblity: visible; }
        &[lazy=error]{ background-color:red; }
        &[lazy=loading] { background-color:gray; }
    }
</style>

 


 

마치며..

 

구현하는데 크게 어려움이 없었지만 세심한 부분이 퀄리티를 높여준다고 생각하기 때문에 구현하게 된 디렉티브입니다.

팀원분들께서 잘 사용할지 안 할지는 모르겠지만 많은 플랫폼에서 image lazy load를 많이 활용하고 있다고 생각하였고 써드파티를 가져와서 쓰기보다는 직접 만들어서 쓰면 저희 서비스에 맞게 수정도 용이할 거 같아 구현하였습니다.

 

빅픽처 프론트 팀원들은 아마 디렉티브 소스만 보시면 바로 아~ 하며 이해할만한 정도의 수준이어서 별다른 피드백이 없을 거 같긴 하지만 아쉬운 부분이 있다거나 제가 놓친 부분이 있다면 언제든지 피드백 주시면 감사하겠습니다.

 

추가로 LVUP에서 쓰고 있는 CdnImg 같은 공용 컴포넌트도 v-lazy-load를 이용해서 만들고 있으니까 

나중에 시간 되시면 한 번씩 봐주시고 적극적으로 사용해주시면 감사하겠습니다!

 

감사합니다~~~

댓글