๐ผ๏ธ Next.js 14์ฅ: next/image & next/font โ Core Web Vitals๋ฅผ ์งํค๋ ์ต์ ํ ๋ฌด๊ธฐ
๐ ๊ฐ์
next/image์ next/font๋ก Core Web Vitals๋ฅผ ๊ฐ์ ํ๋ ์ด๋ฏธ์งยทํฐํธ ์ต์ ํ ๊ธฐ๋ฒ์ ์์๋ด ๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐ผ๏ธ next/image โ ์ด๋ฏธ์ง ์ต์ ํ ์๋ํ ๊ธฐ๊ณ ๐ข
- ๐ค next/font โ ํฐํธ ๊น๋นก์(FOIT/FOUT) ๋ฐ๋ฉธ ๐ก
- โก ํต์ฌ ์ฑ๋ฅ ์งํ: LCP๋ฅผ ์ก์๋ผ ๐ก
- ๐๏ธ ๊ณ ๊ธ ์ด๋ฏธ์ง ํจํด: fill, sizes, blurDataURL ๐ด
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 13๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 6๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์ฒ (์ ์
): "Lighthouse ๋๋ ค๋ดค๋๋ Performance ์ ์๊ฐ 47์ ์ด์์. LCP๊ฐ 5์ด๊ฐ ๋๊ณ CLS๋ ๋นจ๊ฐ๋ถ์ด์์. ์ด๋ฏธ์ง ์ต์ ํ๋ฅผ ์ด๋์๋ถํฐ ์์ํด์ผ ํ ์ง ๋ชจ๋ฅด๊ฒ ์ด์.
<img>ํ๊ทธ๋ฅผ<Image>๋ก ๋ฐ๊พธ๋ฉด ์๋์ผ๋ก ๋ค ํด๊ฒฐ๋๋์?" - ์ํธ(๋ฆฌ๋): "์์ฒ ๋,
<img>โ<Image>๊ต์ฒด๋ ์์์ ์ด์ง ๋์ด ์๋์์.priority,sizes,fill์์ฑ์ ์ ๋๋ก ์จ์ผ LCP๊ฐ ์กํ์. ๊ทธ๋ฆฌ๊ณ Google Fonts๋ฅผ<link>ํ๊ทธ๋ก ์ฐ๊ฒฐํ๋ฉด ์ธ๋ถ ๋คํธ์ํฌ ์๋ณต์ด ๋ฐ์ํด์ ํฐํธ ๊น๋นก์(FOUT)์ด ์๊ฒจ์.next/font๋ก ๋ฐ๊พธ๋ฉด ์์ ํ ์ฌ๋ผ์ง๋๋ฐ, ์ด๊ฒ Lighthouse ์ ์๋ฅผ 10์ ์ด์ ์ฌ๋ ค์ฃผ๋ ๊ฟํ์ด์์."
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
<img> ํ๊ทธ์ ๋ฌธ์ โ next/image ๊ธฐ๋ณธ โ LCP ์ต์ ํ ์์ฑ โ next/font๋ก ํฐํธ ๊น๋นก์ ์ ๊ฑฐ โ ๊ณ ๊ธ ํจํด
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
-
next/image์priority,sizes,fill์์ฑ์ ์ธ์ ์ด๋ป๊ฒ ์จ์ผ ํ๋์ง ํ๋จํ ์ ์๋ค -
next/font๋ก Google Fonts๋ฅผ ์ฐ๊ฒฐํด FOIT/FOUT์ ์์ ํ ์ ๊ฑฐํ ์ ์๋ค - Lighthouse LCP ์ ์๊ฐ ๋ฎ์ ๋ ์ฒดํฌํ ํฌ์ธํธ๋ฅผ ์๊ณ ์๋ค
๐ค ์ ์์์ผ ํ๋๊ฐ
์ผ๋ฐ <img> ํ๊ทธ๋ฅผ ๊ทธ๋๋ก ์ฐ๋ฉด ์ด๋ค ์ผ์ด ์๊ธฐ๋์ง ์์?
- ์๋ณธ ์ด๋ฏธ์ง ๊ทธ๋๋ก ์ ์ก โ 5MB 4K ์ฌ์ง์ ๋ชจ๋ฐ์ผ 300px ํ๋ฉด์ ๊ทธ๋๋ก ๋ด๋ ค๋ณด๋ด
- Layout Shift โ ์ด๋ฏธ์ง ํฌ๊ธฐ๋ฅผ ๋ชจ๋ฅด๋ ๋ก๋๋๋ฉด์ ์๋ ๋ด์ฉ์ด ์ฅ ๋ฐ๋ ค (CLS ์ ํ)
- ์ง์ฐ ๋ก๋ ์ ๋จ โ ์คํฌ๋กคํ๋ฉด ๋ณด์ผ ์ด๋ฏธ์ง๋ ์ฆ์ ๋ค์ด๋ก๋ ์์ (LCP ์ ํ)
- WebP ๋ณํ ์์ โ JPEG ๊ทธ๋๋ก ์ ์ก, ์ต์ ํ์๋ณด๋ค 40% ์ฉ๋ ๋ญ๋น
๊ตฌ๊ธ์ Core Web Vitals ์ฐ๊ตฌ์ ๋ฐ๋ฅด๋ฉด ํ์ด๋ก ์ด๋ฏธ์ง ํ๋๋ฅผ ์ต์ ํํ์ง ์์ผ๋ฉด LCP๊ฐ 2~5์ด ์ ํ๋๊ณ , ์ด๊ฒ ์ดํ๋ฅ 35% ์ฆ๊ฐ๋ก ์ด์ด์ง๋ค๊ณ ํด.
next/image๋ ์ด ๋ชจ๋ ๊ฑธ ์๋์ผ๋ก ํด๊ฒฐํด์ค. ๋ฌธ์ ๋ "๊ทธ๋ฅ ์ด๋ค"๋ ๊ฒ ์๋๋ผ "์ ๋๋ก ์จ์ผ ํ๋ค"๋ ๊ฑฐ์ผ.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
์์ ๋ฐฐ๋ฌ์ ์์ผฐ๋๋ฐ ํญ์ ๊ฐ์ฅ ํฐ ํฌ์ฅ ๋ฐ์ค๋ก๋ง ๋ฐฐ๋ฌํด์ฃผ๋ ๊ฐ๊ฒ๊ฐ ์์ด. ์ปต๋ผ๋ฉด ํ๋๋ฅผ ์์ผ๋ ์ด์ฟ์ง ๋ฐ์ค์ ๋ฃ์ด์ ์. ๋ญ๋น์์?
next/image๋ "์๋ฆฌํ ํฌ์ฅ๊ธฐ"์ผ. ํฐ ํ๋ฉด์ ํฐ ์ด๋ฏธ์ง, ์์ ํ๋ฉด์ ์์ ์ด๋ฏธ์ง๋ฅผ ๋ฑ ๋ง๊ฒ ์๋ผ์ ๋ณด๋ด์ค. ๊ทธ๋ฆฌ๊ณ WebP๋ผ๋ ๋ ๊ฐ๋ฒผ์ด ํฌ์ฅ ๋ฐฉ์์ผ๋ก ์๋ ๋ณํํด์ค.
next/font์ ๋น์ :
์์์ ๋ฉ๋ดํ์ด ์ฒ์์ ๋น ์ข ์ด์๋ค๊ฐ, ์ ์ ํ์ ๊ธ์จ๊ฐ ํ์ผ๋ก ์ฐ์ฌ์ง๋ ๊ฑธ ์์ํด๋ด. ๊ทธ ์๊ฐ ์๋๋ค์ด ํ๋ค์ง ๋๋ผ์ง? ์ด๊ฒ FOUT(ํฐํธ ๊น๋นก์)์ด์ผ.
next/font๋ ๋ฉ๋ดํ์ ๊ฐ์ ธ์ฌ ๋๋ถํฐ ์ด๋ฏธ ๊ธ์จ๊ฐ ์ ํ์๊ฒ ํด์ค. ํฐํธ๊ฐ ๋ก๋๋๊ธฐ ์ ๊น์ง ์์คํ ํฐํธ๋ฅผ ์ ํํ ํฌ๊ธฐ๋ก ์์ฝํด๋๋ ๊ฑฐ์ผ.
๐ผ๏ธ next/image โ ์ด๋ฏธ์ง ์ต์ ํ ์๋ํ ๊ธฐ๊ณ ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
next/image์<Image>์ปดํฌ๋ํธ๋ฅผ ๊ธฐ๋ณธ ์ฌ์ฉํ ์ ์๋คwidth,height๋๋fill์์ฑ์ด ์ ํ์์ธ์ง ์ดํดํ๋ค
// โ ์์ฒ ์ด์ ์์งํ ์ฝ๋ โ ์ผ๋ฐ img ํ๊ทธ
<img src="/hero.jpg" alt="์คํฐ๋ ๋ชจ์ง ๋ฐฐ๋" />
// ๋ฌธ์ : ์๋ณธ ํฌ๊ธฐ ๊ทธ๋๋ก ์ ์ก, CLS ๋ฐ์, WebP ๋ณํ ์์
// โ
์ํธ๊ฐ ๊ณ ์ณ์ค ์ฝ๋ โ next/image
import Image from 'next/image'
// ์ด๋ฏธ์ง ํฌ๊ธฐ๋ฅผ ์ ๋ (์ ์ ์ด๋ฏธ์ง, ์๋ ค์ง ํฌ๊ธฐ)
<Image
src="/hero.jpg"
alt="์คํฐ๋ ๋ชจ์ง ๋ฐฐ๋"
width={1200} // ๋ฐ๋์ ํ์ โ CLS ๋ฐฉ์ง๋ฅผ ์ํด ๋ฏธ๋ฆฌ ๊ณต๊ฐ ํ๋ณด
height={630}
/>next/image๊ฐ ์๋์ผ๋ก ํด์ฃผ๋ ๊ฒ:
| ๊ธฐ๋ฅ | ์ค๋ช |
|---|---|
| WebP/AVIF ์๋ ๋ณํ | ๋ธ๋ผ์ฐ์ ๊ฐ ์ง์ํ๋ฉด ๋ ๊ฐ๋ฒผ์ด ํ์์ผ๋ก ์๋ ๋ณํ |
| ๋ฐ์ํ ํฌ๊ธฐ ์กฐ์ | ์์ฒญํ ํฌ๊ธฐ์ ๋ง๊ฒ ์๋ฒ์์ ๋ฆฌ์ฌ์ด์ง |
| Lazy Loading | ๋ทฐํฌํธ์ ๋ค์ด์ฌ ๋ ๋ก๋ (๊ธฐ๋ณธ๊ฐ) |
| CLS ๋ฐฉ์ง | width/height๋ก ๋ ์ด์์ ๊ณต๊ฐ ๋ฏธ๋ฆฌ ํ๋ณด |
| ์บ์ฑ | CDN ๋๋ Next.js ์๋ฒ์์ ์ต์ ํ๋ ์ด๋ฏธ์ง ์บ์ฑ |
โ ๏ธ ์ฃผ์:
<Image>๋alt์์ฑ์ด ํ์์ผ. ์น ์ ๊ทผ์ฑ(Accessibility) ๋๋ฌธ์ด๊ธฐ๋ ํ์ง๋ง, ์ด๋ฏธ์ง๊ฐ ๋ก๋ ์ ๋์ ๋ ๋์ฒด ํ ์คํธ๋ก ํ์๋ผ. ๋น ๋ฌธ์์ดalt=""์ "์ฅ์์ฉ ์ด๋ฏธ์ง"๋ผ๋ ์๋ฏธ์ผ.
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
next/image์์width/height๋ "์ค์ ํฌ๊ธฐ"๊ฐ ์๋๋ผ "๋น์จ ํํธ"์ผ. ์ค์ ํ์ ํฌ๊ธฐ๋ CSS๋ก ์ ์ดํ๊ณ , ์ฌ๊ธฐ์ CLS ๋ฐฉ์ง๋ฅผ ์ํ ์์ฝ๋ ๊ณต๊ฐ ๋น์จ๋ง ์ ์ธํ๋ ๊ฑฐ์ผ.
๐ค next/font โ ํฐํธ ๊น๋นก์(FOIT/FOUT) ๋ฐ๋ฉธ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Google Fonts๋ฅผ
next/font/google๋ก ์ฐ๊ฒฐํด FOIT/FOUT์ ์์ ํ ์ ๊ฑฐํ ์ ์๋ค- ํฐํธ๋ฅผ CSS ๋ณ์๋ก ๋ฑ๋กํด ์ ์ญ์์ ์ฌ์ฉํ๋ ํจํด์ ๊ตฌํํ ์ ์๋ค
๐ ์ฉ์ด: FOIT/FOUT
- FOIT (Flash Of Invisible Text) โ ํฐํธ ๋ก๋ ์ ํ ์คํธ๊ฐ ์ ๋ณด์ด๋ค๊ฐ ๊ฐ์๊ธฐ ๋ํ๋จ
- FOUT (Flash Of Unstyled Text) โ ํฐํธ ๋ก๋ ์ ๊ธฐ๋ณธ ํฐํธ๋ก ํ์๋๋ค๊ฐ ๊ฐ์๊ธฐ ๋ฐ๋
// โ ์์ฒ ์ด์ ์์งํ ๋ฐฉ๋ฒ โ HTML์ Google Fonts ๋งํฌ ํ๊ทธ
// app/layout.tsx
<head>
{/* ์ด๊ฑด ์ธ๋ถ ์๋ฒ ์๋ณต ๋ฐ์ โ ๋๋ฆฌ๊ณ , FOUT ์๊น */}
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR" />
</head>
// โ
์ํธ๊ฐ ๊ถ์ฅํ๋ ๋ฐฉ๋ฒ โ next/font/google
// app/layout.tsx
import { Noto_Sans_KR, Inter } from 'next/font/google'
const notoSansKR = Noto_Sans_KR({
subsets: ['latin'], // ๋ผํด ๋ฌธ์ ์๋ธ์
(ํ๊ธ์ ์๋ ํฌํจ)
weight: ['400', '700'], // ์ฌ์ฉํ ๊ตต๊ธฐ ์ง์ (๋ถํ์ํ ๊ตต๊ธฐ ๋ก๋ ๋ฐฉ์ง)
variable: '--font-noto', // CSS ๋ณ์๋ช
์ผ๋ก ๋ฑ๋ก
display: 'swap', // FOIT ๋์ FOUT ํ์ฉ (๊ธฐ๋ณธ๊ฐ, ๊ถ์ฅ)
})
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
// className์ผ๋ก ํฐํธ CSS ๋ณ์๋ฅผ <html> ํ๊ทธ์ ๋ฑ๋ก
<html lang="ko" className={`${notoSansKR.variable} ${inter.variable}`}>
<body>{children}</body>
</html>
)
}/* globals.css โ CSS ๋ณ์๋ก ํฐํธ ์ฌ์ฉ */
body {
font-family: var(--font-noto), var(--font-inter), sans-serif;
}next/font์ ์ฅ์ :
| ๊ธฐ์กด ๋ฐฉ์ (link ํ๊ทธ) | next/font |
|---|---|
| ์ธ๋ถ ์๋ฒ ์๋ณต ๋ฐ์ | ํฐํธ ํ์ผ ๋ก์ปฌ ๋ค์ด๋ก๋ ํ ์์ฒด ์๋น |
| FOUT ๋ฐ์ | size-adjust๋ก ๋ ์ด์์ ์ด๋ ๋ฐฉ์ง |
| ๊ฐ์ธ์ ๋ณด (IP) Google์ ์ ์ก | ํ๋ผ์ด๋ฒ์ ๋ณดํธ (์ธ๋ถ ์์ฒญ ์์) |
| ๋งค ํ์ด์ง ๋ ๋๋ง๋ค blocking | ํฐํธ preload๋ก ๋น ๋ฅธ ํ์ |
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
next/font๋ Google Fonts๋ฅผ ์ฐ๋ฆฌ ์๋ฒ์์ ์ง์ ์๋นํ๋ ์์ฒด CDN์ ๋ง๋๋ ๊ฒ๊ณผ ๊ฐ์. ์ธ๋ถ ์์ฒญ ์๊ณ , ๊น๋นก์ ์๊ณ , ๋น ๋ฅด๊ณ .
โก ํต์ฌ ์ฑ๋ฅ ์งํ: LCP๋ฅผ ์ก์๋ผ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
priority์์ฑ์ผ๋ก LCP ์ด๋ฏธ์ง๋ฅผ ์ ์ธํด preload ์ฒ๋ฆฌํ ์ ์๋คsizes์์ฑ์ผ๋ก ๋ฐ์ํ ์ด๋ฏธ์ง ํฌ๊ธฐ๋ฅผ ๋ธ๋ผ์ฐ์ ์ ์๋ ค์ค ์ ์๋ค
LCP(Largest Contentful Paint) โ ๋ทฐํฌํธ์์ ๊ฐ์ฅ ํฐ ์์๊ฐ ๋ ๋๋ง๋๋ ์๊ฐ. ๋๋ถ๋ถ ํ์ด๋ก ์ด๋ฏธ์ง๊ฐ LCP ์์์ผ.
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
๊ธฐ๋ณธ์ ์ผ๋ก<Image>๋ lazy loading์ด์ผ. ๊ทธ๋ฐ๋ฐ ํ์ด์ง ์ต์๋จ์ ํ์ด๋ก ์ด๋ฏธ์ง๋ lazy loading์ผ๋ก ์ฒ๋ฆฌํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น?
// โ ํ์ด๋ก ์ด๋ฏธ์ง์ lazy loading์ด ์ ์ฉ๋ ์ํ (๊ธฐ๋ณธ๊ฐ)
<Image
src="/hero.jpg"
alt="ํ์ด๋ก ๋ฐฐ๋"
width={1200}
height={630}
// priority ์์ผ๋ฉด lazy loading โ LCP ์ด๋ฏธ์ง์ธ๋ฐ ๋ฆ๊ฒ ๋ก๋๋จ!
/>
// โ
ํ์ด๋ก ์ด๋ฏธ์ง์ priority ์ถ๊ฐ
<Image
src="/hero.jpg"
alt="ํ์ด๋ก ๋ฐฐ๋"
width={1200}
height={630}
priority // โ preload ์ฒ๋ฆฌ, ์ฆ์ ๋ค์ด๋ก๋ ์์
// โ LCP ์ด๋ฏธ์ง์๋ง ์ธ ๊ฒ. ๋ชจ๋ ์ด๋ฏธ์ง์ ์ฐ๋ฉด ์คํ๋ ค ์ญํจ๊ณผ
/>sizes ์์ฑ โ ๋ฐ์ํ ์ด๋ฏธ์ง์ ํต์ฌ:
// โ sizes ์๋ ๊ฒฝ์ฐ
// ๋ธ๋ผ์ฐ์ ๊ฐ ์ด๋ฏธ์ง ํฌ๊ธฐ๋ฅผ ๋ชจ๋ฅด๋ ํญ์ ๋ทฐํฌํธ 100% ํฌ๊ธฐ์ ์ด๋ฏธ์ง๋ฅผ ์์ฒญ
// ๋ชจ๋ฐ์ผ(375px)์ธ๋ฐ ๋ฐ์คํฌํฑ์ฉ 1200px ์ด๋ฏธ์ง๋ฅผ ๋ฐ์์ค๋ ๋ญ๋น ๋ฐ์!
<Image src="/card.jpg" alt="์คํฐ๋ ์นด๋" width={400} height={300} />
// โ
sizes ์์ฑ์ผ๋ก ๋ฐ์ํ ํํธ ์ ๊ณต
<Image
src="/card.jpg"
alt="์คํฐ๋ ์นด๋"
width={400}
height={300}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 400px"
// ์ค๋ช
: ๋ชจ๋ฐ์ผ(768px ์ดํ)์์ ํ๋ฉด ์ ์ฒด, ํ๋ธ๋ฆฟ(1200px ์ดํ)์์ ์ ๋ฐ, ๊ทธ ์ธ์ 400px
/>๐๏ธ ๊ณ ๊ธ ์ด๋ฏธ์ง ํจํด: fill, sizes, blurDataURL ๐ด
๐ fill ๋ชจ๋ โ ๋ถ๋ชจ ์ปจํ
์ด๋๋ฅผ ๊ฝ ์ฑ์ฐ๊ธฐ
์ด๋ฏธ์ง ํฌ๊ธฐ๋ฅผ ๋ฏธ๋ฆฌ ์ ์ ์๊ฑฐ๋, ์ปจํ ์ด๋์ ๋ง์ถฐ ๊ฝ ์ฑ์์ผ ํ ๋ ์จ.
// ์นด๋ ์ธ๋ค์ผ, ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง ๋ฑ์ ํ์ฉ
<div style={{ position: 'relative', width: '100%', height: '200px' }}>
{/* fill ์ฌ์ฉ ์ ๋ถ๋ชจ๋ ๋ฐ๋์ position: relative */}
<Image
src={post.thumbnail}
alt={post.title}
fill
style={{ objectFit: 'cover' }} // ๋น์จ ์ ์งํ๋ฉฐ ์ฑ์ฐ๊ธฐ
sizes="(max-width: 768px) 100vw, 50vw"
/>
</div>๐ซ๏ธ blurDataURL โ ์ด๋ฏธ์ง ๋ก๋ ์ ๋ธ๋ฌ ํ๋ ์ด์คํ๋
// ์ธ๋ถ ์ด๋ฏธ์ง์ ๋ธ๋ฌ ํจ๊ณผ ํ๋ ์ด์คํ๋
<Image
src={post.thumbnail}
alt={post.title}
width={400}
height={300}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."
// base64 ์ธ์ฝ๋ฉ๋ ์ด์ ํด์๋ ์ด๋ฏธ์ง (1x1ํฝ์
์ ๋๋ฉด ์ถฉ๋ถ)
/>๐ ์ธ๋ถ ์ด๋ฏธ์ง ๋๋ฉ์ธ ํ์ฉ (next.config.ts)
์ธ๋ถ URL ์ด๋ฏธ์ง๋ฅผ next/image๋ก ์ต์ ํํ๋ ค๋ฉด ๋๋ฉ์ธ์ ํ์ฉํด์ผ ํด.
// next.config.ts
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.youngsu.community', // ์ฐ๋ฆฌ CDN
pathname: '/uploads/**',
},
{
protocol: 'https',
hostname: '**.githubusercontent.com', // GitHub ์๋ฐํ
},
],
},
}๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
โ Invalid src prop โ ํ์ฉ๋์ง ์์ ์ธ๋ถ ๋๋ฉ์ธ
์ธ์ ๋์ค๋๊ฐ?
Error: Invalid src prop on `next/image`, hostname "example.com" is not configured under `images` in your `next.config.js`
ํด๊ฒฐ์ฑ :
// next.config.ts์ ํด๋น ๋๋ฉ์ธ ์ถ๊ฐ
images: {
remotePatterns: [{ protocol: 'https', hostname: 'example.com' }],
}โ Image with src ... missing required width/height
์์ธ: <Image>์ width, height ๋๋ fill ์์ด ์ฌ์ฉ.
ํด๊ฒฐ์ฑ
: ํฌ๊ธฐ๋ฅผ ์๋ฉด width/height ์ถ๊ฐ. ์ปจํ
์ด๋๋ฅผ ์ฑ์์ผ ํ๋ฉด fill ์ฌ์ฉ.
โ ํฐํธ๊ฐ ๋งค ๋ฐฐํฌ๋ง๋ค ๋ค๋ฅธ URL ์์ฑ โ ์บ์ ๋ฌดํจํ
์์ธ: next/font๋ ๋น๋๋ง๋ค ํฐํธ URL์ ํด์๋ฅผ ์ถ๊ฐํด.
ํด๊ฒฐ์ฑ : ์ค์ ๋ก๋ ๋ฌธ์ ๊ฐ ์๋์ผ. Next.js๊ฐ ์๋์ผ๋ก ์ ์ ํ ์บ์ ํค๋๋ฅผ ์ค์ ํด์ค. ๋ธ๋ผ์ฐ์ ๋ ์ URL๋ก ์๋ก ๋ฐ์๊ฐ์ง๋ง, CDN ์บ์ฑ์ผ๋ก ๋น ๋ฅด๊ฒ ์ฒ๋ฆฌ๋ผ.
๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
๐ ์ํฉ๋ณ ์ด๋ฏธ์ง ํจํด
| ์ํฉ | ์์ฑ ์กฐํฉ |
|---|---|
| ํ์ด๋ก ์ด๋ฏธ์ง (LCP) | width + height + priority + sizes |
| ์นด๋ ์ธ๋ค์ผ (ํฌ๊ธฐ ์ ๋) | fill + objectFit: cover + sizes |
| ์ ์ ์ด๋ฏธ์ง (ํฌ๊ธฐ ๊ณ ์ ) | width + height |
| ๋ธ๋ฌ ํ๋ ์ด์คํ๋ | placeholder="blur" + blurDataURL |
โ ๏ธ ์ ๋ ํ์ง ๋ง ๊ฒ
| ์ํฉ | โ ๋์ ์ | โ ์ข์ ์ |
|---|---|---|
| ๋ชจ๋ ์ด๋ฏธ์ง์ priority | ๋ชจ๋ ์ฆ์ ๋ก๋ โ ์ด๊ธฐ ๋ก๋ ๋๋ ค์ง | ํ์ด๋ก(LCP) ์ด๋ฏธ์ง์๋ง |
| sizes ์์ด fill ์ฌ์ฉ | ๋ทฐํฌํธ 100% ์ด๋ฏธ์ง ํญ์ ์์ฒญ | sizes ๋ฐ๋์ ๊ฐ์ด |
| Google Fonts link ํ๊ทธ | ์ธ๋ถ ์๋ณต ๋ฐ์, FOUT | next/font/google ์ฌ์ฉ |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. next/image์์ width์ height ๋๋ fill/sizes๋ฅผ ์ ๊ฒฝ ์จ์ผ ํ๋ ์ด์ ๋?
โ ์ ๋ต: ๋ธ๋ผ์ฐ์ ๊ฐ ์ด๋ฏธ์ง ๊ณต๊ฐ๊ณผ ํ์ํ ๋ฆฌ์์ค ํฌ๊ธฐ๋ฅผ ๋ฏธ๋ฆฌ ํ๋จํ๊ฒ ํ๊ธฐ ์ํด์๋ค.
๐ก ์์ธ ํด์ค: ํฌ๊ธฐ ์ ๋ณด๊ฐ ์์ผ๋ฉด ๋ ์ด์์์ด ๋ฆ๊ฒ ๋ฐ๋ฆฌ๊ณ CLS๊ฐ ๋๋น ์ง๋ค. sizes๊ฐ ๋ถ์ ํํ๋ฉด ํ์ ์ด์์ผ๋ก ํฐ ์ด๋ฏธ์ง๋ฅผ ๋ฐ์ ์ ์๋ค.
Q2. next/font๊ฐ ์ฑ๋ฅ์ ๋์ ๋๋ ์ง์ ์?
โ ์ ๋ต: ํฐํธ๋ฅผ ๋ก์ปฌ ์์ฐ์ฒ๋ผ ์ ๊ณตํด ์ธ๋ถ ์์ฒญ๊ณผ ํฐํธ ๋ก๋ฉ์ผ๋ก ์ธํ ํ๋ค๋ฆผ์ ์ค์ธ๋ค.
๐ก ์์ธ ํด์ค: ํฐํธ๋ ํ ์คํธ ๋ ๋๋ง๊ณผ ๋ ์ด์์์ ์ง์ ์ํฅ์ ์ค๋ค. preload์ fallback ์ ๋ต๊น์ง ํจ๊ป ๋ณด๋ฉด ์ฒซ ํ๋ฉด ์์ ์ฑ์ด ์ข์์ง๋ค.
Q3. ์์ฒ ์ด์ ํ ์คํธ ํ์: ํ์ด๋ก ์ด๋ฏธ์ง๊ฐ LCP์ธ๋ฐ ๋ฆ๊ฒ ๋ฌ๋ค. ๋ฌด์์ ๋จผ์ ์ ๊ฒํ ๊น?
โ ์ ๋ต: Image ์ฌ์ฉ ์ฌ๋ถ, ์ค์ ํ์ ํฌ๊ธฐ์ ๋ง๋ sizes, ์ฐ์ ๋ก๋ฉ ํํธ, ์๋ณธ ์ด๋ฏธ์ง ์ฉ๋์ ํ์ธํ๋ค.
๐ก ์์ธ ํด์ค: ๋จ์ํ CDN๋ง ๋ถ์ธ๋ค๊ณ ํด๊ฒฐ๋์ง ์๋๋ค. ๋ธ๋ผ์ฐ์ ๊ฐ ๋นจ๋ฆฌ ๋ฐ๊ฒฌํ๊ณ ์ฌ๋ฐ๋ฅธ ํฌ๊ธฐ๋ฅผ ๋ฐ๋๋ก ์ฝ๋์ ์์ฐ์ ๊ฐ์ด ์กฐ์ ํด์ผ ํ๋ค.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ์ด๋ฏธ์ง๋ฅผ ๋ณด์ด๋ ์์ฐ์ด ์๋๋ผ ์ฑ๋ฅ ์งํ๋ฅผ ์ข์ฐํ๋ ์ ๋ ฅ์ผ๋ก ๋ณด๊ฒ ๋๋ค.
๐ก "์๊ฐ ์์ฐ ์ต์ ํ๋ ์์ ํ๋ฉด ์ด์ ์ ๋น ๋ฅด๊ณ ํ๋ค๋ฆฌ์ง ์๋ ์ฒซ ํ๋ฉด์ ์กฐ๊ฑด์ด๋ค."
๋ค์ ๋์์ธ ์ ์ฉ ๋๋ ์ด๋ฏธ์ง ํฌ๊ธฐ์ ํฐํธ ๋ก๋ฉ ์ ๋ต์ QA ์ฒดํฌ๋ฆฌ์คํธ์ ๋ฃ๊ฒ ๋ค.
๐ ๋ ์์๋ณด๊ธฐ
- Next.js ๊ณต์ ๋ฌธ์ โ next/image
- Next.js ๊ณต์ ๋ฌธ์ โ next/font
- Core Web Vitals โ web.dev