프론트엔드 이미지 최적화에 관하여

나무·2023년 4월 5일
2

개요


이미지 최적화는 실제로 우리가 서버에서 데이터를 받아올 때 불필요한 요청을 여러번 하지 않아도 되며, 서버의 저장 공간이 적게 필요로 한다. 이는 비용 절감으로도 이어지며, 유저가 서비스에 접근했을 경우 더 빠르고 쾌적한 웹 환경을 제공해주기 때문에 최적화를 해야한다고 생각한다. 또 구글 SEO 순위를 결정할 때 모바일 응답성을 고려하여, 검색 순위에 노출됩니다.

그럼, 최적화를 하지 않으면 어떤 일이 일어날까?

  • 이미지 로딩이 늦어지기 때문에 웹화면이 그려지는데 오랜 시간이 소요됨
  • 서버에 저장공간을 많이 잡아먹기 때문에 서버 비용이 올라감
  • 불필요한 요청을 계속해서 받아와 그려내야 함
  • 웹 페이지 바이트가 커지며 버벅거릴 수 있음

최적화를 할 수 있는 방법?


웹 화면에 랜더링을 빠르게 하기 위해선, 이미지 리소스 최적화가 반드시 필요하다.

  • width 값 조정하기
  • 최적화된 이미지 포맷사용
  • tag를 사용하여 width, height 값을 선언해 reflow를 방지
    • Reflow?
      어떤 액션이나 이벤트에 의해 DOM요소의 크기나 위치 등을 변경하면 해당 노드의 하위 노드와 상위의 노드들을 포함하여 Layout 단계를 다시 수행하게 됩니다. 변경하려는 특정 요소와 위치, 크기 뿐만 아니라 연관된 요소들의 위치와 크기도 재계산을 하기 때문에 퍼포먼스를 저하시키는 요인이다.
  • 여러 버전의 이미지를 제공
  • 이미지 크기 조절 툴 사용
  • Image CDNs 사용
  • CSS Sprite 기법 사용
  • lazy loading 활용

width 값 조정하기


웹사이트에서 사용하는 이미지는 보통 width값 1,000px을 넘지 않는다. 블로그처럼 좌우에 메뉴바가 존재한다면 800px로도 충분하다.

width값을 줄여주는 것만으로도 10배, 20배, 30배는 더 이미지 용량을 줄여줄 수 있다

<img src="url" width="800" height="300" alt="아주 귀여운 이미지임" />

이미지 포맷(format) 설정


이미지의 종류에 맞게 포맷을 설정하면 이미지 최적화를 할 수 있다

  • JPG → 카메라로 찍은 실제 사진
  • PNG → 만들어진 이미지

ex) 아이폰이나 카메라로 찍은 사진 (JPG), 일러스트나 아이패드로 그린 그림(PNG)

JPEG, WdbP, AVIF

  • JPEG : 손실이 많은 압축 디지털 이미지를 만드는 데 사용할 수 있는 압축 방법이며, 크기와 품질 사이에서 절충하기 위해 압축 정도를 선택할 수 있다
  • WebP : 구글이 발표한 포맥이고, 파일 크기를 줄이기 위해 손실 없는 압축과 무손실 압축을 모두 사용하고 있다 웹사이트의 트래픽 감소 및 로딩 속도 단축을 겨냥한 것으로, 주로 사진 이미지 압축 효과가 높다
  • AVIF : 압축과 비 손실 압축을 전부 지원하기 때문에 WebP처럼 GIF, PNG, JPEG등 상용 이미지 포맷을 대체할 수 있다 애니메이션 기능이 있어 움짤로 쓸 수 있고, 압축 효율이 뛰어나 WebP와 닮았다

이 세가지를 같은 이미지로 압축했을 때 결과적으로 AVIF > WebP > PNG/JPEG 순이었다

브라우저가 AVIF를 지원하면 AVIF를 사용하고, 그렇지 않으면 WebP, 그렇지 않으면 PNG 혹은 JPEG를 사용하는 것이 좋다

HTML에서의 사용법

<picture>
  <source srcset="supercar.avif" type="image/avif" />
  <source srcset="supercar.webp" type="image/webp" />
  <img src="supercar.jpeg" alt="Fast red car" />
</picture>

이미지 고정값


Reflow를 발생시키는 원인(치수가 없는 이미지) 해결법

치수가 없는 이미지들은 Reflow를 발생시켜 퍼포먼스를 저하시키기 때문에 이를 해결하기 위해 이미지 및 비디오 요소에 width와 height 속성을 항상 포함하거나 또는 CSS를 사용하여 필요한 공간 aspect-ratio를 잡는다. 이 방법을 사용하면 이미지가 로드되는 동안 브라우저가 문서의 공간을 올바르게 할당할 수 있다.

<img src="puppy.jpg" width="640" height="360" alt="Puppy with balloons" />

반응형 웹 디자인의 경우 width와 height를 생략하고 CSS를 사용하여 이미지 크리를 조정하기 시작하는데, 이 접근 방식의 단점은 다운로드가 시작되고 브라우저가 크기를 결정할 수 있는 경우에만 이미지를 위한 공간을 할당할 수 있다는 점이다.

이미지가 로드되어 각 이미지가 화면에 나타나면 reflow 되어 텍스트가 갑자기 화면 아래로 튀어나가는 등의 문제가 발생하였는데 이것을 방지하기 위해 aspect-ratio를 사용한다. 이 속성을 사용하면 복잡한 계산 없이 간단하게 속성으로 레이아웃 이동 방지를 할 수 있다.

여러 버전의 이미지 제공


일반적으로 3~5개의 서로 다른 크기의 이미지를 제공하고, 더 많은 이미지 크기를 제공하면 성능 향상은 되지만 그만큼 서버에서 더 많은 공간을 차지하고 HTML을 조금 더 작성해야 합니다.

# Before
<img src="flower-large.jpg" />

# After
<img
  src="flower-large.jpg"
  srcset="flower-small.jpg 480w, flower-large.jpg 1080w"
  sizes="50vw"
/>

src 속성

브라우저가 srcset과 sizes 속성을 지원하지 않으면 fall back으로 src 속성이 동작한다

src 속성은 모든 디바이스 크기에서 동작할 수 있을만큼 충분히 커야한다

srcset 속성

srcset 속성은 이미지 파일명과 width 또는 density 설명응ㄹ 쉼표로 구분한다

480w는 480px임을 브라우저에게 말해준다

이미지 크기 조절 툴


  • sharp npm package
  • ImageMagick CLI tool

sharp npm package


const sharp = require("sharp");
const fs = require("fs");
const directory = "./images";

fs.readdirSync(directory).forEach((file) => {
  sharp(`${directory}/${file}`)
    .resize(200, 100) // width, height
    .toFile(`${directory}/${file}-small.jpg`);
});

ImageMagick


원래 크기의 33%로 조절할 경우

convert -resize 33% flower.jpg flower-small.jpg

# macOS/Linux
convert flower.jpg -resize 300x200 flower-small.jpg

# Windows
magick convert flower.jpg -resize 300x200 flower-small.jpg

외에도 Thumbor나 CLoudinary 같은 이미지 서비스도 있다 Thumbor는 서버에 설치하여 설정되고, Cloudinary는 이러한 세부 정보를 처리하며 서버 설정이 필요하지 않다

Image CDNs


Image CDN을 사용하는 이유는 이미지 최적화에 탁월하고, 이를 이용해 전환하면 파일 크기를 40%~80% 줄일 수 있고 이미지 변환, 최적화 및 전송을 전문으로 하고, 사이트에서 사용되는 이미지에 대한 접근이나 조작을 위한 API로 생각할 수 있다

  • Origin 도메인
  • Image 이미지 검색
  • Security key 다른 사람이 이미지의 새 버전을 만드는 것을 방지
  • Transformations 다양한 이미지 변환 제공

CSS Sprite


웹페이지에 필수적으로 자주 사용되는 아이콘, 버튼 같은 이미지들을 쓸 때마다 여러 이미지들을 불러오는 것이 아니라, 한 이미지 파일로 통합한 후 배경 이미지를 만들어놓고 position 값으로 각각의 이미지를 불러오는 것이 sprite 기법이다

div#sprite {
  background: url(/images/sprite.png) no-repeat;
} //한 이미지를 불러옴

// position으로 각 이미지를 불러옴
div#sprite > .first {
  background-position: 0 0;
}
div#sprite > .second {
  background-position: 0 -15px;
}
div#sprite > .third {
  background-position: 0 -30px;
}

Lazy loading


페이지를 로드할 때, 모든 이미지를 로드하는 것이 아니라 중요하지 않은 자원 또는 당장 필요 없는 자원의 경우 서버에 요청을 미루고 필요한 경우 해당 자원을 요청 받는 방법을 말한다

이를 사용함으로 인해 데이터의 낭비를 막을 수 있고, 브라우저는 서버로부터 자원을 요청받고 난 뒤에 화면을 랜더링하기 때문에 불필요한 자원의 다운로드를 막는 것만으로도 프로세스 시간이 단축될 수 있다 (브라우저의 랜더링 시간을 줄여준다)

사용 방법


img 태그 내의 속성을 통해 lazy-loading을 지원한다

<img loading="lazy">

<picture>
  <source media="(min-width: 800px)" srcset="large.jpg 1x, larger.jpg 2x">
  <img src="photo.jpg" loading="lazy">
</picture>

loading의 속성 값은 다음 3가지와 같다.

  • auto : 디폴트 값으로, 속성값을 지정하지 않은 것과 동일하다.
  • lazy : 뷰포트 상에서 해당 이미지의 위치를 계산하여 이미지 자원을 요청함
  • eager : 어느 위치에 있든지 이미지 자원을 바로 요청받음

이미지 영역의 크기를 지정하는 것이 권장되며, 영역 크기에 대한 정보가 없으면 영역을 0*0으로 인식하게 되고, 해당 이미지 영역으로 스크롤 할 경우 이미지가 로드 되면서 layout shift가 일어날 수 있기 때문에 되도록 해당 img 태그에 명시적으로 높이/너비 값을 지정해야 한다

페이지의 첫 시작부터 보이는 이미지에 대해서는 lazy-loading을 사용하지 말아야 하고, background-image에서는 적용 안 된다 <iframe loading="lazy"> 은 아직 비표준이다.(링크)

Intersaction Observer API


MDN 문서에 따르면 타겟 요소와 상위 요소 또는 최상위 document의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법 즉, 비동기적으로 사용자의 이벤트를 관찰하는 방법을 제공하는 웹 API로서 사용자가 웹 페이지를 스크롤 할 때 어떤 Element 이미지가 해당 뷰 포트 내에 교차되었는지 판단할 수 있는 방법을 제공한다

사용방법


  • callback
const io = new IntersectionObserver(callback[, options])
  • options
    • root
      • default: null, 브라우저의 viewport교차 영역의 기준이 될 root 엘리먼트. observe의 대상으로 등록할 엘리먼트는 반드시 root의 하위 엘리먼트여야 한다.
    • rootMargin
      • default: '0px 0px 0px 0px'
      • root 엘리먼트의 마진값. css에서 margin을 사용하는 방법으로 선언할 수 있고, 축약도 가능하다. px과 %로 표현할 수 있다. rootMargin 값에 따라 교차 영역이 확장 또는 축소된다.
    • threshold
      • default: 0
      • 0.0부터 1.0 사이의 숫자 혹은 이 숫자들로 이루어진 배열로, 타겟 엘리먼트에 대한 교차 영역 비율을 의미한다. 0.0의 경우 타겟 엘리먼트가 교차영역에 진입했을 시점에 observer를 실행하는 것을 의미하고, 1.0의 경우 타켓 엘리먼트 전체가 교차영역에 들어왔을 때 observer를 실행하는 것을 의미한다.

IntersectionObserverEntry 객체의 배열을 동작시킬 수 있는 속성


IntersectionObserver의 callback 함수를 통해 생성된 객체의 배열의 속성을 확인할 수 있는 방법은 다음과 같다.

사용 예제

<div class="example">
  <img src="https://picsum.photos/600/400/?random?0" alt="random image" class="image-default">
  <img data-src="https://picsum.photos/600/400/?random?1" alt="random image" class="image">
  <img data-src="https://picsum.photos/600/400/?random?2" alt="random image" class="image">
  <img data-src="https://picsum.photos/600/400/?random?3" alt="random image" class="image">
</div>
// IntersectionObserver 를 등록한다.
// entries는 위에 data-src로 설정된 img 태그들의 배열 
const io = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    // 관찰 대상이 viewport 안에 들어온 경우 image 로드
    if (entry.isIntersecting) {
      // data-src 정보를 타켓의 src 속성에 설정
      entry.target.src = entry.target.dataset.src;
      // 이미지를 불러왔다면 타켓 엘리먼트에 대한 관찰을 멈춘다.
      observer.unobserve(entry.target);
    }
  })
}, options)

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

참고 사이트 (여기) 👈🏻 위 글은 벨로그 참고

profile
🌳

0개의 댓글