Next.js Image/Font 최적화의 내부 동작 원리와 주의사항
Next.js는 애플리케이션의 속도와 Core Web Vitals를 향상시키기 위한 다양한 내장 최적화 기능을 제공합니다. 특히 next/image, next/font가 최적화를 해준다고 알려져있는데요. 기본적으로 개발자의 최소설정으로 최적화를 해주는건 맞지만 항상 옳은 건 아닐 수 있다고 생각하는데요. Next.js는 개발자의 고민을 어떻게 해결해 주고 있는지가 궁금했습니다.
Next.js의 이미지 최적화

이미지는 일반 웹사이트에서 큰 부분을 차지하며, 웹사이트의 LCP 성능에도 영향을 줄 수 있는데요. 공식문서에서는 Next.js의 이미지 컴포넌트는 HTML <img>
요소를 확장하여 자동 이미지 최적화 기능을 제공하고 있습니다.
각 장치에 적합한 크기의 이미지를 자동으로 제공하고, WebP 및 AVIF와 같은 최신 이미지 형식을 사용합니다.
이미지가 로드될 때 CLS를 자동으로 방지합니다.
네이티브 브라우저 지연 로딩을 사용하여 이미지가 뷰포트에 들어올 때만 로드되며, 선택적 블러 업 플레이스홀더를 제공합니다.
이미지 리사이징 지원합니다(원격 서버 포함)
img
VS next/image
img
VS next/image

동일한 이미지를 img
태그와 next/image
로 각각 렌더링한 결과, format 변환과 로드 시간에서 약 2배
의 성능 차이를 확인할 수 있습니다.
next/image
사용 예시와 렌더링 결과
next/image
사용 예시와 렌더링 결과같은 조건에서 두 이미지를 렌더링해보았는데요
<img
src="/test.png"
width={100}
height={100}
alt="logo"
className="w-10 h-10" />
<Image
src="/test.png"
width={100}
height={100}
alt="logo"
className="w-10 h-10" />

렌더링 후 이미지 경로가 다르게 나오는 것을 알 수 있습니다.
next/image
의 경우 /_next/image
라우트뒤에 파일 경로(url
)와 width(w
), quality(q
, default: 75)를 포함하고 있습니다. 눈 여겨볼만한 부분은 width값이 입력한 100
이 아니라 srcset
에 맞춘 breakpoint에 따라 256으로 변환되어 나오고 있습니다.
// next15 (image-loader source 일부에서 변경된 URL 생성코드를 볼 수 있습니다.)
function defaultLoader(param) {
...
return config.path + "?url=" + encodeURIComponent(src) + "&w=" + width + "&q=" + q + (src.startsWith('/_next/static/media/') && process.env.NEXT_DEPLOYMENT_ID ? "&dpl=" + process.env.NEXT_DEPLOYMENT_ID : '');
}
로컬 이미지 사용
로컬 이미지는 프로젝트의 public
폴더에 저장되며 경로를 통해 쉽게 접근할 수 있습니다.
원격 이미지 사용
원격 이미지는 next.config.js
에 도메인을 등록한 후 사용할 수 있습니다.
// next.config.js
module.exports = {
images: {
domains: ['cdn.example.com'],
},
}
이미지 최적화 시점과 이미지 재사용
.next/cache/images
폴더에 첫 요청 시 캐시 파일이 생성되는데요. 이후 요청의 경우에는 캐시파일을 가져오기때문에 더 빠른 속도로 재사용하고 있습니다.
재사용 시 X-Nextjs-Cache
가 HIT
로 header에서도 확인가능합니다

모든 이미지를 최적화하는가?
벡터 기반인 svg와 같은 경우에는 기본적으로 최적화 작업에서 빠져있습니다.(unoptimized
)
// next15 (image-loader source 일부)
if (isDefaultLoader && !config.dangerouslyAllowSVG && src.split('?', 1)[0].endsWith('.svg')) {
// Special case to make svg serve as-is to avoid proxying
// through the built-in Image Optimization API.
unoptimized = true;
}
다만, dangerouslyAllowSVG
옵션을 통해 최적화과정에 포함시킬 수 있는데요. 그런 경우에는 XSS 공격을 함께 고려해주는 것이 좋습니다.

// next.config.js
module.exports = {
images: {
dangerouslyAllowSVG: true,
contentSecurityPolicy: "default-src 'self'; img-src 'self' data: https:;"
},
}
이미지 최적화
lazy loading
Intersection Observer이용해서 data-src를 사용
img태그의 loading=lazy 속성 사용
next/image > Image 컴포넌트에서 자동으로 적용
이미지 사이즈 최적화
img태그의 srcSet을 미리 적용
next/image > Image컴포넌트에서 적용하지 않아도 next.config와 props에 따라 srcSet 목록이 변경
// next.config.js module.exports = { images: { deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], } }
이미지 포맷 최적화
picture 태그 > source 태그를 이용해서 fallback과 최적화 포맷을 각각 제공
next/image > Image 컴포넌트에서 next.config image.format 설정에 따라 제공(단, svg는 제외, 기본 포맷은 webp)
placeholder 제공
기존에는 직접 구현
next/image > Image 컴포넌트는 로컬은 build타임에 생성, 원격 이미지는 data url(base64) props 설정으로 제공
원격 이미지인 경우 CDN(물리적 거리의 한계를 극복하기 위해 사용자와 가까운 곳에 컨텐츠 서버 두는 방법)은 processing한 CDN
Next.js의 폰트 최적화
Next.js에서는 next/font
라이브러리를 통해 웹 폰트의 로딩 성능 및 CLS를 개선할 수 있습니다.
폰트 파일을 자동으로 최적화하고 미리 로드할 수 있습니다. 이는 폰트 로딩 시간을 줄여 LCP를 개선하는 데 기여합니다.
외부 리소스에 의존하지 않고 폰트를 자체 호스팅함으로써 로딩 시간을 단축하고, 사용자의 데이터 프라이버시도 보호합니다.
폰트의
font-display
옵션을 설정하여, 폰트 로드가 지연되는 동안 사용자에게 텍스트를 어떻게 보여줄지 제어할 수 있습니다. 일반적으로swap
옵션을 사용하여 폰트가 로드되기 전까지 시스템 폰트를 보여주고, 로드가 완료되면 즉시 웹 폰트로 전환합니다.최신 폰트 포맷을 사용하여 파일 크기를 최소화하고 로딩 속도를 최적화합니다.
next/font
사용 예시와 렌더링 결과
next/font
사용 예시와 렌더링 결과기본적으로 css의 size-adjust
속성을 사용하여 CLS를 방지할 수 있습니다.
next/font/google
next/font/google
CLS 방지를 위해
adjustFontFallback
옵션을 사용하고 default는True
입니다.폰트는 배포에 포함되어 배포와 동일한 도메인에서 제공됩니다. 브라우저에서 Google에 요청을 보내지 않습니다.(외부 네트워크 요청을 제거)
next/font/local
next/font/local
CLS 방지를 위해
adjustFontFallback
옵션을 사용하고 default는Arial
(Arial, Times New Roman, False)입니다.
렌더링 결과
기존 Fallback Font의 size-adjust 속성을 조정하기 때문에 Fallback 폰트와 선택한 폰트 사이의 크기 차이가 발생하지 않고, 이에 따라 Layout Shift가 발생하지 않고 있음을 확인할 수 있습니다.
sizeAdjust값을 계산하여 zero layout shift를 달성할 수 있도록 조치합니다.
// next15 (font-utils 일부)
function calculateSizeAdjustValues(fontName) {
const fontKey = formatName(fontName);
const fontMetrics = capsizeFontsMetrics[fontKey];
let { category, ascent, descent, lineGap, unitsPerEm, xWidthAvg } = fontMetrics;
const mainFontAvgWidth = xWidthAvg / unitsPerEm;
const fallbackFont = category === 'serif' ? _constants.DEFAULT_SERIF_FONT : _constants.DEFAULT_SANS_SERIF_FONT;
const fallbackFontName = formatName(fallbackFont.name);
const fallbackFontMetrics = capsizeFontsMetrics[fallbackFontName];
const fallbackFontAvgWidth = fallbackFontMetrics.xWidthAvg / fallbackFontMetrics.unitsPerEm;
let sizeAdjust = xWidthAvg ? mainFontAvgWidth / fallbackFontAvgWidth : 1;
ascent = formatOverrideValue(ascent / (unitsPerEm * sizeAdjust));
descent = formatOverrideValue(descent / (unitsPerEm * sizeAdjust));
lineGap = formatOverrideValue(lineGap / (unitsPerEm * sizeAdjust));
return {
ascent,
descent,
lineGap,
fallbackFont: fallbackFont.name,
sizeAdjust: formatOverrideValue(sizeAdjust)
};
}
폰트 최적화
fallback 구현 FOIT(보이지 않거나), FOUT(늦게 적용되거나)
cssom 트리시점에서 다운로드 시작되어서 paint에 완료가 안된 경우 문제
preload로 해결
의도적으로 FOUT(loading동안 fallback font로 대체,
font-display: swap
)CSS Font Loading Module
Font Face Observer 비동기방식(서드파티, 구버전 라이브러리 지원)
폰트 파일 용량 최적화
폰트 파일의 용량을 줄여 해결(
woff2
, fallback으로ttf
)subset 폰트(실생활에서 쓰는 단어 위주)
unicode-range(일부만 선택)
프로덕션에서 주의사항
srcSet에 너무 많은 이미지(next/image
)들이 생성되는 이슈
next/image
)들이 생성되는 이슈이미지 사이즈가 고정되어 있는 경우 fill=False, width/height 값을 지정합니다.
2개의 srcSet만 생성합니다.
srcSet를 변경하는 방법
// next.config.js module.exports = { images: { imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], }, };
마무리
Next.js는 이미지와 폰트 최적화를 통해 개발자의 부담을 크게 덜어주는 것은 맞습니다.
이미지 최적화의 경우
대부분의 경우 성능 향상을 가져다주지만, SVG나 아주 작은 이미지의 경우 오히려 오버헤드가 될 수 있습니다
srcSet 생성으로 인한 불필요한 이미지들이 많이 생성될 수 있어 설정 조정이 필요합니다
폰트 최적화의 경우
CLS 방지와 자체 호스팅을 통한 성능 개선이 뛰어나지만, 폰트 종류와 사용 패턴에 따라 설정을 조정해야 합니다
Next.js가 제공해주는 최적화 부분을 이해하고 기존에 활용할 수 있었던 최적화와 같이 프로젝트 특성에 맞게 적절히 적용해보는 것이 중요할 것 같습니다.
참고
Last updated