웹 사이트에서 있어서 이미지라는 요소는 아마 가장 크고 핵심적인 부분을 차지하는 중요한 요소이지 않을까 싶다.
이미지를 적절히 활용하면 사용자를 더 오래 머물 수 있도록 만들고, 사용자의 참여도를 더욱 높일 수 있다.
하지만, 적절하게 최적화 되지 않거나 무분별하게 사용된 이미지는 오히려 역효과를 불러 일으킬 가능성이 있다.
예시로 포맷이나 압축에 대한 고려 없이 고해상도의 이미지를 불러와 페이지 로딩 시간이 길어진다거나, 불러올 때 이미지 크기 관련(width/height) 정보가 예측되지 않아 레이아웃 시프트 현상이 발생하는 것과 같은 경우가 그렇다.
따라서 오늘은 이렇게 웹에서 사용자 만족도를 높일 수 있도록, 이미지를 효과적으로 사용하는 방법에 대해 알아보자.
(참고: 다음 내용은 Next.js v13 환경을 대상으로 작성되었습니다.)
본격적으로 Next.js 에서 이미지를 최적화 하는 방법에 대해 알아보기 전에, 먼저 이미지와 같은 정적 자산을 프로젝트에서 어떻게 관리하는지에 대해 짚고 넘어가 보려 한다.
Next.js 에서 정적 애셋은 주로 프로젝트 루트의 /public 경로에서 관리된다. '정적'이라는 단어에서 추측해볼 수 있듯이, 일반적으로 변경되지 않을 애셋들을 이곳에 두고 사용한다.
이미지 파일
: jpg, png, gif, svg 등 포맷의 로고, 배너, 아이콘, 배경 이미지 등글꼴 파일
: woff, woff2, ttf 등문서 파일
: pdf, xlsx, doc, txt 등미디어 파일
: mp3, mp4, wav, mov 등JSON 파일
: 웹 사이트에서 사용하는 정적 데이터 파일src='/image.png'
정리하면, 정적 애셋은 애플리케이션에서 변경되지 않는 파일들을 말하고 요청이 있을 때마다 동일한 형태로써 제공된다.
따라서 여러 번 재사용 할 수 있고, 로컬 캐싱을 통해 빠른 속도로 제공된다는 장점이 있는 웹 사이트 리소스의 일종이다.
그럼 본격적으로 이미지 최적화라는 과정이 왜 필요한지에 대해 생각해보자.
만약 일반적인 HTML에서 이미지를 화면에 보여주어야 한다면, 간단하게는 다음과 같은 코드를 작성할 수 있다.
<img src="/hero.png" alt="웹사이트의 배너 이미지입니다" />
그런데 만약 다음과 같은 요구사항이 생긴다면 이에 대해 어떻게 처리할 것인가?
반응형 이미지
: 이미지가 다양한 화면 사이즈에 대해 반응형으로 보여진다. (화면 크기에 맞게 자동 조정)다양한 장치 크기에 맞는 이미지
: 다양한 장치에 대해 이미지 크기를 지정한다.레이아웃 시프트 방지
: 이미지를 불러올 때 레이아웃이 바뀌는 것을 방지한다.Lazy loading
: 뷰포트 외부에 있는 이미지에 대해 Lazy load를 적용한다.이러한 요구사항은 바로 이미지 최적화의 일부분이다. 모두 웹에서 이미지를 렌더링할 때 효율적이고, 빠르고, 안정적으로 사용자에게 제공하기 위한 기법에 해당한다.
이미지 최적화라는 주제 그 자체로도 하나의 전문 분야로 간주될 수 있을 만큼 웹 개발에 있어서 그만큼 필수적인 개념 중 하나라고도 할 수 있다.
이미지를 사용할 때 여러 가지 방식으로 최적화를 수행할 수 있다. 각각의 기법이 가장 효과적일 수 있는 상황은 때에 따라 다르니 각 방식의 특징에 대해 알아 두면 도움이 될 것 같다. 다음은 대표적인 이미지 최적화 기법이다.
srcset
, sizes
속성 및 <picture>
요소: 다양한 해상도와 크기에 맞는 이미지 제공Next.js에는 이러한 최적화를 수동으로 구현하는 대신 자동으로 이미지를 최적화시킬 수 있는 기능을 제공하고 있다. 아래에서 해당 컴포넌트에 대해 본격적으로 알아보자.
next/image
모듈<Image>
컴포넌트지난 포스트에서 알아 봤던 폰트 최적화처럼, 이미지 최적화 역시 별도의 모듈을 통해서 제공된다.
next/image
모듈 안에 있는 <Image> 컴포넌트는 HTML <img> 태그의 확장으로써, 자동으로 이미지 최적화와 관련된 몇 가지 기능을 제공하고 있다.
.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️⃣번 방식:
width
, height
속성을 전달하지 않아도 컴포넌트가 자동으로 너비와 높이 인식 가능2️⃣번 방식:
width
, height
속성을 제공하는 게 필수원격에 있는 이미지를 불러올 때는 src
속성에 URL 문자열을 전달하여 불러올 수 있다.
로컬 이미지와는 달리 원격 이미지는 빌드 과정 중에서 액세스 할 수 없으므로, width
및 height
를 필수적으로 명시해야 한다.
올바른 종횡비(가로세로 비율)로 이미지를 보여주기 위해서는 알맞은 너비와 높이 지정이 필요하다.
또한 허용된 이미지 경로만 불러오도록 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 가 제공하는 자동 이미지 최적화 기능 중 일부분을 확인해볼 수 있다.
width
및 height
: 종횡비에 맞게 width
를 기준으로 height
가 자동으로 조정된다. ("Rendered size" 부분)loading
: 기본적으로 lazy loading이 적용된다. (뷰포트에 보이기 전까지 이미지 로드 지연)srcset
: 브라우저에 사용자 디스플레이 해상도에 가장 적합한 이미지를 제공하기 위해 일반 해상도(x1) 및 고해상도(2x) 이미지를 제공하고 있다.Next.js에 내장된 Image Optimization API를 사용하는 대신, 커스텀 로더를 통해 이미지를 제공할 수 있다.
이 설정은 클라우드 공급자와 같이 외부 이미지 최적화 서비스를 사용해 이미지를 최적화하는 방법을 직접 정의하려는 경우 유용하다.
이미지 로더는 기본적으로 이미지의 URL을 생성하는 일을 한다. src
속성을 통해 전달된 URL을 기반으로 여러 크기의 이미지를 요청할 수 있도록 다양한 URL을 생성한다. 이를 통해 사용자 뷰포트에 맞춰 적절한 크기의 이미지를 제공하고, 최적화된 포맷으로 변환하고, 품질 조정 등을 수행하는 것과 같은 작업이 가능해진다.
또한, 원격 이미지인데도 불구하고 로컬 이미지를 가져올 때처럼 부분 URL을 전달해 이미지를 가져올 수 있다. (src="/images/bg.png"
처럼)
loderFile
에 자신만의 커스텀 로더를 정의loader
prop을 전달다음은 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"
/>
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
속성이미지가 로딩 되는 동안 임시로 표시할 내용을 정의할 수 있도록 하는 속성이다. "empty"
(기본 값), "blur"
, "data:image/..."
중 하나의 값을 가진다.
이 중에서 알아볼 속성 값은 placeholder="blur"
인 경우이다.
blurDataURL
속성의 내용이 대신해서 보여진다."data:image/..."
와 같이 채워진다.placeholder
속성을 활용해서 아까의 예제와 같이 깜빡이는 현상을 개선시켜 보자. 예제는 정적 방식으로 이미지를 가져오고 있으므로 기존 코드에서 단 한줄을 추가해 주었다.
<Image
alt="Hero image"
src={heroImage}
sizes="100vw"
style={{
width: '100%',
height: 'auto',
}}
placeholder="blur" // 추가한 부분
/>
극명한 테스트 효과를 위해 네트워크 탭에서 Disable Cache 및 데이터 다운로드 속도를 제한시키고 진행하였다.
이전과 같이 이미지가 아직 완전히 로드 되지 않았을 때, 배경색이 그대로 보여지게 하는 것 보다는 훨씬 화면 전환이 부드러워 보이는 효과가 있다.
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개의 이미지를 가져오고 있는데, 모두 width
나 height
값을 지정하지 않았는데도 불구하고 제대로 보여진다.
그럼 objectFit
속성값 별 각각의 결과가 어떻게 다른지 비교해 보자.
quality
속성이미지 압축 품질을 설정하는 데 사용되는 속성으로, 최적화 수치를 결정짓기 위해 1-100 사이의 정수 값을 가진다.
기본 값은 75이며, 최대 값인 100을 가지면 파일 크기가 가장 큰 대신 최고 품질의 이미지를 제공할 수 있다.
해당 속성을 활용하여 별도의 외부 도구 없이도 이미지 품질을 손쉽게 조절할 수 있다.
다만 이미지 품질을 너무 낮추게 되면 사용자가 육안으로 느낄 수 있는 품질 저하 현상이 일어날 수도 있으니 적절한 품질을 선택하는 것이 중요하다.
아래 사진은 동일한 이미지를 3번 가져와서 보여주는데, quality
값을 각각 100/75/20 으로 서로 다르게 설정했을 때의 결과이다. 각 수치에 따라 이미지 용량이 변하는 걸 확인할 수 있다.
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
속성을 전달하면 된다.
LCP는 페이지 로드 성능 시 중요한 지표로, 사용자가 페이지를 로드할 때 뷰포트 내에서 가장 큰 컨텐츠 요소가 완전하게 표시되는 데 걸리는 시간을 측정한다.
주로 페이지에서 많은 공간을 차지하는 큰 텍스트 블록, 비디오, 이미지 등이 LCP 와 관련된 요소에 해당한다.
이렇게 페이지 내의 주요 컨텐츠가 사용자에게 얼마나 빨리 표시되는지를 기준으로 측정하므로 잘 활용하면 사용자 경험에 중요한 영향을 미칠 수 있다. LCP 요소가 좋은 값을 가지고 있다면, 사용자는 페이지가 빠르게 로드되었다고 느끼게 되는 효과가 있다.
빠른 LCP는 그만큼 페이지 로드가 빠르다는 신호이며, 이는 사용자 만족도와 SEO 성능에도 긍정적인 영향을 준다.
*더 알아보기: https://nextjs.org/learn-pages-router/seo/web-performance/lcp