포스트

[이제와서 시작하는 Next.js 마스터하기 #6] 이미지, 폰트, 메타데이터 최적화

[이제와서 시작하는 Next.js 마스터하기 #6] 이미지, 폰트, 메타데이터 최적화

“이미지 한 줄로 자동 최적화?” - Next.js의 Image 컴포넌트는 마법입니다!

🎯 이 글에서 배울 내용

  • next/image로 이미지 최적화
  • next/font로 폰트 최적화
  • 메타데이터로 SEO 향상
  • Open Graph와 Twitter 카드
  • 최적화 후 실제로 확인할 체크리스트

예상 소요 시간: 40분


🖼️ 이미지 최적화 (next/image)

1. 기본 사용법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app/page.js
import Image from 'next/image';

export default function Home() {
  return (
    <div>
      <Image
        src="/hero.jpg"
        alt="히어로 이미지"
        width={1200}
        height={600}
        priority  // 우선 로딩
      />
    </div>
  );
}

자동 최적화:

  • ✅ WebP/AVIF 형식으로 자동 변환
  • ✅ 반응형 이미지 생성
  • ✅ 지연 로딩 (lazy loading)
  • ✅ 블러 placeholder

priority는 모든 이미지에 붙이는 옵션이 아닙니다. 첫 화면에서 가장 중요한 LCP 이미지, 예를 들면 hero 이미지나 상품 대표 이미지에만 사용하세요. 화면 아래쪽 이미지까지 우선 로딩하면 오히려 초기 로딩이 느려질 수 있습니다.

2. 외부 이미지

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        pathname: '/photo-*',
      },
    ],
  },
};

export default nextConfig;

remotePatterns는 최대한 좁게 잡는 편이 좋습니다. hostname: '**'처럼 모든 외부 이미지를 허용하면 의도하지 않은 URL까지 이미지 최적화 대상이 될 수 있습니다.

1
2
3
4
5
6
<Image
  src="https://images.unsplash.com/photo-..."
  alt="Unsplash 이미지"
  width={800}
  height={600}
/>

3. sizes를 꼭 지정해야 하는 경우

fill을 쓰거나 반응형 레이아웃에서 이미지가 차지하는 너비가 화면마다 달라진다면 sizes를 함께 적어야 브라우저가 적절한 이미지 크기를 고릅니다.

1
2
3
4
5
6
7
8
9
10
<div className="relative aspect-[16/9] w-full">
  <Image
    src="/hero.jpg"
    alt="서비스 대시보드 미리보기"
    fill
    priority
    sizes="(max-width: 768px) 100vw, 1200px"
    style={{ objectFit: 'cover' }}
  />
</div>

카드 그리드라면 이렇게 더 좁게 잡을 수 있습니다.

1
2
3
4
5
6
7
<Image
  src={post.cover}
  alt={post.title}
  width={640}
  height={360}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

sizes가 없으면 실제 표시 크기보다 큰 이미지를 내려받는 일이 생깁니다. Lighthouse에서 LCP 이미지가 크다고 나오면 가장 먼저 이 값을 확인하세요.

4. 이미지 선택 기준

상황 권장 방식 이유
본문 안 고정 크기 이미지 width/height 레이아웃 이동을 줄이기 쉽다
카드 썸네일 width/height + sizes 그리드 너비에 맞는 파일을 받는다
hero 배경형 이미지 fill + sizes + priority 첫 화면 LCP를 제어하기 좋다
아이콘, 로고 SVG 일반 img 또는 inline SVG 작은 벡터는 Image 최적화 이점이 적다
사용자 업로드 원본 CDN 변환 또는 저장 시 리사이즈 원본이 너무 크면 런타임 비용이 커진다

이미지 최적화의 핵심은 “무조건 Image 컴포넌트”가 아니라, 브라우저가 필요한 크기의 이미지만 받도록 힌트를 주는 것입니다.

5. Fill 모드 (부모 크기 채우기)

1
2
3
4
5
6
7
8
<div className="relative w-full h-96">
  <Image
    src="/background.jpg"
    alt="배경"
    fill
    style={{ objectFit: 'cover' }}
  />
</div>

6. 블러 Placeholder

1
2
3
4
5
6
7
8
<Image
  src="/profile.jpg"
  alt="프로필"
  width={400}
  height={400}
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,..."  // 작은 블러 이미지
/>

🔤 폰트 최적화 (next/font)

1. Google Fonts 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app/layout.js
import { Inter, Roboto_Mono } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',  // 폰트 로딩 전략
});

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  weight: ['400', '700'],
});

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

장점:

  • ✅ 자동으로 최적화된 폰트 로딩
  • ✅ FOUT (Flash of Unstyled Text) 방지
  • ✅ 자동 self-hosting (Google CDN 의존 없음)

Next.js의 next/font는 Google Fonts도 빌드 시점에 가져와 앱에서 직접 서빙합니다. 그래서 브라우저가 런타임에 Google 서버로 폰트를 요청하지 않아 개인정보와 성능 양쪽에서 이점이 있습니다.

2. 로컬 폰트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app/layout.js
import localFont from 'next/font/local';

const myFont = localFont({
  src: './fonts/MyFont.woff2',
  display: 'swap',
  weight: '400',
});

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

한글 사이트라면 폰트 파일 크기를 꼭 확인하세요. 전체 굵기와 전체 문자 집합을 한 번에 넣으면 JS를 줄여도 폰트가 병목이 될 수 있습니다. 가능하면 필요한 굵기만 고르고, 본문 폰트와 코드 폰트를 분리해 적용하세요.

3. 여러 폰트 조합

1
2
3
4
5
6
7
8
9
10
11
12
import { Inter, Noto_Sans_KR } from 'next/font/google';

const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
const noto = Noto_Sans_KR({ subsets: ['korean'], variable: '--font-noto' });

export default function RootLayout({ children }) {
  return (
    <html lang="ko" className={`${inter.variable} ${noto.variable}`}>
      <body className="font-sans">{children}</body>
    </html>
  );
}
1
2
3
4
5
6
7
8
9
10
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  body {
    font-family: var(--font-inter), var(--font-noto), sans-serif;
  }
}

4. 폰트 적용 점검표

  • Root Layout에서 전역 폰트를 한 번만 적용했는가?
  • 제목/본문/코드 폰트가 너무 많이 섞이지 않는가?
  • 한글 폰트 weight를 불필요하게 많이 불러오지 않는가?
  • DevTools Network 탭에서 외부 폰트 요청이 남아 있지 않은가?
  • Lighthouse에서 Cumulative Layout Shift가 증가하지 않는가?

📄 메타데이터와 SEO

1. 정적 메타데이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/layout.js
export const metadata = {
  title: 'My Blog',
  description: 'Next.js로 만든 블로그',
  keywords: ['Next.js', 'React', '블로그'],
};

export default function RootLayout({ children }) {
  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

metadatagenerateMetadata는 Server Component에서만 지원됩니다. Client Component 파일에 "use client"를 붙인 뒤 같은 파일에서 metadata를 export하려고 하면 구조를 나눠야 합니다.

2. 동적 메타데이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  };
}

export default async function PostPage({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

동적 메타데이터는 페이지 데이터와 같은 source를 쓰는 편이 안전합니다. 본문 제목은 “A”인데 OG 제목은 “B”인 상태가 오래 남으면 검색 결과와 공유 미리보기가 어긋납니다.

3. Open Graph와 Twitter 카드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
export const metadata = {
  title: 'My Awesome Post',
  description: 'This is an awesome post about Next.js',

  // Open Graph (페이스북, 카카오톡 등)
  openGraph: {
    title: 'My Awesome Post',
    description: 'This is an awesome post about Next.js',
    url: 'https://mysite.com/blog/awesome-post',
    siteName: 'My Blog',
    images: [
      {
        url: 'https://mysite.com/og-image.jpg',
        width: 1200,
        height: 630,
        alt: 'Post cover image',
      },
    ],
    locale: 'ko_KR',
    type: 'article',
  },

  // Twitter 카드
  twitter: {
    card: 'summary_large_image',
    title: 'My Awesome Post',
    description: 'This is an awesome post about Next.js',
    images: ['https://mysite.com/twitter-image.jpg'],
    creator: '@myhandle',
  },

  // 추가 메타 태그
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
};

4. 파일 기반 메타데이터

Next.js는 app 폴더 안의 특별한 파일 이름을 메타데이터로 자동 인식합니다.

파일 역할
app/favicon.ico 브라우저 탭과 북마크 아이콘
app/icon.png 앱 아이콘
app/apple-icon.png iOS 홈 화면 아이콘
app/opengraph-image.png 기본 Open Graph 이미지
app/twitter-image.png 기본 Twitter 카드 이미지
app/robots.txt 크롤러 접근 정책
app/sitemap.xml 검색엔진용 사이트맵

정적인 사이트라면 이미지 파일을 그대로 두는 방식이 가장 단순합니다. 블로그처럼 글마다 다른 OG 이미지가 필요하다면 route segment 안에 opengraph-image.tsx를 두고 동적으로 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';

export const size = {
  width: 1200,
  height: 630,
};

export const contentType = 'image/png';

export default async function Image({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);

  return new ImageResponse(
    (
      <div
        style={{
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          background: '#111827',
          color: 'white',
          fontSize: 72,
          padding: 80,
        }}
      >
        {post.title}
      </div>
    )
  );
}

동적 OG 이미지는 예쁘게 만드는 것보다 먼저 실패하지 않게 만드는 것이 중요합니다. 제목이 길 때 줄바꿈이 되는지, 한글 폰트가 깨지지 않는지, 404 글에서 fallback이 있는지를 확인하세요.


🌍 다국어 메타데이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// app/[lang]/page.js
export async function generateMetadata({ params }) {
  const { lang } = await params;

  const translations = {
    en: {
      title: 'Welcome to My Site',
      description: 'This is my awesome website',
    },
    ko: {
      title: '제 사이트에 오신 것을 환영합니다',
      description: '멋진 웹사이트입니다',
    },
  };

  const t = translations[lang] || translations.en;

  return {
    title: t.title,
    description: t.description,
    alternates: {
      canonical: `https://mysite.com/${lang}`,
      languages: {
        'en-US': 'https://mysite.com/en',
        'ko-KR': 'https://mysite.com/ko',
      },
    },
  };
}

✅ 최적화 후 확인 루틴

최적화 코드를 넣었다면 숫자로 확인해야 합니다.

1
2
npm run build
npm run start

브라우저에서 확인할 항목은 네 가지입니다.

  • Lighthouse의 LCP 이미지가 의도한 hero 이미지인지 확인한다.
  • Network 탭에서 이미지가 표시 크기보다 과하게 크지 않은지 본다.
  • 폰트 요청이 자체 도메인에서 서빙되는지 확인한다.
  • 공유 디버거 또는 <head> 태그에서 title, description, OG 이미지가 맞는지 본다.

Vercel에 배포한다면 Speed Insights나 Web Analytics를 함께 보세요. Lighthouse 점수는 실험실 지표이고, 실제 사용자의 네트워크와 기기는 더 다양합니다.


🔍 자주 묻는 질문 (FAQ)

Q1: 일반 img 태그 대신 Image를 써야 하나요?

: 네! Image 컴포넌트가 훨씬 좋습니다.

일반 img 태그:

1
<img src="/large-image.jpg" />  // 5MB 원본 그대로 로딩

next/image:

1
2
3
4
<Image src="/large-image.jpg" alt="제품 화면" width={800} height={600} />
// → 자동으로 WebP로 변환 (500KB)
// → 화면 크기에 맞게 여러 버전 생성
// → 지연 로딩

단, 실제 성능 차이는 원본 크기, CDN, sizes, 네트워크 상태에 따라 달라집니다. “무조건 몇 배 빠르다”보다 빌드 후 Lighthouse와 Network 탭으로 확인하는 습관이 더 중요합니다.

Q2: 폰트 깜빡임(FOUT)을 방지하려면?

: next/font를 사용하세요!

1
2
3
4
5
6
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',  // ← 이게 핵심!
});

display 옵션:

  • swap: 폰트 로딩 전 시스템 폰트 사용
  • optional: 네트워크 상태에 따라 결정
  • block: 폰트 로딩까지 텍스트 숨김 (권장 안 함)

🎯 오늘 배운 내용 정리

  1. 이미지 최적화
    • next/image 자동 최적화
    • LCP 이미지만 priority
    • fill과 반응형 그리드에는 sizes 지정
  2. 폰트 최적화
    • next/font Google Fonts
    • 로컬 폰트
    • self-hosting과 레이아웃 이동 방지
  3. 메타데이터
    • 정적/동적 메타데이터
    • 파일 기반 favicon/OG 이미지
    • 다국어 canonical과 alternates
  4. 검증
    • Lighthouse, Network, <head> 태그 확인
    • 실제 사용자 지표와 함께 판단

📚 시리즈 네비게이션


“최적화는 어렵지 않습니다. Next.js가 대부분 해줍니다!” ⚡

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.