이미지 지연 로딩 구현하기 (feat. custom-directive vs component)

SungMoon, Yoon·2020년 3월 14일
7

Vue.js

목록 보기
1/3

웹 프론트엔드 개발을 하다 보면, IntersectionObserver Web API를 활용하여 다양한 기능들을 구현하곤 한다. Infinite scroll, Lazy Loading, Advertisement, Analytics 등 수많은 곳에서 활용이 가능하다.

위의 기능들은 모두 scrollEvent를 활용하여 구현되어왔었다. 그러나 IntersectionObserver가 발표되고 대부분의 브라우저에서 사용 가능하게 되면서 이제는 반드시 알아야 할 API가 되었다.

2020년 3월 현재 기준 대부분의 PC와 모바일 브라우저에서 지원하며(IE 미지원), polyfill를 제공하기 때문에, 크로스 브라우징에 대한 걱정이 없다.

IntersectionObserver에 대해 자세하게 다룬 포스트, from 레진코믹스 개발 블로그

내가 공동창업자로 있는 스타트업 INPOCK는 인플루언서가 가볍게 만들 수 있는 나만의 쇼핑몰 플랫폼이다. 커머스플랫폼의 특성상 컨텐츠의 대부분을 이미지가 차지한다. 상품 사진이 늦게 보인다면, 소비자들은 서비스를 이탈할 것이다.

인포크는 nuxt.js를 이용하여 개발되었고, img태그를 대체할 LazyImage.vue 컴포넌트를 만들어 재활용했었다. 개발을 마치고 보니, Vue.js에서 제공하는 user directive를 활용하면 어땠을까 하는 생각이 들었다. 그래서 이번 글에서는 현재 사용 중인 LazyImage 컴포넌트구조와, directive 방식을 비교해보고자 한다.

LazyImage 컴포넌트 구현

// LazyImage.vue

<template>
    <img ref="image" >
</template>

<script>
export default {
    props: {
        origin: String
    },
    data() {
        return {
            observer: null
        }
    },
    mounted() {
    	this.initObserver()
    },
    methods: {
        initObserver() {
            this.observer = new IntersectionObserver((entries, ovserver) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        observer.unobserve(entry.target)
                        
                        let image = new Image()
                        image.src = resizedImageURI() // 리사이징 된 이미지의 주소 반환
                        image.onload = () => {
                            this.$refs.image.src = image.src // IE는 object-fit이 적용되지 않기때문에, IE의 경우, backgroundImage로 스타일링
                        }
                        
                        image.onerror = async err => {} // 이미지를 가져오는데 실패하는 경우에 서버에게 이 사실을 알려, 이미지 복원 처리를 수행함.
                        
                        image = null // GARBAGE COLLECTION : 이 부분이 성능적으로 개선을 주는지에 대하여는 검증하지 못했음
                    }
                })
            },{
                rootMargin: '50%'// Intersect 시점을 View port 밖 50%에 위치하는 지점으로 설정
            })
            Array.from([this.$refs.image]).forEach(image => {
                this.observer.observe(image)
            })
        }
    },
    beforeDestroy() {
        if(this.observer !== null) {
            this.observer.disconnect() // 컴포넌트가 소멸하기 전, 모든 IntersectionObserver를 비활성화한다.
        }
    }
    
}
</script>

Vue.directive를 이용해서 새로 구현해본 결과

import Vue from "vue"

Vue.directive("lazy", {
  inserted: (el, { value }) => {
    const observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          observer.unobserve(entry.target)
          let image = new Image()
          image.src = value
          image.onload = () => {
            entry.target.src = value
          }
          image = null
        }
      })
    })
    observer.observe(el)
  }
})

각 방법으로 구현한 component/diretive 호출 상황

<template>
    <div>
        // LazyImage 컴포넌트를 호출하는 방식
        <LazyImage :src="img_url" size="small" />
        <LazyImage :src="img_url" size="small" :use-origin-source="false" />
        
        // v-lazy directive를 활용하는 방식
        <img v-lazy.small="img_url">
        <img v-lazy.small.origin="img_url">
    </div>
</template>

<script>
    // LazyImage component를 호출하기 위한 필수 단계
    import LazyImage from '...'
    export default {
        components: {
            LazyImage
        }
    }
</script>

결론

얼핏 보면 비슷하고, 왜 굳이 두 가지 방법을 만들어서 고민하나 싶지만, 각각 호출하여 실제 개발을 한다고 생각해보니 directive를 활용한 방식이 더 좋았다. 왜냐하면 실제 프로덕트에서는 원본 이미지를 사용해야 할 때도 있고, 상황에 따른 변수들을 넘겨주어야 할 경우가 충분히 발생할 수 있다. component방식이라면 일일이 props로 넘겨주어야 하지만, 이럴 때 v-lazy.small.origin처럼 directive 훅 전달인자 의 binding.modifiers를 활용한다면 좀 더 직관적이고, 관련 있는 속성들을 그룹화할 수 있어서 가독성 측면에서도 좋다는 결론을 내렸다. 그리고 directive는 component호출과 다르게 import, component 등록을 하지 않아도 돼서, 코드량도 줄일 수 있었다.

1개의 댓글

comment-user-thumbnail
2022년 1월 14일

안녕하세요 올려주신 글 잘 읽었습니다

궁금한 점이 있어서 이렇게 글을 남기게 되었습니다.

현재 쇼핑몰을 제작해보고있는데요

카테고리마다 다른 이미지가 있고 컴포넌트는 List.vue로 만들어진걸 쓰고있습니다.

주소값은 /list/:name/:id/:page

이런식으러 사용중인데요

name과 id와 page가 변경 되었을때 이미지가 처음 들어온 이미지로만 고정되어있습니다.

실제로 리스트를 뿌리는 이미지 주소값은 변경이되어있는데 lazy에 있는 value로 들어가지 않더라구요

lazy컴포넌트가 아닌 directive로 이용했습니다.

이럴 경우 어떻게 해야지 변경이 될까요?

한가지 더 궁금한점이 있습니다 small과 origin 이라고 적어주셨는데 따로 스타일 지정이나 태그 값이 없더라구요

혹시 다른곳에서 만드신거면 이것 또한 보여주실 수 있을까 합니다

감사합니다 늦었지만 새해 복 많이 받으세요

답글 달기