Next.js 이미지 최적화 방법과 <Image> 컴포넌트 사용 예제

시소·2024년 6월 19일
0
post-thumbnail

Intro

웹 사이트에서 있어서 이미지라는 요소는 아마 가장 크고 핵심적인 부분을 차지하는 중요한 요소이지 않을까 싶다.
이미지를 적절히 활용하면 사용자를 더 오래 머물 수 있도록 만들고, 사용자의 참여도를 더욱 높일 수 있다.

하지만, 적절하게 최적화 되지 않거나 무분별하게 사용된 이미지는 오히려 역효과를 불러 일으킬 가능성이 있다.
예시로 포맷이나 압축에 대한 고려 없이 고해상도의 이미지를 불러와 페이지 로딩 시간이 길어진다거나, 불러올 때 이미지 크기 관련(width/height) 정보가 예측되지 않아 레이아웃 시프트 현상이 발생하는 것과 같은 경우가 그렇다.

따라서 오늘은 이렇게 웹에서 사용자 만족도를 높일 수 있도록, 이미지를 효과적으로 사용하는 방법에 대해 알아보자.
(참고: 다음 내용은 Next.js v13 환경을 대상으로 작성되었습니다.)

Image Optimization 개요

정적 애셋 (Static Assets)

본격적으로 Next.js 에서 이미지를 최적화 하는 방법에 대해 알아보기 전에, 먼저 이미지와 같은 정적 자산을 프로젝트에서 어떻게 관리하는지에 대해 짚고 넘어가 보려 한다.

Next.js 에서 정적 애셋은 주로 프로젝트 루트의 /public 경로에서 관리된다. '정적'이라는 단어에서 추측해볼 수 있듯이, 일반적으로 변경되지 않을 애셋들을 이곳에 두고 사용한다.

정적 애셋으로 다루기 적합한 예시

  • 이미지 파일 : jpg, png, gif, svg 등 포맷의 로고, 배너, 아이콘, 배경 이미지 등
  • 글꼴 파일 : woff, woff2, ttf 등
  • 문서 파일 : pdf, xlsx, doc, txt 등
  • 미디어 파일 : mp3, mp4, wav, mov 등
  • JSON 파일 : 웹 사이트에서 사용하는 정적 데이터 파일

해당 폴더에서 관리하는 이유?

  • /public 위치 아래에 있는 애셋들은 코드에서 불러올 때 간결하게 접근 가능하다.
    • /public/image.png → src='/image.png'
  • 빌드 과정에서 별도의 프로세싱이나 번들링 되는 것 없이 그대로 제공된다.
    • 빌드 아티팩트로 복사되지 않는다.

정리하면, 정적 애셋은 애플리케이션에서 변경되지 않는 파일들을 말하고 요청이 있을 때마다 동일한 형태로써 제공된다.
따라서 여러 번 재사용 할 수 있고, 로컬 캐싱을 통해 빠른 속도로 제공된다는 장점이 있는 웹 사이트 리소스의 일종이다.


이미지 최적화는 왜 필요한가?

그럼 본격적으로 이미지 최적화라는 과정이 왜 필요한지에 대해 생각해보자.

만약 일반적인 HTML에서 이미지를 화면에 보여주어야 한다면, 간단하게는 다음과 같은 코드를 작성할 수 있다.

<img src="/hero.png" alt="웹사이트의 배너 이미지입니다" />

그런데 만약 다음과 같은 요구사항이 생긴다면 이에 대해 어떻게 처리할 것인가?

  • 반응형 이미지: 이미지가 다양한 화면 사이즈에 대해 반응형으로 보여진다. (화면 크기에 맞게 자동 조정)
  • 다양한 장치 크기에 맞는 이미지: 다양한 장치에 대해 이미지 크기를 지정한다.
  • 레이아웃 시프트 방지: 이미지를 불러올 때 레이아웃이 바뀌는 것을 방지한다.
  • Lazy loading: 뷰포트 외부에 있는 이미지에 대해 Lazy load를 적용한다.

이러한 요구사항은 바로 이미지 최적화의 일부분이다. 모두 웹에서 이미지를 렌더링할 때 효율적이고, 빠르고, 안정적으로 사용자에게 제공하기 위한 기법에 해당한다.

이미지 최적화라는 주제 그 자체로도 하나의 전문 분야로 간주될 수 있을 만큼 웹 개발에 있어서 그만큼 필수적인 개념 중 하나라고도 할 수 있다.


다양한 이미지 최적화 기법

이미지를 사용할 때 여러 가지 방식으로 최적화를 수행할 수 있다. 각각의 기법이 가장 효과적일 수 있는 상황은 때에 따라 다르니 각 방식의 특징에 대해 알아 두면 도움이 될 것 같다. 다음은 대표적인 이미지 최적화 기법이다.

적절한 이미지 포맷 선택

  • JPEG: 사진과 같은 복잡한 이미지. 손실 압축을 통해 파일 크기 감소
  • PNG: 투명도가 필요한 이미지. 무손실 압축
  • GIF: 움직이는 이미지
  • WebP: 높은 압축률과 품질을 제공하는 이미지. JPEG + PNG 장점 결합
  • AVIF: 최신 이미지 형식. 역시 높은 압축률과 품질 제공

반응형 이미지

  • srcset, sizes 속성 및 <picture> 요소: 다양한 해상도와 크기에 맞는 이미지 제공
  • 참고: Responsive images

이미지 압축 전략

  • 손실 압축: 일부 데이터를 영구적으로 제거하여 이미지 파일 크기를 줄이는 방법. 이미지 품질이 약간 떨어질 수는 있지만 대개 육안으로는 큰 차이를 느끼기 어려움 (Ex. JPEG)
  • 무손실 압축: 데이터 손실 없이 이미지 파일 크기를 줄이는 방법. 이미지 품질이 저하되지 않지만, 손실 압축에 비해서는 파일 크기 감소 정도가 적음 (Ex. PNG)

클라우드 이미지 공급자

  • Image Provider: Cloudinary, Imgix, Akamai 등과 같은 서비스를 활용해 이미지 최적화 기능 활용

지연 로딩

  • Lazy load: 화면에 보이지 않는 이미지는 로드하지 않음. 이미지가 뷰포트에 들어올 때 로드하여 초기 로딩 시간 감소
  • Intersection Observer API: 요소가 뷰포트에 보이는지 감지하기 위한 Web API

캐싱/CDN 활용

  • 브라우저 캐싱: 브라우저에 이미지 파일을 캐시하여 재방문 시 로딩 시간 단축
  • 컨텐츠 전송 네트워크(CDN): 전 세계 지역에 분산된 서버 네트워크를 통해 지리적으로 가까운 위치에서 이미지 제공, 속도 향상

스프라이트 이미지

  • CSS Sprites: 여러 작은 이미지를 하나의 단일 이미지 파일로 결합하고, CSS의 background-position 속성을 활용해 특정 이미지 표시

Next.js에는 이러한 최적화를 수동으로 구현하는 대신 자동으로 이미지를 최적화시킬 수 있는 기능을 제공하고 있다. 아래에서 해당 컴포넌트에 대해 본격적으로 알아보자.


next/image 모듈

<Image> 컴포넌트

지난 포스트에서 알아 봤던 폰트 최적화처럼, 이미지 최적화 역시 별도의 모듈을 통해서 제공된다.

next/image 모듈 안에 있는 <Image> 컴포넌트는 HTML <img> 태그의 확장으로써, 자동으로 이미지 최적화와 관련된 몇 가지 기능을 제공하고 있다.

  • 이미지 로드 시, layout shift 방지
  • 이미지 사이즈를 조정하여, 작은 뷰포트를 가진 장치에 큰 이미지가 제공되는 것을 방지
  • 기본적으로 이미지에 lazy loading 적용 (뷰포트에 들어왔을 때 로드)
  • WebP 혹은 AVIF 같은 최신 형식으로 이미지 제공 가능 (브라우저가 지원해야 가능)

기본 사용법

로컬 이미지

.jpg, .png, .gif, .webp 등 다양한 형식의 이미지 파일을 불러올 수 있다.

로컬에서 제공하는 이미지를 사용할 때의 장점은 가져온 파일을 기반으로 이미지 높이와 너비(width, height)가 자동으로 결정되기 때문에, 값을 명시하지 않아도 된다는 편리함이 있다. 추가로 이미지 레이아웃 시프트 현상을 방지해주는 효과까지 있다.

만약 이미지가 프로젝트 루트의 /public 디렉토리 아래에 있을 때 아래와 같이 가져올 수 있다.
이 때 2가지 방식으로 이미지를 가져올 수 있다. 차이점에 유의한다.

// app/page.jsx

import simpsonImage from '/public/simpson.gif' // 1️⃣
import Image from 'next/image'

...

<Image 
  src={simpsonImage}  // 1️⃣
  alt="이미지 불러오기 1" 
/>

<Image 
  src={'/simpson.gif'}  // 2️⃣
  alt="이미지 불러오기 2" 
  width={320}
  height={320}
/>

1️⃣번 방식:

  • 이미지를 모듈로 가져오는 방식
  • 번들러에서 이미지를 처리하고, 빌드 되면서 해당 이미지를 포함하며 이 때 이미지 메타데이터(너비, 높이)가 확인됨
  • 따라서 <Image> 컴포넌트에 width, height 속성을 전달하지 않아도 컴포넌트가 자동으로 너비와 높이 인식 가능
  • 브라우저가 해당 이미지를 메모리 캐시에 저장하고 있어, 네트워크 요청 없이 매우 빠르게 로드됨

2️⃣번 방식:

  • 이미지를 직접 경로를 통해 서버에서 가져오는 방식
  • 이 때는 <Image> 컴포넌트에 width, height 속성을 제공하는 게 필수
  • 서버와 통신하여 이미지가 최신 상태인지 확인하고, 캐시된 이미지를 반환
  • 따라서 정적인 이미지 파일 보다는, 이미지가 자주 변경되거나 서버와 동기화 되어야 할 때 유용

원격 이미지

원격에 있는 이미지를 불러올 때는 src 속성에 URL 문자열을 전달하여 불러올 수 있다.
로컬 이미지와는 달리 원격 이미지는 빌드 과정 중에서 액세스 할 수 없으므로, widthheight를 필수적으로 명시해야 한다.

올바른 종횡비(가로세로 비율)로 이미지를 보여주기 위해서는 알맞은 너비와 높이 지정이 필요하다.
또한 허용된 이미지 경로만 불러오도록 next.config.js 파일에서 원격 호스트 설정을 따로 해주지 않으면 Un-configured Host 를 마주할 수 있으니 설정도 미리 해주도록 하자. (참고: Errors: next/image Un-configured Host)

// app/page.jsx

 <Image
   src="https://cdn.pixabay.com/photo/2023/10/11/04/08/water-lilies-8307632_1280.jpg"
   width={320}
   height={200}
   alt="Water lilies"
/>
// next.config.js
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.pixabay.com',
        port: '',
        pathname: '/photo/**',
      },
    ],
  },
};

위 캡처를 통해 Next.js 가 제공하는 자동 이미지 최적화 기능 중 일부분을 확인해볼 수 있다.

  • widthheight: 종횡비에 맞게 width를 기준으로 height가 자동으로 조정된다. ("Rendered size" 부분)
  • loading: 기본적으로 lazy loading이 적용된다. (뷰포트에 보이기 전까지 이미지 로드 지연)
  • srcset: 브라우저에 사용자 디스플레이 해상도에 가장 적합한 이미지를 제공하기 위해 일반 해상도(x1) 및 고해상도(2x) 이미지를 제공하고 있다.

이미지 로더 활용

Next.js에 내장된 Image Optimization API를 사용하는 대신, 커스텀 로더를 통해 이미지를 제공할 수 있다.

이 설정은 클라우드 공급자와 같이 외부 이미지 최적화 서비스를 사용해 이미지를 최적화하는 방법을 직접 정의하려는 경우 유용하다.

로더의 역할

이미지 로더는 기본적으로 이미지의 URL을 생성하는 일을 한다. src 속성을 통해 전달된 URL을 기반으로 여러 크기의 이미지를 요청할 수 있도록 다양한 URL을 생성한다. 이를 통해 사용자 뷰포트에 맞춰 적절한 크기의 이미지를 제공하고, 최적화된 포맷으로 변환하고, 품질 조정 등을 수행하는 것과 같은 작업이 가능해진다.

또한, 원격 이미지인데도 불구하고 로컬 이미지를 가져올 때처럼 부분 URL을 전달해 이미지를 가져올 수 있다. (src="/images/bg.png" 처럼)

로더 적용 방법

  • 애플리케이션 단 적용) next.config.js 에서 loderFile에 자신만의 커스텀 로더를 정의
  • 이미지 별 적용) <Image> 컴포넌트에 loader prop을 전달

Cloudinary 로더 예시

다음은 Next.js 에서 사용 가능한 여러 종류의 로더 중, Cloudinary 로더를 사용하는 예시이다.

// next.config.js

const nextConfig = {
  images: {
    loader: 'custom',
    path: '',
    loaderFile: './image-loader.js',
  },
};
// image-loader.js
'use client';

export default function cloudinaryLoader({ src, width, quality }) {
  const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`];
  return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`;
}
// app/page.jsx

<Image
  src="/turtles.jpg"
  width={300}
  height={200}
  alt="Demo Image"
/>


Examples

Responsive Image

sizes 속성

다양한 breakpoints 에서 이미지 너비에 대한 정보를 제공하기 위해 사용되는 속성이다. CSS의 미디어 쿼리와 유사한 형식으로 정의된다.

이미지 성능 지표를 향상시키기 위한 목적으로 사용된다. 주요 효과는 다음과 같다.

  • 적절한 크기의 이미지 제공: 브라우저가 최적의 크기로 이미지를 다운로드 할 수 있게 만든다.
  • 자동 srcset 조정: sizes 속성을 기반으로 srcset을 자동 생성한다.
  • 성능 향상: 불필요하게 큰 이미지를 다운로드 하지 않도록 해 페이지 로딩 시간을 단축할 수 있다.

다양한 뷰포트 너비에 최적화된 이미지 제공하기

<Image
  alt="Hero image"
  src={heroImage}
  sizes="100vw" // 브라우저에게 이미지가 항상 뷰포트의 전체 너비(100%) 만큼 크기로 표시되어야 함을 알림
  style={{
    width: '100%',
    height: 'auto',
  }}
/>

위 코드는 브라우저에서 렌더링 될때 아래와 같은 코드 형태로 실행된다.

<img 
  alt="Hero image"
  sizes="100vw" 
  srcset="/_next/image?url=이미지경로.png&w=640&q=75 640w, 
          /_next/image?url=이미지경로.png&w=750&q=75 750w, 
          /_next/image?url=이미지경로.png&w=828&q=75 828w, 
          /_next/image?url=이미지경로.png&w=1080&q=75 1080w, 
          /_next/image?url=이미지경로.png&w=1200&q=75 1200w, 
          /_next/image?url=이미지경로.png&w=1920&q=75 1920w, 
          /_next/image?url=이미지경로.png&w=2048&q=75 2048w, 
          /_next/image?url=이미지경로.png&w=3840&q=75 3840w"
  src="/_next/image?url=이미지경로.png&w=3840&q=75"
  style="color:transparent;width:100%;height:auto"
  loading="lazy" 
  width="2000" 
  height="1520"
/>

이를 통해 브라우저는 srcset에 정의된 여러 버전의 이미지들 중 가장 적합한 것을 선택해 화면에 표시한다. 예를 들어 작은 화면에서는 작은 이미지(640w)를, 큰 화면에서는 큰 이미지(1920w)를 보여준다.

또한 당연하게도, 작은 이미지는 큰 이미지보다 파일 크기도 적은 용량을 차지한다. 따라서 네트워크와 관련된 오버헤드를 줄이는데도 도움이 된다.

그런데 잘 보면 이미지를 새로 불러올 때, 잠깐 화면이 깜빡이는 현상이 있다. 이는 이미지가 아직 다 로드 되지 않아서 이미지 컨테이너 요소의 배경색만 보이게 되고 있기 때문이다. 아래에서 설명할 "플레이스홀더"를 활용하면 이 문제를 개선할 수 있다.


Placeholder Blur

placeholder 속성

이미지가 로딩 되는 동안 임시로 표시할 내용을 정의할 수 있도록 하는 속성이다. "empty"(기본 값), "blur", "data:image/..." 중 하나의 값을 가진다.

이 중에서 알아볼 속성 값은 placeholder="blur" 인 경우이다.

  • 이미지를 다 불러올 때 까지 blurDataURL 속성의 내용이 대신해서 보여진다.
  • 만약 이미지를 정적 임포트로 불러오고 있고 포맷이 .jpg, .png, .webp, .avif 중 하나 인 경우 해당 속성이 자동으로 "data:image/..."와 같이 채워진다.
  • 그렇지 않은 경우 해당 속성에 base64로 인코딩 된 이미지를 전달해야 한다.

로딩 중 블러 효과 추가하기

placeholder 속성을 활용해서 아까의 예제와 같이 깜빡이는 현상을 개선시켜 보자. 예제는 정적 방식으로 이미지를 가져오고 있으므로 기존 코드에서 단 한줄을 추가해 주었다.

<Image
  alt="Hero image"
  src={heroImage}
  sizes="100vw"
  style={{
    width: '100%',
    height: 'auto',
  }}
  placeholder="blur" // 추가한 부분
/>

극명한 테스트 효과를 위해 네트워크 탭에서 Disable Cache 및 데이터 다운로드 속도를 제한시키고 진행하였다.

이전과 같이 이미지가 아직 완전히 로드 되지 않았을 때, 배경색이 그대로 보여지게 하는 것 보다는 훨씬 화면 전환이 부드러워 보이는 효과가 있다.


Fill Container

fill 속성

만약 이미지의 크기(너비와 높이)를 모르고 있는 경우 어떻게 하면 좋을까? 그럴 때 도움이 되는 속성이다.

해당 속성 값이 true이면, 이미지 사이즈를 부모 요소에 따라 자동으로 조정되도록 만든다. 이를 따라 부모 요소를 따라 가득 채워지도록 확장하거나 축소시킬 수 있으며, 이와 같은 동작은 반응형 디자인에 유용하게 활용될 수 있다.

또한 object-fit 속성이나 object-position과 함께 사용하여, 이미지가 해당 공간을 채우는 방식을 직접 정의할 수도 있다.

이미지를 컨테이너에 맞추기

fill 속성을 활용하려면 부모 요소(컨테이너)가 다음의 요구 사항을 지켜야 한다.

  • 요소에 display: "block" 지정
  • 요소에 position: "relative", position: "fixed", position: "absolute" 중 하나를 지정
// Tailwind CSS 사용하여 작성
<div className="m-4 flex h-96 gap-4 border-4 border-blue-500 p-4">
  <div className="relative w-1/3 border-4 border-green-500 bg-green-100 p-4">
    <Image
      alt="Image 1"
      src={myImage}
      sizes="100vw"
      fill
      style={{ objectFit: 'cover' }}
    />
  </div>
  <div className="relative w-1/3 border-4 border-green-500 bg-green-100 p-4">
    <Image
      alt="Image 2"
      src={myImage}
      sizes="100vw"
      fill
      style={{ objectFit: 'contain' }}
    />
  </div>
  <div className="relative w-1/3 border-4 border-green-500 bg-green-100 p-4">
    <Image
      alt="Image 3"
      src={myImage}
      sizes="100vw"
      fill
      style={{ objectFit: 'none' }}
    />
  </div>
</div>

화면에 3개의 이미지를 가져오고 있는데, 모두 widthheight 값을 지정하지 않았는데도 불구하고 제대로 보여진다.
그럼 objectFit 속성값 별 각각의 결과가 어떻게 다른지 비교해 보자.

  • "cover": 종횡비를 유지하며, 컨텐츠의 박스를 가득 채운다. 가로세로 비율이 일치하지 않으면 일부분이 잘려나간다.
  • "contain": 종횡비를 유지하며, 이미지가 컨텐츠 내부에 완전히 들어갈 수 있게 크기를 조정한다.
  • "none": 크기를 조정하지 않는다.

Adjust Image quality

quality 속성

이미지 압축 품질을 설정하는 데 사용되는 속성으로, 최적화 수치를 결정짓기 위해 1-100 사이의 정수 값을 가진다.
기본 값은 75이며, 최대 값인 100을 가지면 파일 크기가 가장 큰 대신 최고 품질의 이미지를 제공할 수 있다.

해당 속성을 활용하여 별도의 외부 도구 없이도 이미지 품질을 손쉽게 조절할 수 있다.

다만 이미지 품질을 너무 낮추게 되면 사용자가 육안으로 느낄 수 있는 품질 저하 현상이 일어날 수도 있으니 적절한 품질을 선택하는 것이 중요하다.

수치 별 이미지 품질 비교

아래 사진은 동일한 이미지를 3번 가져와서 보여주는데, quality 값을 각각 100/75/20 으로 서로 다르게 설정했을 때의 결과이다. 각 수치에 따라 이미지 용량이 변하는 걸 확인할 수 있다.


Image Priority

priority 속성

특정 이미지를 높은 우선순위로 간주해 사전 로드(Preload) 하도록 설정하는 속성이다. (우선적으로 로드)
해당 속성이 true 값을 가지면, 이미지에 대해 지연 로딩(Lazy loading)이 비활성화 된다.

개발 환경에서 이 속성에 대해 전혀 고려하지 않고 있다가 보면 콘솔에서 아래와 같은 경고 메시지를 확인할 수 있다.

warn-once.js:16 
Image with src "이미지경로" was detected as the Largest Contentful Paint (LCP). 
Please add the "priority" property if this image is above the fold.
Read more: https://nextjs.org/docs/api-reference/next/image#priority

무슨 의미일까? 메시지를 읽어 보면 '특정 이미지가 Largest Contentful Paint (LCP) 요소로 감지되었음'을 알려주고 있다. 이는 이미지가 페이지 로딩 성능 측정 시 중요한 역할을 하며, 이 이미지에 priority 속성을 추가해 빠르게 로드될 수 있도록 설정하라는 권장 사항을 담고 있다.

따라서 해당 경고를 해소하려면 <Image> 컴포넌트에 간단히 priority 속성을 전달하면 된다.

Largest Contentful Paint (LCP) 알아보기

LCP는 페이지 로드 성능 시 중요한 지표로, 사용자가 페이지를 로드할 때 뷰포트 내에서 가장 큰 컨텐츠 요소가 완전하게 표시되는 데 걸리는 시간을 측정한다.

주로 페이지에서 많은 공간을 차지하는 큰 텍스트 블록, 비디오, 이미지 등이 LCP 와 관련된 요소에 해당한다.

이렇게 페이지 내의 주요 컨텐츠가 사용자에게 얼마나 빨리 표시되는지를 기준으로 측정하므로 잘 활용하면 사용자 경험에 중요한 영향을 미칠 수 있다. LCP 요소가 좋은 값을 가지고 있다면, 사용자는 페이지가 빠르게 로드되었다고 느끼게 되는 효과가 있다.
빠른 LCP는 그만큼 페이지 로드가 빠르다는 신호이며, 이는 사용자 만족도와 SEO 성능에도 긍정적인 영향을 준다.

*더 알아보기: https://nextjs.org/learn-pages-router/seo/web-performance/lcp

References

profile
배우고 익힌 것을 나만의 언어로 정리하는 공간 ..🛝

0개의 댓글