(번역) HTML 이미지 최적화

강엽이·2023년 2월 13일
145
post-thumbnail

원문 : https://www.builder.io/blog/fast-images

여러분이 운영하는 멋있는 페이지가 있고 그 페이지에 배경 이미지를 추가한다고 가정해봅시다.

.hero {
  /* 🚩 */
  background-image: url("/image.png");
}

잠시만요!

이 방법이 성능에 최적화 되지 않은 방법이라는 사실을 알고 계십니까? 그리고 그것보다 더 많은 방법이 있다는 사실도 알고 계십니까?

CSS에서 일반적으로 background-image를 피해야 하는 이유

이미지 크기 최적화

SVG를 사용하는 것 외에, 오늘날 개개인이 가지고 있는 방대한 화면 크기와 해상도를 고려할 때 사이트 모든 방문자가 동일한 이미지 파일을 받아야 하는 경우는 거의 없습니다.

귀하의 사이트는 시계로도 작동합니까? (농담입니다... 제 생각에는)

여러분은 "오! 미디어 쿼리, 화면 크기와 이미지의 크기 범위를 수동으로 지정하겠습니다."라고 말할 수 있습니다.

/* 🚩 */
.hero {
  background-image: url("/image.png");
}
@media only screen and (min-width: 768px) {
  .hero {
    background-image: url("/image-768.png");
  }
}
@media only screen and (min-width: 1268px) {
  .hero {
    background-image: url("/image-1268.png");
  }
}

음, 위 코드에는 문제가 있어요. 꽤 지루하고 장황한 것 외에도, 이 방법은 화면 크기 를 고려할 뿐 해상도 는 고려하지 않습니다.

여러분은 "아하! 저는 다양한 해상도에 대해 다양한 이미지 크기를 지정하는 이미지 세트(image-set)라는 멋진 방법을 알고 있어요." 라고 말할 수 있습니다.

/* 🚩 */
.hero {
  background-image: image-set(url("/image-1x.png") 1x, url("/image-2x.png") 2x);
}

여러분이 맞습니다. 이 방법에는 몇 가지 이점이 있습니다. 하지만 일반적으로 말하면, 우리는 화면 크기와 해상도를 모두 고려해야 합니다.

그래서 우리는 미디어 쿼리와 이미지 세트를 결합한 큰 CSS를 작성할 수도 있지만, 이것은 점점 복잡해질 수 있고, 시간이 지남에 따라 사이트 레이아웃이 발전하더라도 각 화면에 대한 우리의 이미지가 얼마나 큰지 정확하게 알아야 한다는 것을 의미합니다.

그럼에도 불구하고, 이것은 지연 로딩, 지원되는 브라우저를 위한 차세대 형식, 우선순위 힌트, 비동기 코딩 등과 같은 중요한 것을 지원하지 않습니다.

추가적으로, 연쇄적인 요청에도 문제가 있습니다.

연쇄적인 요청 피하기

HTML에서 이미지 태그를 사용하면 src 속성으로 링크를 설정할 수 있습니다. 따라서 브라우저는 초기 HTML을 가져오고 이미지를 스캔하며 우선순위가 높은 이미지를 즉시 가져오기 시작할 수 있습니다.

CSS에서 이미지를 로드하는 경우, 외부 스타일시트(모든 곳에서 인라인 style 대신 link rel=”styleshset”)를 사용한다고 가정하면 브라우저는 HTML을 스캔하고 CSS를 가져온 다음 요소에 background-image가 적용된 것을 발견해야 하며, 이 모든 것이 끝난 후에만 해당 이미지를 가져올 수 있습니다. 이 작업은 더 오래 걸릴 것입니다.

그리고, 인라인 CSS, 사전 로딩(preloading) 이미지, 오리진에 사전 연결(pre-connecting)과 같은 작업을 수행할 수 있습니다. 그러나, 계속 읽다 보면 안타깝게도 CSS의 background-image에서는 얻을 수 없고 HTML img 태그에서 얻을 수 있는 추가적인 이점을 볼 수 있게 됩니다.

배경 이미지를 고려할 때

이미지를 로드하는 가장 최적의 방법에 대해 논의하기 전에 모든 규칙과 마찬가지로 예외가 있다는 것을 기억해야 합니다. 예를 들어, background-repeat로 타일처럼 붙이고자 하는 매우 작은 이미지가 있을 때, 제가 아는 img 태그로 반복을 수행하는 쉬운 방법은 없습니다.

그러나 50px보다 큰 이미지의 경우 CSS에서 설정하는 것을 피하고 img 태그에서 모든 것을 설정하는 것을 강력하게 제안합니다.

이미지를 최적으로 로딩

이제 CSS에서 background-image를 사용하는 것의 어려움에 대해 불평했으므로 실제 해결책에 대해 이야기 해보겠습니다.

현대 HTML에서 img 태그는 이미지를 최적으로 로드하는 데 유용한 여러 속성을 제공합니다. 그것들을 살펴봅시다.

브라우저 자체 지연 로딩

첫 번째로 소개할 이미지 성능을 향상시키기 위한 img 태그의 속성은 loading=lazy입니다.

<!-- 😍 -->
<img loading="lazy" ... />

이제 사용자는 뷰포트에 없는 이미지를 자동으로 다운로드하지 않기 때문에 이것은 이미 크게 개선된 것입니다. 더 좋은 점은 이것은 성능이 굉장히 좋고 브라우저에서 기본적으로 구현되어 있어 JS가 필요하지 않으며 모든 최신 브라우저에서 지원된다는 점입니다.

한 가지 중요한 세부 사항 - 이상적으로 "above the fold(위에 있는 이미지)"는 지연 로딩을 하지 마세요. (즉, 첫 번째 로드 즉시 브라우저의 뷰포트에 보여지는 이미지입니다.) 이렇게 하면 가장 중요한 이미지가 가능한 한 빨리 로드 되고 다른 모든 이미지는 필요한 경우에만 로드됩니다.

P.S.: loading=lazyiframes에서도 동작합니다. 😍

모든 화면 크기와 해상도에 최적의 크기를 제공

srcset을 이미지와 함께 사용하는 것은 매우 중요합니다. SVG를 로드하는 경우가 아니라면 다양한 화면 크기와 해상도에서 최적의 이미지 크기를 얻을 수 있어야 합니다.

<img
  srcset="
    /image.png?width=100 100w,
    /image.png?width=200 200w,
    /image.png?width=400 400w,
    /image.png?width=800 800w
  "
  ...
/>

한 가지 중요한 점은 이것이 CSS에서 image-set로 얻을 수 있는 것보다 더 강력한 버전이라는 것입니다. 왜냐하면 imgsrcset에서 w 단위를 사용할 수 있기 때문입니다.

w 단위의 유용한 점은 크기와 해상도를 모두 고려한다는 것입니다. 따라서 이미지가 현재 2배 픽셀 밀도의 장치에 200px 너비로 표시되어 있으면 브라우저는 400w 이미지(즉, 400px 너비이므로 2배 픽셀 밀도에서 완벽하게 표시)를 잡을 수 있습니다. 마찬가지로 1배 픽셀 밀도 이미지에서는 200w의 이미지가 잡힙니다.

picture 태그가 있는 현대적인 포맷

여기 예제에서 .png를 사용하고 있다는 것을 눈치채셨을 것입니다. 이는 모든 브라우저에서 지원되지만 최적의 이미지 형식은 아닙니다.

img 상단에 picture를 추가하면 webp와 같은 보다 현대적이고 최적의 이미지 포맷을 지정할 수 있습니다. source 태그를 통해 지원가능한 브라우저에서 사용할 수 있습니다.

<picture>
  <source
    type="image/webp"
    srcset="
      /image.webp?width=100 100w,
      /image.webp?width=200 200w,
      /image.webp?width=400 400w,
      /image.webp?width=800 800w
    "
  />
  <img ... />
</picture>

선택적으로 AVIF와 같은 추가 형식을 지원할 수도 있습니다.

<picture>
  <source
    type="image/avif"
    srcset="
      /image.avif?width=100 100w,
      /image.avif?width=200 200w,
      /image.avif?width=400 400w,
      /image.avif?width=800 800w,
      ...
    "
  />
  <source
    type="image/webp"
    srcset="
      /image.webp?width=100 100w,
      /image.webp?width=200 200w,
      /image.webp?width=400 400w,
      /image.webp?width=800 800w,
      ...
    "
  />
  <img ... />
</picture>

aspect-ratio를 잊지 않기

우리는 layout shifts(레이아웃 변경)도 피하는 것도 중요하다는 것을 명심해야 합니다. 이 문제는 이미지 다운로드 전에 이미지의 정확한 크기를 지정하지 않은 상태에서 이미지가 로드될 때 발생합니다. 이를 달성할 수 있는 두 가지 방법이 있습니다.

첫 번째는 이미지의 widthheight 속성을 지정하는 것입니다. 선택적으로, CSS에 heightauto로 설정하여 화면 크기가 변경될 때 이미지가 적절하게 반응하도록 합니다. 이는 종종 좋은 방법입니다.

<img width="500" height="300" style="height: auto" ... />

또는, CSS의 새로운 aspect-ratio 속성을 사용하여 항상 올바른 비율을 자동으로 유지할 수도 있습니다. 이 옵션을 사용하면 이미지의 정확한 너비와 높이를 알 필요 없이 가로 세로 비율만 알면 됩니다.

<img style="aspect-ratio: 5 / 3; width: 100%" ...>

aspect-ratio는 또한 object-fit, object-position와 매우 잘 어울립니다. 이는 배경 이미지의 background-size, background-position와 상당히 유사합니다.

.my-image {
  aspect-ratio: 5 / 3;
  width: 100%;
  /* 이미지의 고유 가로 세로 비율이 다른 경우에도 가능한 공간을 채웁니다. */
  object-fit: cover;
}

비동기 이미지 디코딩

또한, 브라우저가 메인 스레드에서 이미지 디코딩하지 않고 이동할 수 있도록 이미지에 decoding="async"를 지정할 수 있습니다. MDN은 이것을 오프스크린 이미지에 사용하는 것을 권장합니다.

<img decoding="async" ... />

리소스 힌트

마지막 고급 발전된 옵션은 fetchpriority입니다. 이는 LCP 이미지와 같이 이미지의 우선 순위가 매우 높은 경우 브라우저에 힌트를 주어 도움이 될 수 있습니다.

<img fetchpriority="high" ... />

또한, 화면 상단에 있지만 중요성이 높지 않은 캐러셀같은 이미지의 경우, 이미지의 우선 순위를 낮추기 위해 다음과 같이 할 수 있습니다.

<div class="carousel">
  <img class="slide-1" fetchpriority="high" />
  <img class="slide-2" fetchpriority="low" />
  <img class="slide-3" fetchpriority="low" />
</div>

alt 텍스트 추가

alt 텍스트는 접근성과 SEO에 매우 중요하며 간과해서는 안됩니다.

<img alt="Builder.io drag and drop interface" ... />

또한, 추상적인 모양, 색상 또는 그라데이션과 같이 순수하게 표현되는 이미지의 경우 다음과 같은 role 속성을 사용하여 표현으로만 명시적으로 표시할 수 있습니다.

<img role="presentation" ... />

sizes 속성에 대한 이해

위에서 언급한 srcset 속성에 대한 한 가지 중요한 사항은 브라우저가 정확한 이미지의 사이즈를 패치해 오기 위해 렌더링될 이미지의 사이즈를 알아야 한다는 것입니다.

즉, 이미지가 렌더링되면 브라우저는 실제 디스플레이 크기를 알고 픽셀 밀도를 곱하여 srcset에서 가능한 가장 가까운 크기의 이미지를 가져옵니다.

그러나 초기 페이지 로드의 경우, 크롬과 같은 브라우저에서는 HTML에서 img 태그를 찾아 즉시 프리 패칭을 시작하는 프리로드 스캐너가 있습니다.

중요한 것은 페이지가 렌더링되기도 전에 이런 일이 일어난다는 것입니다. 예를 들어, 우리의 CSS는 아직 패치되지도 않았기 때문에 이미지가 어떻게 표시되고 어떤 크기로 표시되는지에 대한 정보가 없습니다. 따라서 브라우저에서는 몇 가지 가정이 필요합니다.

기본적으로 브라우저는 모든 이미지가 전체 페이지 너비인 100vw라고 가정합니다. 그들이 실제 있는 크기 보다 조금 더 크거나 아주 클 수 있습니다. 따라서 최적과는 거리가 멉니다.

여기서 size 속성이 유용합니다.

<img
  srcset="..."
  sizes="(max-width: 400px) 200px, (max-width: 800px) 100vw, 50vw"
  ...
/>

이 속성을 사용하면 다양한 창 크기에서 브라우저에 이미지가 얼마나 클 것으로 예상하는지 (정확하게는, 500px과 같은 정확한 픽셀 값 또는 윈도우 너비의 약 50%를 표시해야 한다고 말하는 50vw와 같은 윈도우에 상대적인 값) 알 수 있습니다.

따라서 위의 예에서 900px 와이드 스크린은 처음 두 개의 조항 중 어느 것과도 일치하지 않으며 대신 더 큰 스크린에 대해 이미지가 50vw로 표시된다고 가정하는 조항과 일치합니다.

따라서 브라우저는 50vw * 900px = 450px이므로 1x 픽셀 밀도 디스플레이의 경우 450px 와이드 이미지를, 2x 픽셀 밀도 디스플레이의 경우 900px 와이드 이미지를 목표로 합니다. 그런 다음 srcset에서 가장 가까운 일치 항목을 찾아 사전 패치할 이미지로 사용합니다.

요약

좋아요, 정말 많습니다. 다 같이 해봅시다.

다음은 로드에 매우 최적화된 이미지의 좋은 예시입니다.

<picture>
  <source
    type="image/avif"
    srcset="
      /image.avif?width=100 100w,
      /image.avif?width=200 200w,
      /image.avif?width=400 400w,
      /image.avif?width=800 800w
    "
  />
  <source
    type="image/webp"
    srcset="
      /image.webp?width=100 100w,
      /image.webp?width=200 200w,
      /image.webp?width=400 400w,
      /image.webp?width=800 800w
    "
  />
  <img
    src="/image.png"
    srcset="
      /image.png?width=100 100w,
      /image.png?width=200 200w,
      /image.png?width=400 400w,
      /image.png?width=800 800w
    "
    sizes="(max-width: 800px) 100vw, 50vw"
    style="width: 100%; aspect-ratio: 16/9"
    loading="lazy"
    decoding="async"
    alt="Builder.io drag and drop interface"
  />
</picture>

우선 순위가 높은 이미지의 경우

위의 이미지는 좋은 기본값이며, 아래에 위치하는 이미지에 가장 적합합니다.

우선 순위가 가장 높은 이미지의 경우 LCP 이미지와 같이 loading="lazy"decoding="async"를 제거하고 가장 우선 순위가 높은 이미지인 경우 fetchpriority="high"를 추가하는 것을 고려해야 합니다.

      style="width: 100%; aspect-ratio: 16/9"
-     loading="lazy"
-     decoding="async"
+     fetchpriority="high"
      alt="Builder.io drag and drop interface"

SVG와 같은 벡터 형식

또한 SVG와 같은 벡터 형식의 경우 여러 크기와 형식을 제공할 필요가 없으며 다음과 같이 포함될 수 있습니다.

<!-- for SVG -->
<img
  src="/image.svg"
  style="width: 100%; aspect-ratio: 16/9"
  loading="lazy"
  decoding="async"
  alt="Builder.io drag and drop interface"
/>

<picture><source> 태그를 완전히 제거했을 뿐만 아니라 srcsetsizes 속성도 더 이상 필요하지 않으므로 제거했습니다.

우선 순위가 높은 SVG의 경우 위에서 언급한 동일한 규칙이 적용됩니다.(loading, decoding을 제거하고 선택적으로 LCP 이미지에 대해 loadpriority="high"를 추가합니다.)

배경으로 이미지를 사용

맞아요, 이 글을 시작할 때 우리는 예시로 배경 이미지를 이야기한 것을 잊을 뻔했습니다.

이제 여기서 논의된 이미지 최적화는 사용할 수 있는 모든 유형의 이미지(예: 배경, 전경 등)에 적용되지만, imgbackground-image처럼 동작하도록 만드는 데는 약간의 CSS (절대 위치 지정 및 object-fit 속성)만 필요합니다.

다음은 직접 시도해 볼 수 있는 예시입니다.

<div class="container">
  <picture class="bg-image">
    <source type="image/webp" ... />
    <img ... />
  </picture>
  <h1>I am on top of the image</h1>
</div>
<style>
  .container {
    position: relative;
  }
  h1 {
    position: relative;
  }
  .bg-image {
    position: absolute;
    inset: 0;
  }
  .bg-image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
</style>

HTML에 많이 추가하는 것은 성능에 좋지 않을까요?

맞기도 하고 아니기도 합니다. 하지만 대부분 아닙니다.

이미지가 바이트 단위로 얼마나 큰지 잊어버리기 쉽습니다. HTML에 몇 바이트를 추가하면 훨씬 최적화된 버전을 로드하여 이미지의 수천 또는 수백만 바이트를 절약할 수 있습니다.

둘째, 압축이 중요하다는 것을 잊지 마세요. 각 이미지에 추가되는 마크업은 매우 중복되어 gizp이 압축시키기에 완벽하게 적합합니다.

따라서 DOM의 팽창과 페이로드의 사이즈는 분명히 우려해야 하지만, 저는 이 교환이 여러분에게 유리하다고 제안하고 싶습니다.

쉬운 방법

요즘에는 그런 것들을 직접 손으로 다 쓸 필요가 거의 없습니다. NextJS, Qwik과 같은 프레임워크와 Cloudinary, Builder.io와 같은 플랫폼은 이런 작업을 쉽게 수행할 수 있는 컴포넌트를 제공합니다. 다음과 같습니다.

<!-- 😍 -->
<image src="/image.png" alt="Builder.io drag and drop interface" />

이를 통해 위의 최적화(모든 이미지 크기 및 형식 생성 포함)를 대부분 자동으로 얻을 수 있습니다.

알아야 할 것은 여전히 대부분의 경우 이미지의 우선순위가 높은 경우 다음과 같이 지정해 주어야 합니다.

<!-- 높은 우선순위 이미지 -->
<image priority src="/image.png" alt="Builder.io drag and drop interface" />

그리고 대부분의 경우 sizes 속성을 사용하려면 수동으로 지정해야 합니다. 대부분의 컴포넌트를 사용하면 다음과 같은 옵션을 직접 넘겨줄 수 있습니다.

<!-- 수동으로 크기 지정 -->
<image
  sizes="(max-width: 500px) 200px, 50vw"
  src="/image.png"
  alt="Builder.io drag and drop interface"
/>

sizes 속성 설정을 자동화할 수 있는 유일한 이미지 컴포넌트는 puppeteer 스크립트를 백그라운드에서 실행하여 다양한 화면 크기로 이미지의 실제 표시 레이아웃을 분석하고 그에 따라 생성할 수 있는 제가 만든 Builder.io용 컴포넌트입니다.

결론

가능하면 CSS에 background-image 대신 HTML에 img를 사용하세요. 그리고 이미지를 최적 방법으로 전달하기 위해 지연 로딩, srcset, picture 태그, 그리고 위에서 논의한 다른 최적화 방식을 사용하세요. 우선 순위가 낮은 이미지와 비교하여 우선 순위가 높은 이미지를 인식하고 그에 따라 속성을 조정하세요.

또는, NextJS 또는 Qwik와 같은 좋은 프레임워크를 사용하거나 Cloudinary 또는 Builder.io와 같은 좋은 플랫폼을 사용하면 쉽게 처리될 수 있습니다.

profile
FE Engineer

5개의 댓글

comment-user-thumbnail
2023년 2월 19일

좋은 글 감사합니다~! 🙇🙇🙇

3개의 답글
comment-user-thumbnail
2023년 8월 1일

배우고 갑니다!!! 감사합니다

답글 달기