[이제와서 시작하는 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: 폰트 로딩까지 텍스트 숨김 (권장 안 함)
🎯 오늘 배운 내용 정리
- 이미지 최적화
- next/image 자동 최적화
- WebP/AVIF 변환
- 지연 로딩
- 폰트 최적화
- next/font Google Fonts
- 로컬 폰트
- FOUT 방지
- 메타데이터
- 정적/동적 메타데이터
- Open Graph
- Twitter 카드
📚 시리즈 네비게이션
“최적화는 어렵지 않습니다. Next.js가 대부분 해줍니다!” ⚡
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.