학습 자료

이미지와 폰트 — Next 16 에서 바뀐 기본값들


Article

페이지에서 가장 무거운 것은 대개 자바스크립트가 아니다. 이미지와 폰트다.

<img> 태그 하나를 넣어본다.

<img src="/hero.jpg" alt="히어로" />

디자이너가 준 원본은 4000픽셀 폭에 3MB다. 모바일에서도 그 3MB를 전부 받는다. 그리고 도착하는 순간 아래 있던 글자들이 밀린다. 브라우저는 이미지가 도착하기 전까지 그 자리가 얼마나 클지 몰랐다.

next/image 가 대신 하는 일

import Image from "next/image";

<Image src="/hero.jpg" alt="히어로" width={1600} height={900} />
img 와 Image

<img>

브라우저 기본

  • 원본을 그대로 준다
  • 화면 폭을 모른다
  • 포맷도 원본 그대로
  • 자리를 미리 못 잡는다

<Image>

요청 시 변환

  • 화면 크기에 맞춰 줄인다
  • WebP · AVIF 로 바꾼다
  • 화면 밖이면 나중에 받는다
  • 자리를 미리 잡는다

마지막 항목이 widthheight를 강제하는 이유다. 실제 표시 크기가 아니라 비율을 알려주는 값이다. 브라우저는 그 비율로 빈 자리를 잡아두고 기다린다.

이걸 안 하면 누적 레이아웃 시프트가 생긴다. 사용자가 누르려던 버튼이 이미지가 도착하는 순간 아래로 도망간다.

모바일에서 받는 히어로 이미지
원본 JPG3000KB
리사이즈 + WebP84KB
같은 화면이다. 사용자는 차이를 못 느낀다

크기를 모를 때는 fill

CMS에서 오는 이미지는 크기를 알 수 없다. fill을 쓴다.

<figure style={{ position: "relative", aspectRatio: "16 / 9" }}>
  <Image src={post.cover} alt="" fill sizes="(max-width: 768px) 100vw, 50vw" />
</figure>

fill은 부모를 꽉 채운다. 부모에 position: relative가 있어야 한다. 그리고 부모가 자리를 잡아줘야 하므로 aspectRatio나 높이를 준다.

sizes가 빠지면 경고가 뜬다.

콘솔
  • Image with src "/cover.jpg" has "fill" but is missing "sizes" prop.
  • Please add it to improve page performance.

sizes는 **"이 이미지가 화면에서 얼마나 넓게 보이나"**를 알려주는 값이다. 없으면 브라우저는 100vw로 가정하고 가장 큰 파일을 받는다. 폭의 절반만 차지하는 이미지라면 두 배로 낭비한다.

priority 는 사라졌다

첫 화면에 보이는 큰 이미지는 미리 받아야 한다. 예전에는 priority를 붙였다.

<Image src="/hero.jpg" alt="" width={1600} height={900} priority />

Next.js 16에서 이 prop은 deprecated다. 이름이 하는 일을 정확히 말하지 않았기 때문이다. preload로 바뀌었다.

무엇을 원하나무엇을 쓰나
<head> 에 link 태그를 넣어 미리 받는다preload
lazy loading 을 끈다 (대개 이걸 원한다)loading="eager"
우선순위를 높인다fetchPriority="high"

quality 가 잠겼다

<Image src="/hero.jpg" alt="" width={1600} height={900} quality={90} />

Next 16에서 이 코드는 에러다. 허용된 품질값이 [75] 하나뿐이기 때문이다.

next.config.js에 등록해야 쓸 수 있다.

module.exports = {
  images: {
    qualities: [50, 75, 90],
  },
};

왜 잠갔을까. 이미지 최적화 엔드포인트는 주소로 열려 있다. 아무나 quality 값을 1부터 100까지 바꿔가며 요청하면, 우리 서버가 같은 이미지를 100번 변환하고 100개를 저장한다. 허용 목록을 두면 그 공격이 막힌다.

설정Next 15 까지Next 16
qualities아무 값이나[75] 만 — 직접 등록
minimumCacheTTL60초4시간 (14400초)
priority prop권장deprecated → preload
images.domainsdeprecatedremotePatterns 를 쓴다

외부 이미지는 허락받아야 한다

<Image src="https://images.example.com/a.jpg" alt="" width={800} height={600} />
콘솔
  • Error: Invalid src prop on `next/image`, hostname "images.example.com" is not configured under images in your `next.config.js`

같은 이유다. 아무 주소나 최적화해주면 우리 서버가 남의 이미지 CDN 노릇을 한다.

module.exports = {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "images.example.com", pathname: "/photos/**" },
    ],
  },
};

pathname까지 좁혀둔다. 호스트만 열어두면 그 호스트의 모든 이미지가 통과한다.

로컬 이미지는 import 한다

import hero from "./hero.jpg";

<Image src={hero} alt="히어로" placeholder="blur" />

widthheight를 안 썼다. 빌드할 때 파일을 읽어 크기를 알아냈기 때문이다. 오타로 비율이 틀어질 일이 없다.

placeholder="blur"도 공짜다. Next.js가 작은 흐릿한 버전을 만들어 넣는다. 원격 이미지에서는 blurDataURL을 직접 줘야 한다.

폰트는 구글에 요청을 보내지 않는다

<link href="https://fonts.googleapis.com/...">를 쓰면 사용자의 브라우저가 구글에 접속한다. 느리고, 개인정보가 나간다.

next/font빌드할 때 폰트 파일을 받아 우리 서버에 함께 올린다.

// app/layout.js
import { Noto_Sans_KR } from "next/font/google";

const notoSansKr = Noto_Sans_KR({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-sans",
});

export default function RootLayout({ children }) {
  return (
    <html lang="ko" className={notoSansKr.variable}>
      <body>{children}</body>
    </html>
  );
}

런타임에 구글로 나가는 요청이 없다. 폰트 파일은 우리 도메인에서 온다.

옵션이 하는 일
  1. 01

    variable 폰트면 weight 를 안 쓴다

    굵기 하나하나를 파일로 받는 대신 한 파일이 전 범위를 덮는다. 크기도 작다.

  2. 02

    display: "swap" 이 기본이다

    폰트가 도착하기 전에는 대체 폰트로 글자를 먼저 보여준다. 안 보이는 것보다 낫다.

  3. 03

    대체 폰트의 크기를 자동으로 맞춘다

    adjustFontFallback 이 기본으로 켜져 있다. 폰트가 바뀔 때 글자가 밀리는 것을 줄인다.

  4. 04

    variable 로 CSS 변수를 만든다

    --font-sans 를 Tailwind 나 globals.css 에서 쓴다.

    @theme inline {
      --font-sans: var(--font-sans);
    }
display 값에 따라 첫 글자가 보이는 시각
block
빈 화면폰트로 표시
swap (기본)
대체 폰트로 즉시진짜 폰트로 교체
요청폰트 도착
글자가 한 번 바뀌지만, 안 보이는 것보다는 낫다

폰트를 어디서 부르느냐가 어디까지 미리 받을지를 정한다. 루트 레이아웃에서 부르면 모든 라우트에서 미리 받고, 특정 페이지에서 부르면 그 라우트에서만 받는다.

정리하면

  • <Image>는 크기를 줄이고, 포맷을 바꾸고, 자리를 미리 잡는다. width/height는 비율을 알려주는 값이다.
  • 크기를 모르면 fill을 쓴다. **부모에 position: relative**가 필요하고, sizes를 반드시 준다.
  • priority는 Next 16에서 deprecated다. 그냥 preload로 바꾸지 말고, 대개는 loading="eager"fetchPriority="high"가 맞다.
  • quality의 기본 허용값은 [75] 하나다. 다른 값은 next.config.js에 등록한다.
  • 외부 이미지는 **remotePatterns**에 등록해야 한다. pathname까지 좁힌다.
  • 로컬 이미지는 import 하면 크기와 blur 자리표시자가 공짜다.
  • next/font는 빌드할 때 폰트를 받아 직접 호스팅한다. 구글로 나가는 요청이 없다.
  • 가변 폰트를 쓰면 weight가 필요 없다. display: "swap"이 기본이다.
  • 폰트 함수는 모듈 최상단에서 부른다. 여러 곳에서 쓰면 한 파일에서 부르고 나눠 쓴다.

Part 5의 마지막 강의가 남았다. 지금까지 한 번도 만들지 않은 것이 둘 있다.

11강에서 Server Actions를 배우며 API Route를 만들 필요가 없다고 했다. 하지만 웹훅을 받거나 외부에 JSON을 내려줄 때는 필요하다. 그리고 요청이 페이지에 닿기 전에 가로채는 파일이 있다. 예전 이름은 middleware.ts였는데, Next 16에서 이름이 바뀌었다.

다음 강의에서 그 둘을 만들고, 만든 것을 배포한다.

이미지와 폰트 — Next 16 에서 바뀐 기본값들 — 디코드랩(DCODELAB)