๐ 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. ๋ค์ ์ค priority ์์ฑ์ ๋ฐ๋์ ์ถ๊ฐํด์ผ ํ๋ ์ํฉ์?
- A) ํ์ด์ง ํ๋จ์ ๊ด๋ จ ๊ฒ์๊ธ ์ธ๋ค์ผ ์ด๋ฏธ์ง
- B) ๋ทฐํฌํธ ์ต์๋จ์ ๋ ธ์ถ๋๋ ํ์ด๋ก ๋ฐฐ๋ ์ด๋ฏธ์ง
- C) ์คํฌ๋กคํด์ผ ๋ณด์ด๋ ์ฌ์ฉ์ ์๋ฐํ ์ด๋ฏธ์ง
- D) ํด๋ฆญํ๋ฉด ์ด๋ฆฌ๋ ๋ชจ๋ฌ ์์ ์ด๋ฏธ์ง
โ ์ ๋ต: B
ํด์ค:
priority๋ preload ์ฒ๋ฆฌ๋ผ ์ฆ์ ๋ค์ด๋ก๋๊ฐ ์์๋ผ. LCP ์์์ธ ํ์ด๋ก ์ด๋ฏธ์ง์๋ง ์จ์ผ ํจ๊ณผ๊ฐ ์๊ณ , ๋๋จธ์ง์ ๋จ์ฉํ๋ฉด ์ด๊ธฐ ๋ก๋ ์ฑ๋ฅ์ด ์คํ๋ ค ๋๋น ์ ธ.๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "๋ทฐํฌํธ์ ์ฒ์๋ถํฐ ๋ณด์ด๋ ์ด๋ฏธ์ง ์ค ์ ์ผ ํฐ ๊ฒ ํ๋์๋ง priority."
Q2. Google Fonts๋ฅผ next/font๋ก ๋ง์ด๊ทธ๋ ์ด์
ํ ๋์ ๊ฐ์ฅ ํฐ ์ด์ ์ด ์๋ ๊ฒ์?
- A) ์ธ๋ถ ์๋ฒ ์๋ณต์ด ์ฌ๋ผ์ ธ ํฐํธ ๋ก๋๊ฐ ๋นจ๋ผ์ง๋ค
- B) FOUT/FOIT์ด ์ ๊ฑฐ๋๋ค
- C) ์ฌ์ฉ์ IP๊ฐ Google์ ์ ์ก๋์ง ์์ ํ๋ผ์ด๋ฒ์๊ฐ ๋ณดํธ๋๋ค
- D) ํฐํธ ํ์ผ ์ฉ๋์ด ์๋์ผ๋ก 50% ๊ฐ์ํ๋ค
โ ์ ๋ต: D
ํด์ค: ํฐํธ ํ์ผ ์์ฒด์ ์ฉ๋์
next/font๊ฐ ์ค์ฌ์ฃผ์ง ์์. ๋ค๋งsubsets์weight์ต์ ์ผ๋ก ํ์ํ ๊ฒ๋ง ๋ก๋ํ ์ ์์ด.๐ ํต์ฌ ๊ธฐ์ต๋ฒ: A, B, C๋ ์ง์ง ์ด์ . D๋ ์คํด. "์ฉ๋ ๊ฐ์"๊ฐ ์๋๋ผ "๋ถํ์ํ ์๋ธ์ ์ ์ธ"๊ฐ ์ ํํ ํํ์ด์ผ.
Q3. ์น๊ตฌ์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
sizes์์ฑ ์์ดfill์ ์ฐ๋ฉด ์ ๋ฌธ์ ๊ฐ ์๊ธฐ๋์ง ๋น์ ๋ก ์ค๋ช ํด๋ด.
์์ ๋ต๋ณ:
"ํ๋ฐฐ ๋ฐ์ค ํฌ๊ธฐ๋ฅผ ๋ง ์ ํ๋ฉด ๋ฐฐ๋ฌ ๊ธฐ์ฌ๊ฐ ์ ์ผ ํฐ ๋ฐ์ค๋ก ๋ณด๋ด๋ฒ๋ ค.
sizes์์ดfill์ ์ฐ๋ฉด ๋ธ๋ผ์ฐ์ ๊ฐ '์ผ๋ง๋ ํฐ ์ด๋ฏธ์ง๊ฐ ํ์ํ์ง ๋ชจ๋ฅด๋๊น ์ ์ผ ํฐ ๊ฑธ๋ก ์ฃผ์ธ์'๋ผ๊ณ ์์ฒญํด. ๋ชจ๋ฐ์ผ 375px ํ๋ฉด์ธ๋ฐ 1920px ์ด๋ฏธ์ง๋ฅผ ๋ฐ์์ค๋ ๋ญ๋น๊ฐ ์๊ธฐ๋ ๊ฑฐ์ผ."
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ์ฐ๋ฆฌ ์๋น์ค์ '์ฑ๋ฅ๊ณผ ๋ฏธํ'์ ๋์์ ์ก์ ๋ ์ด์ผ! ๊ทธ๋์ ๊ณ ํ์ง ์ด๋ฏธ์ง๋ฅผ ์ฌ๋ฆด ๋๋ง๋ค ์ฌ์ดํธ๊ฐ ๋ฒ๋ฒ ๊ฑฐ๋ ค์ ๊ณ ๋ฏผ์ด ๋ง์๋๋ฐ, ์ํธ ๋ฆฌ๋ ๋์ด ์๋ ค์ฃผ์ 'next/image' ๋๋ถ์ 5MB ์ฌ์ง๋ 100KB๋๋ก ๊ฐ๋ณ๊ฒ ์๋นํ ์ ์๊ฒ ๋์ด.
๐ก ์ค๋์ ๊ตํ: "์ด๋ฏธ์ง๋ next/image๋ก ๊ฐ๋ณ๊ฒ, ํฐํธ๋ next/font๋ก ํ๋ค๋ฆผ ์์ด! ์ฑ๋ฅ์ด ๊ณง ์ฌ์ฉ์ ๊ฒฝํ์ด๋ค."
ํนํ ํฐํธ๊ฐ ๊น๋นก๊ฑฐ๋ฆฌ๋ ๋ฌธ์ (FOUT)๋ฅผ next/font๋ก ํด๊ฒฐํ์ ๋, ์ฌ์ดํธ๊ฐ ํจ์ฌ ๊ณ ๊ธ์ค๋ฌ์์ง ๊ฑธ ๋๋ผ๋ฉด์ ์ ๋ง ๋ฟ๋ฏํ์ด. ์ฑ๋ฅ ์ต์ ํ๊ฐ ๋จ์ํ ์ซ์๋ฅผ ๋ฎ์ถ๋ ๊ฒ ์๋๋ผ, ์ฌ์ฉ์์๊ฒ ์ ๋ขฐ๋ฅผ ์ฃผ๋ ๊ณผ์ ์ด๋ผ๋ ๊ฑธ ๊นจ๋ฌ์์ด. ์ค๋ ๋ฟ๋ฏํ ๋ง์์ผ๋ก ํด๊ทผํด์ ๋ณด๊ณ ์ถ์๋ ๋ทํ๋ฆญ์ค ์ ์ฃผํ์ด๋ ํด์ผ์ง. ๋ด์ผ์ ๋ '๊ฐ๊ฐ์ ์ธ' ๊ฐ๋ฐ์๊ฐ ๋๋ ๊ฑฐ์ผ! ๐ฃ