포스트

[이제와서 시작하는 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

2. 외부 이미지

1
2
3
4
5
6
7
8
9
10
11
// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
    ],
  },
};
1
2
3
4
5
6
<Image
  src="https://images.unsplash.com/photo-..."
  alt="Unsplash 이미지"
  width={800}
  height={600}
/>

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

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

4. 블러 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 의존 없음)

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>
  );
}

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;
  }
}

📄 메타데이터와 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>
  );
}

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>
  );
}

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,
    },
  },
};

🌍 다국어 메타데이터

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',
      },
    },
  };
}

🔍 자주 묻는 질문 (FAQ)

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

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

일반 img 태그:

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

next/image:

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

성능 차이: 5~10배 빠름!

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 자동 최적화
    • WebP/AVIF 변환
    • 지연 로딩
  2. 폰트 최적화
    • next/font Google Fonts
    • 로컬 폰트
    • FOUT 방지
  3. 메타데이터
    • 정적/동적 메타데이터
    • Open Graph
    • Twitter 카드

📚 시리즈 네비게이션


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

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