next/image는 NextJS에서 제공하는 Image 최적화 라이브러리입니다. NextJS의 프레임워크를 활용하여 다양한 기능을 쓰기 쉽게 제공하고 있는데, 공식 문서에서는 기본적인 기능과 사용법에 대해서만 설명하고 있습니다. 이 포스트에서는 공식 문서에서 설명해주지 않는 디테일한 next/image의 동작 원리를 NextJS의 소스코드를 통해 다뤄보겠습니다.
<div style="text-align:center"><span style="color:grey">ㅁㅁㅁ</span></div>
라이브러리의 기본적인 사용법은 간단합니다. 리액트의 <img>
태그와 동일하게 src prop에 해당하는 이미지의 경로를 추가해주면 됩니다.
import React, { ReactElement } from 'react';
import Image from 'next/image';
import moutainImage from '../../public/mountains.jpeg'
const ImageDemoPage = (): ReactElement => {
return (
<Image
src={moutainImage}
/>
);
};
export default ImageDemoPage;
리모트 이미지 같은 경우, 주의할 점이 몇가지 있습니다.
첫번째로 next.config.js
파일에 remote 이미지 url에 사용되는 도메인을 추가해야 합니다. 두번째로는 width와 height prop을 명시적으로 지정해줘야 합니다.
//next.conf.js
const nextConfig = {
images: {
domains: ['assets.vercel.com'],
}
}
const ImageDemoPage = (): ReactElement => {
return (
<Image
src="https://assets.vercel.com/image/upload/v1538361091/repositories/next-js/next-js-bg.png"
width={1500}
height={500}
/>
);
};
이렇게 이미지를 렌더링 한 후 HTML Dom 트리에서 확인해보면 여러 랩핑 dom 및 attribute들과 함께 src에 /_next/image?url=
로 시작하는 경로가 들어가 있음을 알 수 있습니다. 이상하게 생긴 이 url이 NextJS가 이미지 최적화를 진행하고 있다는 증거라고 볼 수 있습니다.
NextJS는 SEO, SSR등의 기능을 수행하기 위해 NodeJS 서버를 띄웁니다. 이미지 최적화 작업도 이 Serverside의 소스코드를 통해 1차적으로 진행하고 있습니다. 브라우저에서 요청한 /_next/image?url=
로 시작하는 리퀘스트는 NextJS의 Serverside 프레임웍을 따라가다 /package/next/server
의 imageOptimzer
function에서 최적화 작업을 진행하게 됩니다. (해당 경로는 NextJS 깃헙의 소스코드 기준 경로이므로 실제 빌드되는 .node_module의 경로와 다를 수 있습니다.)
해당 function 내에서 이미지 리사이징 및 최적화 라이브러리들을 통해 최적화를 진행하게 됩니다.
그 순서를 요약해서 정리하면,
Image를 최적화하기 위에서 먼저 해당 Image의 버퍼를 가져오게 됩니다. 로컬 이미지는 빌드시 서버에 업로드된 static 파일로부터,
remote 이미지는 해당 url로 요청을 날려 buffer를 가져옵니다.
최적화가 필요하지 않은 svg파일이나 최적화가 어려운 animated 이미지 같은 경우 최적화 과정 없이 이미지를 리턴하도록 설계되어 있습니다.
// image-optimizer.ts
// VECTOR_TYPES = ['image/svg+xml']
// ANIMATABLE_TYPES = ['image/webp', 'image/png', 'image/gif']
const vector = VECTOR_TYPES.includes(upstreamType)
const animate =
ANIMATABLE_TYPES.includes(upstreamType) && isAnimated(upstreamBuffer)
if (vector || animate) {
return { buffer: upstreamBuffer, contentType: upstreamType, maxAge }
}
NextJS는 최적화를 위해 sharp와 squoosh라는 두가지 라이브러리를 사용하고 있습니다. 이유는 모르겠지만 NextJS에서는 production 환경에서는 sharp를 권장하고 있고, 해당 라이브러리는 유저가 따로 package.json에 추가하여 설치해야 합니다. 해당 라이브러리가 존재하지 않을 때는 사용자에게 warning 메세지를 띄우고 squoosh 라이브러리를 사용합니다.
해당 라이브러리에서는 브라우저에서 요청한 width와 quality에 따라 이미지를 리사이징 해서 최적화를 진행합니다.
// image-optimizer.ts
//sharp
...
const transformer = sharp(upstreamBuffer)
...
} else if (contentType === WEBP) {
transformer.webp({ quality })
} else if (contentType === PNG) {
transformer.png({ quality })
} else if (contentType === JPEG) {
transformer.jpeg({ quality })
}
...
//squoosh
...
} else if (contentType === WEBP) {
optimizedBuffer = await processBuffer(
upstreamBuffer,
operations,
'webp',
quality
)
} else if (contentType === PNG) {
optimizedBuffer = await processBuffer(
upstreamBuffer,
operations,
'png',
quality
)
} else if (contentType === JPEG) {
optimizedBuffer = await processBuffer(
upstreamBuffer,
operations,
'jpeg',
quality
)
}
...
next/image 에서는 캐싱 역시 지원하고 있습니다. 이미지 요청시에 해당 이미지가 캐시에 존재할 경우, 캐시에 저장된 이미지를 리턴합니다. 캐시에 존재하지 않을 경우, 최적화 작업을 진행한 후 지정된 캐시 폴더에 이미지를 캐싱하게 됩니다.
여기서 주의할 점은 캐시키를 구성하는 요소에 파일 경로 뿐만 아니라 width, quality 값도 포함하고 있기 때문에 동일한 이미지라도 최적화 파라미터가 달라지게 된다면 캐싱 작업을 다시 진행하게 됩니다.
next/image가 강력한 이미지 최적화 툴인것은 사실입니다. 이미지 사이즈 최적화, Cache, Lazy loading, UI Disarray 방지 등의 기능은 개발을 편하게 만들고 대부분의 상황에서 퍼포먼스를 향상시킬 수 있습니다. 그러나 최적화를 위해 많은 과정을 거치기 때문에 적합하지 않은 특정 상황(ex. 사용자가 적어 캐싱이 불필요한 경우, 사용되는 대부분의 이미지가 svg인 경우, 새로운 이미지를 자주 렌더링해야 하는 경우 등등) 에서는 퍼포먼스를 저해하는 요소가 될 수 있습니다. 따라서, 동작 원리를 이해하고 적합한 상황에서 적절하게 사용하는 것이 필요할 것 같습니다.