이미지 최적화, Lazy Load & Intersection Observer API

HYl·2022년 4월 13일
0

HTTP 아카이브 연구에 따르면 평균 웹사이트의 반 이상이 이미지로 이루어져 있으며, 이미지의 용량도 다른 콘텐츠에 비해 월등히 높다. 따라서 이미지의 사이즈를 적절히 줄이고, 렌더링 속도를 빠르게 하면, 웹 사이트의 렌더링 성능은 더욱더 빨라질 것이다.

하지만 무작정 이미지를 최적화하기 전에, 앞서 내가 만들고 있는 웹 사이트가 이미지 최적화가 필요한지 먼저 고민을 해보아야 한다. 이미지가 중요한 사이트가 아니라면 굳이 리소스와 비용을 투자할 필요가 없기 때문이다.

이미지 최적화 방법에는 여러 가지가 있다.

  • 브라우저 사이즈에 맞춰 적절한 이미지 제공
    • 미디어 쿼리를 활용하는 방법
    • <img> 태그의 srcset 속성을 사용하는 방법
    • <picture> 태그를 사용하는 방법
  • CSS image sprite
  • 이미지 Lazy Loading

올리브영 웹 사이트 최적화 방법 의 글을 참조하자.

이 글에서는 이미지 Lazy Load 기법을 통하여 이미지 최적화 하는 방법에 대하려 알아보려 한다.


사용자가 처음부터 보지 않는 부분을 초기 렌더링 시 로드하게 되면 정작 사용자가 보이는 화면의 로딩 시간이 지연되게 된다.

웹사이트의 이미지는 최대한 사용자가 보이는 부분부터 로드되도록 처리하며, 사용자가 보이지 않는 부분은 Lazy Loading을 적용하여 사용자의 사용자 경험 저하를 막을 수 있도록 해야한다.

10개의 이미지를 사용하여 화면에 나타내주었다.
사용자들에게는 처음 2개의 이미지만 보일 것임에도 불구하고, 10장의 이미지가 초기에 다 로드가 된다.

네트워크 탭에서 속도를 Slow 3G 으로 변경하게 되면 사용자 경험 관점에서 이것이 얼마나 성능에 악영향을 끼치는지 확인할 수 있다. 이때 사용자들은 몇 초간은 아무 것도 보이지 않을 수 있다.


Lazy Loading Image

html 을 살펴보자.

먼저, img srcwidth=10, height 를 지정해두었고 data-src에는 width=300 을 지정을 해두었다.
javascript 를 이용하여 src에 data-src를 대체하여 넣을 것이다.

<div class="container">
  <img src="https://picsum.photos/10/301" data-src="https://picsum.photos/400/301" />
  <img src="https://picsum.photos/10/302" data-src="https://picsum.photos/400/302" />
  <img src="https://picsum.photos/10/303" data-src="https://picsum.photos/400/303" />
  <img src="https://picsum.photos/10/304" data-src="https://picsum.photos/400/304" />
  <img src="https://picsum.photos/10/305" data-src="https://picsum.photos/400/305" />
  <img src="https://picsum.photos/10/306" data-src="https://picsum.photos/400/306" />
  <img src="https://picsum.photos/10/307" data-src="https://picsum.photos/400/307" />
  <img src="https://picsum.photos/10/308" data-src="https://picsum.photos/400/308" />
</div>

밑의 사진은 src에 적용된 사진이 브라우저에 로드된 모습이다. 현재 width를 10으로 지정해두었기 때문에 뿌옇게 흐려지는 사진이 보여진다.

이제, Javascript 를 통해서 src를 data-src로 대체 할 것이다.
forEach문을 사용하여 모든 image 들의 src를 data-src로 대체하였다.

const images = document.querySelectorAll('img');

images.forEach((image) => {
  const newURL = image.getAttribute('data-src');
  image.src = newURL
})

image태그에 data-src 의 url이 잘 적용되어 width가 400으로 지정되어진 확인할 수 있다. 그런데 아직까지는 초기 로딩 시에, 모든 이미지 리소스들을 다 다운로드 받아진 것이 network tab 에서 보여진다.

이제, Intersection Observer API를 이용하여 이미지가 화면에 표시되는지 여부를 감지하여 스크롤을 할 때 화면에 보여지는 이미지만 로드되도록 개선해 볼 것이다.


InterSection Observer API

Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법이다.

페이지가 스크롤 되는 도중에 발생하는 이미지나 다른 컨텐츠의 지연 로딩을 구현하기 위하여 InterSection Observer API을 사용해보자.

또한, IntersectionObserverEntry의 속성을 활용하면 getBoundingClientRect()를 호출한 것과 같은 결과를 알 수 있기 때문에 따로 getBoundingClientRect() 함수를 호출할 필요가 없어 리플로우 현상을 방지할 수 있다.

  • 리플로우(reflow): 리플로우는 브라우저가 웹 페이지의 일부 또는 전체를 다시 그려야하는 경우 발생한다.
const images = document.querySelectorAll('img');

// IntersectionObserver 를 등록한다.
let observer = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
	// 관찰 대상이 viewport 안에 들어오지 않은 경우 return
    if (!entry.isIntersecting) return;    
    
    // 관찰 대상이 viewport 안에 들어온 경우 image 로드
    const image = entry.target;
    const newURL = image.getAttribute('data-src');
    // data-src 정보를 타켓의 src 속성에 설정
    image.src = newURL
    // 이미지를 불러왔다면 타켓 엘리먼트에 대한 관찰을 멈춘다.
    observer.unobserve(image)
  });
}, imageOptions);

// 관찰할 대상을 선언하고, 해당 속성을 관찰시킨다.
images.forEach((image) => {
  observer.observe(image);
})

스크롤을 내려보면, 스크롤이 해당 이미지의 위치에 도달했을 때 이미지를 로딩하고 있다. Network 탭을 보면 순차적으로 이미지를 불러오는 것을 확인할 수 있다.

또한 unobserve 를 이용하여 이미지를 불러왔다면 target element 에 대한 관찰을 멈추었기 때문에, 스크롤을 올렸을 때 다시 이미지가 로드되지 않는 것을 볼 수 있다.


코드 리팩토링

html에서 src와 data-src 두 가지를 선언하여, javascript를 이용하여 lazy load images 를 구현하였는 데, 코드가 길어져 지저분해 보일 수 있다.
코드를 좀 더 짧게 정리를 해보자.

우선 html에 data-src를 다 지우고, src 만 남겨준다.
javascript 에서 replace 를 이용해서 width 를 바꾸어줄 수 있다.

따라서 html 에는 src 만 남게 되어 코드가 간결해졌다.

<div class="container">
  <img src="https://picsum.photos/10/301" />
  <img src="https://picsum.photos/10/302" />
  <img src="https://picsum.photos/10/303" />
  <img src="https://picsum.photos/10/304" />
  <img src="https://picsum.photos/10/305" />
  <img src="https://picsum.photos/10/306" />
  <img src="https://picsum.photos/10/307" />
  <img src="https://picsum.photos/10/308" />
</div>
const images = document.querySelectorAll('img');

let observer = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    console.log(entry)
    if (!entry.isIntersecting) return;
    const image = entry.target;
    image.src = image.src.replace('photos/10/', 'photos/400/')
    observer.unobserve(image)
  });
}, imageOptions);

images.forEach((image) => {
  observer.observe(image);
})

REFERENCE

profile
꾸준히 새로운 것을 알아가는 것을 좋아합니다.

0개의 댓글