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

실제로 Next.js에서 제공하는 Image 컴포넌트를 쓰지않고 img 태그를 쓰면 경고를 확인 할 수 있습니다.

“Warning: Using ‘img’ could result in slower LCP …

동일한 이미지를 img 태그와 next/image로 각각 렌더링한 결과, format 변환과 로드 시간에서 약 2배의 성능 차이를 확인할 수 있습니다.

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-CacheHIT로 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 사용 예시와 렌더링 결과

기본적으로 css의 size-adjust 속성을 사용하여 CLS를 방지할 수 있습니다.

next/font/google

  • CLS 방지를 위해 adjustFontFallback 옵션을 사용하고 default는 True입니다.

  • 폰트는 배포에 포함되어 배포와 동일한 도메인에서 제공됩니다. 브라우저에서 Google에 요청을 보내지 않습니다.(외부 네트워크 요청을 제거)

여러 단어로 구성된 폰트 이름에는 밑줄(_)을 사용하세요. (예: Roboto Mono -> Roboto_Mono)

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)들이 생성되는 이슈

  • 이미지 사이즈가 고정되어 있는 경우 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