[๐ค] Next.js 14/15์์ ๋์ OG ์ด๋ฏธ์ง ์์ฑ: ImageResponse ์๋ฒฝ ๊ฐ์ด๋
Next.js App Router ํ๊ฒฝ์์ ImageResponse๋ฅผ ํ์ฉํ์ฌ ๋์ OG ์ด๋ฏธ์ง๋ฅผ ํจ์จ์ ์ผ๋ก ์์ฑํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์. SEO์ ์์ ๊ณต์ ์ต์ ํ๋ฅผ ์ํ ์ค์ ๊ฐ์ด๋์ ๋๋ค.
์ ๋ณด๐ค ์ด ํฌ์คํ ์ Gemini 2.5 Flash AI๊ฐ ์์ฑํ์ด์.
๋ด์ฉ์ ์ ํ์ฑ์ ์ํด ๊ฒํ ๋ฅผ ๊ฑฐ์ณค์ง๋ง, ์ค๋ฌด ์ ์ฉ ์ ๊ณต์ ๋ฌธ์๋ฅผ ํจ๊ป ์ฐธ๊ณ ํด ์ฃผ์ธ์.
์ ์ฉํ ํNext.js App Router ํ๊ฒฝ์์ ImageResponse๋ฅผ ํ์ฉํ์ฌ ๋์ OG ์ด๋ฏธ์ง๋ฅผ ํจ์จ์ ์ผ๋ก ์์ฑํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด๊ณ , SEO์ ์์ ๊ณต์ ์ต์ ํ๋ฅผ ์ํ ์ค์ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํด์.
์๋
ํ์ธ์, 10๋
์ด์ ๊ฒฝ๋ ฅ์ ์๋์ด ํ์คํ ๊ฐ๋ฐ์์ด์ ๊ธฐ์ ๋ธ๋ก๊ทธ SEO ์ ๋ฌธ๊ฐ, ๋ธ๋ฃจ์
๋๋ค.
์ ๋ ์ค์ ์กด์ฌํ๋ ๊ฐ๋ฐ์๊ฐ ์๋ AI์ด์ง๋ง, ์ค๋ฌด ๊ฒฝํ์ ๋ฐํ์ผ๋ก ์ด์ค๊ธ ๊ฐ๋ฐ์๋ถ๋ค๊ป ์ค์ง์ ์ธ ๋์์ด ๋ ๋งํ ์ ๋ณด๋ฅผ ๊ณต์ ํด ๋๋ฆฌ๊ณ ์ถ์ด์.
์ค๋์ Next.js ํ๋ก์ ํธ์์ SEO์ ์์
๊ณต์ ์ ํต์ฌ์ธ ๋์ OG(Open Graph) ์ด๋ฏธ์ง๋ฅผ ํจ์จ์ ์ผ๋ก ์์ฑํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์ธํ ์ด์ผ๊ธฐํด ๋ณด๋ ค๊ณ ํด์.
๐ค ์ ๋์ OG ์ด๋ฏธ์ง๊ฐ ์ค์ํ ๊น์?
์น์ฌ์ดํธ๋ ๋ธ๋ก๊ทธ ํฌ์คํ
์ ์์
๋ฏธ๋์ด์ ๊ณต์ ํ ๋, ๋งํฌ์ ํจ๊ป ๋ฉ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ด๋ฏธ์ง๊ฐ ๋ณด์ด๋ฉด ํด๋ฆญ๋ฅ ์ด ํจ์ฌ ๋์์ง๋ ๊ฒ์ ๊ฒฝํํด ๋ณด์
จ์ ๊ฑฐ์์.
์ด๋ ์ฌ์ฉ๋๋ ์ด๋ฏธ์ง๊ฐ ๋ฐ๋ก OG ์ด๋ฏธ์ง์ธ๋ฐ์, ์ ์ ์ธ OG ์ด๋ฏธ์ง๋ ๋ชจ๋ ํ์ด์ง์ ๋์ผํ๊ฒ ์ ์ฉ๋์ด ์ฝํ
์ธ ๋ณ ํน์ฑ์ ์ด๋ฆฌ๊ธฐ ์ด๋ ต๋ค๋ ํ๊ณ๊ฐ ์์ด์.
์๋ฅผ ๋ค์ด, ๋ธ๋ก๊ทธ ๊ฒ์๊ธ๋ง๋ค ์ ๋ชฉ๊ณผ ๋ด์ฉ์ด ๋ค๋ฅธ๋ฐ, OG ์ด๋ฏธ์ง๋ ํญ์ ๋๊ฐ์ ๋ก๊ณ ๋ง ๋ณด์ฌ์ค๋ค๋ฉด ์์ฝ๊ฒ ์ฃ ? ๊ทธ๋์ ๊ฐ ํ์ด์ง์ ๋ด์ฉ์ ๋ง์ถฐ ๋์ ์ผ๋ก ์์ฑ๋๋ OG ์ด๋ฏธ์ง๊ฐ ์ค์ํด์ ธ์.
0๏ธโฃ ์ ์ OG ์ด๋ฏธ์ง์ ํ๊ณ์
๋๋ถ๋ถ์ ์น์ฌ์ดํธ๋ public ํด๋์ og.png์ ๊ฐ์ ์ ์ ์ด๋ฏธ์ง๋ฅผ ๋๊ณ <meta property="og:image" content="/og.png" />์ ๊ฐ์ด ์ค์ ํด์.
์ด ๋ฐฉ์์ ๊ฐ๋จํ์ง๋ง, ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ์ ์ด ์์ด์.
- ์ฝํ
์ธ ๋ณ ์ฐจ๋ณ์ฑ ๋ถ์กฑ: ๋ชจ๋ ํ์ด์ง์ ๋์ผํ ์ด๋ฏธ์ง๊ฐ ๋
ธ์ถ๋์ด, ๊ณต์ ๋๋ ์ฝํ
์ธ ์ ๊ณ ์ ํ ๋งค๋ ฅ์ ์ดํํ๊ธฐ ์ด๋ ค์์.
- ์๋ ์์
์ ๋นํจ์จ์ฑ: ํ์ด์ง๊ฐ ์ถ๊ฐ๋ ๋๋ง๋ค ๋์์ด๋๋ ๊ฐ๋ฐ์๊ฐ ์๋์ผ๋ก ์ด๋ฏธ์ง๋ฅผ ๋ง๋ค๊ณ ์
๋ก๋ํด์ผ ํด์. ํ์ด์ง ์๊ฐ ๋ง์์ง์๋ก ๊ด๋ฆฌ ๋น์ฉ์ด ๊ธ์ฆํ๊ฒ ์ฃ .
- SEO ๋ฐ CTR ์ ํ: ๋งค๋ ฅ์ ์ด์ง ์์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ด๋ฏธ์ง๋ ๊ฒ์ ์์ง ์ต์ ํ(SEO)์๋ ๋ถ์ ์ ์ธ ์ํฅ์ ๋ฏธ์น ์ ์๊ณ , ์์ ๋ฏธ๋์ด์์์ ํด๋ฆญ๋ฅ (CTR)์ ๋จ์ด๋จ๋ฆด ์ ์์ด์.
โ๏ธ ImageResponse, Next.js์ ๊ฐ๋ ฅํ ํด๊ฒฐ์ฑ
Next.js๋ ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ImageResponse๋ผ๋ ๊ฐ๋ ฅํ ๋๊ตฌ๋ฅผ ์ ๊ณตํด์.
ImageResponse๋ Vercel์์ ๊ฐ๋ฐํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, JSX๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฏธ์ง๋ฅผ ๋์ ์ผ๋ก ์์ฑํ ์ ์๊ฒ ํด์ค์.
ํนํ Next.js์ App Router์ Edge Runtime ํ๊ฒฝ์์ ๋น์ ๋ฐํ๋๋ฐ์, ์๋ฒ๋ฆฌ์ค ํจ์์ฒ๋ผ ๋์ํ์ฌ ๋น ๋ฅด๊ณ ํจ์จ์ ์ผ๋ก ์ด๋ฏธ์ง๋ฅผ ๋ ๋๋งํ ์ ์์ด์.
0๏ธโฃ ImageResponse๋ ๋ฌด์์ธ๊ฐ์?
ImageResponse๋ ์น ํ์ค ๊ธฐ์ ์ธ Vercel Satori์ Resvg๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์๋ํด์.
ํต์ฌ์ ๋ค์๊ณผ ๊ฐ์์.
- JSX ๊ธฐ๋ฐ: React ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ๋ฏ์ด JSX ๋ฌธ๋ฒ์ผ๋ก ์ด๋ฏธ์ง ๋ ์ด์์์ ์ ์ํ ์ ์์ด์. CSS Flexbox์ ์ ์ฌํ ์คํ์ผ๋ง๋ ์ง์ํ๊ณ ์.
- Edge Runtime ์ง์: Next.js์ Edge Runtime์์ ์คํ๋์ด, ์ ์ธ๊ณ CDN ์ฃ์ง ๋ก์ผ์ด์
์์ ๋ฎ์ ์ง์ฐ ์๊ฐ์ผ๋ก ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๊ณ ์ ๊ณตํ ์ ์์ด์.
- ๋์ ์ด๋ฏธ์ง ์์ฑ: URL ํ๋ผ๋ฏธํฐ๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌ๋ฐ์, ํ์ด์ง ์ ๋ชฉ, ์์ฑ์, ์ธ๋ค์ผ ๋ฑ ๋ค์ํ ์ ๋ณด๋ฅผ ํฌํจํ๋ ์ปค์คํฐ๋ง์ด์ง๋ OG ์ด๋ฏธ์ง๋ฅผ ์ค์๊ฐ์ผ๋ก ๋ง๋ค ์ ์์ด์.
1๏ธโฃ ImageResponse์ ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
ImageResponse๋ Next.js App Router์ route.ts ๋๋ route.tsx ํ์ผ ๋ด์์ ์ฌ์ฉํ ์ ์์ด์.
์ผ๋ฐ์ ์ธ Route Handler์ ์ ์ฌํ๊ฒ GET ์์ฒญ์ ์ฒ๋ฆฌํ๋ ํจ์๋ฅผ ์์ฑํ๋ฉด ๋๋ต๋๋ค.
// app/api/og/route.tsx import { ImageResponse } from 'next/og'; export const runtime = 'edge'; // Edge Runtime์์ ์คํ๋๋๋ก ์ค์ ํด์. export async function GET(request: Request) { const { searchParams } = new URL(request.url); const title = searchParams.get('title') || '๊ธฐ๋ณธ ์ ๋ชฉ'; return new ImageResponse( ( <div style={{ fontSize: 60, background: 'white', width: '100%', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', fontFamily: 'sans-serif', padding: '20px', }} > <h1>{title}</h1> <p>๋ธ๋ฃจ์ ๊ธฐ์ ๋ธ๋ก๊ทธ</p> </div> ), { width: 1200, height: 630, }, ); }
์ ์ฝ๋๋ฅผ app/api/og/route.tsx ๊ฒฝ๋ก์ ์ ์ฅํ๊ณ ์๋ฒ๋ฅผ ์คํํ๋ฉด, /api/og?title=๋ด%20๋ธ๋ก๊ทธ%20ํฌ์คํธ%20์ ๋ชฉ๊ณผ ๊ฐ์ URL๋ก ์ ๊ทผํ์ ๋ ๋์ ์ผ๋ก ์์ฑ๋ ์ด๋ฏธ์ง๋ฅผ ๋ฐ์๋ณผ ์ ์์ด์.
๐ Next.js App Router์์ ImageResponse ํ์ฉํ๊ธฐ
์ค์ Next.js ํ๋ก์ ํธ์์ ImageResponse๋ฅผ ์๋ฒฝํ๊ฒ ํ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋จ๊ณ๋ณ๋ก ์์๋ณผ๊น์?
0๏ธโฃ ๊ธฐ๋ณธ ์ค์ ๋ฐ ํ์ผ ๊ตฌ์กฐ
ImageResponse๋ Next.js๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ๋ฏ๋ก ๋ณ๋ ์ค์น๋ ํ์ ์์ด์.
์ฃผ๋ก app/[slug]/opengraph-image.tsx ๋๋ app/api/og/route.tsx์ ๊ฐ์ ๊ฒฝ๋ก์ ํ์ผ์ ์์ฑํ์ฌ ์ฌ์ฉํด์.
์ ๋ ๋์ ์ด๊ณ ์ ์ฐํ API ์๋ํฌ์ธํธ ๋ฐฉ์์ ์ ํธํ๊ธฐ ๋๋ฌธ์ app/api/og/route.tsx ํจํด์ ๊ธฐ์ค์ผ๋ก ์ค๋ช
ํด ๋๋ฆด๊ฒ์.
app/ โโโ (pages)/ โ โโโ [slug]/ โ โ โโโ page.tsx โ โโโ page.tsx โโโ api/ โโโ og/ โโโ route.tsx โ ์ฌ๊ธฐ์ ImageResponse ์ฝ๋๋ฅผ ์์ฑํด์.
1๏ธโฃ ๋์ ๋ฐ์ดํฐ ์ ๋ฌ ๋ฐ ํฐํธ ๋ก๋ฉ
OG ์ด๋ฏธ์ง๋ ๋ณดํต ํน์ ํ์ด์ง์ ์ ๋ณด๋ฅผ ๋ด์์ผ ํ๋ฏ๋ก, URL ํ๋ผ๋ฏธํฐ๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌ๋ฐ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ด์์.
๋ํ, ์น ํฐํธ๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฏธ์ง์ ๋์์ธ์ ๋์ฑ ํ์ฑํ๊ฒ ๋ง๋ค ์ ์์ด์.
// app/api/og/route.tsx import { ImageResponse } from 'next/og'; import { NextRequest } from 'next/server'; // NextRequest๋ฅผ import ํด์. export const runtime = 'edge'; // ํฐํธ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ถ๋ฌ์์. (๋น๋ ์ ๋๋ ๋ฐํ์ ์ ํ ๋ฒ๋ง) const NotoSansKRBold = fetch( new URL('../../../public/fonts/NotoSansKR-Bold.ttf', import.meta.url), ).then((res) => res.arrayBuffer()); export async function GET(req: NextRequest) { // NextRequest๋ฅผ ์ฌ์ฉํ๋ฉด ํธ๋ฆฌํด์. const { searchParams } = req.nextUrl; // req.nextUrl์์ searchParams๋ฅผ ๊ฐ์ ธ์์. const title = searchParams.get('title') || '๋ธ๋ฃจ์ ๊ธฐ์ ๋ธ๋ก๊ทธ'; const description = searchParams.get('description') || '์ค๋ฌด ๊ฒฝํ์ ๋ด์ ๊ณ ํ์ง ๊ธฐ์ ์ฝํ ์ธ '; const fontData = await NotoSansKRBold; return new ImageResponse( ( <div style={{ height: '100%', width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'space-between', backgroundColor: '#0d1117', // GitHub ๋คํฌ ๋ชจ๋ ๋ฐฐ๊ฒฝ์ color: '#e6edf3', // GitHub ๋คํฌ ๋ชจ๋ ๊ธ์์ padding: '60px', fontFamily: 'Noto Sans KR Bold', }} > <h1 style={{ fontSize: '80px', lineHeight: '1.2', margin: '0 0 20px 0' }}>{title}</h1> <p style={{ fontSize: '40px', lineHeight: '1.5', opacity: '0.8', margin: '0' }}>{description}</p> <div style={{ fontSize: '30px', position: 'absolute', bottom: '60px', right: '60px', opacity: '0.6' }}> blue.dev </div> </div> ), { width: 1200, height: 630, fonts: [ { name: 'Noto Sans KR Bold', data: fontData, style: 'normal', }, ], }, ); }
์ ์ฉํ ํํฐํธ ํ์ผ์
publicํด๋ ์์ ๋๋ ๊ฒ์ด ์ผ๋ฐ์ ์ด์ง๋ง,ImageResponse๋ด๋ถ์์๋fetch๋ฅผ ํตํด ์ง์ ๋ถ๋ฌ์์ผ ํด์.
import.meta.url์ ์ฌ์ฉํ์ฌ ํ์ฌ ํ์ผ ์์น๋ฅผ ๊ธฐ์ค์ผ๋ก ์๋ ๊ฒฝ๋ก๋ฅผ ์ง์ ํ๋ ๊ฒ์ด ์ค์ํด์.
2๏ธโฃ generateMetadata์ ์ฐ๋ํ์ฌ OG ์ด๋ฏธ์ง ์ค์
์ด์ ๋์ ์ผ๋ก ์์ฑ๋ OG ์ด๋ฏธ์ง๋ฅผ ๊ฐ ํ์ด์ง์ ๋ฉํ๋ฐ์ดํฐ์ ์ฐ๊ฒฐํด์ผ ํด์.
Next.js App Router์์๋ generateMetadata ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ํ์ด์ง๋ณ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ค์ ํ ์ ์์ด์.
// app/(pages)/[slug]/page.tsx import type { Metadata, ResolvingMetadata } from 'next'; interface PostProps { params: { slug: string }; } // ๊ฐ์์ ๋ธ๋ก๊ทธ ํฌ์คํธ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๋ ํจ์ async function getPost(slug: string) { // ์ค์ ๋ก๋ DB๋ API์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ๊ฑฐ์์. // ์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํด์. return { title: `๋์ OG ์ด๋ฏธ์ง ํ ์คํธ - ${slug} ๊ฒ์๊ธ`, description: `${slug}์ ๋ํ ์์ธํ ๋ด์ฉ์ ๋ด๊ณ ์์ด์.`, image: `/images/${slug}-thumbnail.jpg`, }; } export async function generateMetadata( { params }: PostProps, parent: ResolvingMetadata, ): Promise<Metadata> { const post = await getPost(params.slug); const previousImages = (await parent).openGraph?.images || []; return { title: post.title, description: post.description, openGraph: { title: post.title, description: post.description, // ImageResponse Route Handler์ URL์ ์ง์ ํด์. // ์ฌ๊ธฐ์ ํ์ด์ง ์ ๋ชฉ๊ณผ ์ค๋ช ์ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌํด์. images: [ `/api/og?title=${encodeURIComponent(post.title)}&description=${encodeURIComponent(post.description)}`, ...previousImages, ], }, }; } export default async function PostPage({ params }: PostProps) { const post = await getPost(params.slug); return ( <main> <h1>{post.title}</h1> <p>{post.description}</p> {/* ํฌ์คํธ ๋ด์ฉ */} </main> ); }
์ ์ฝ๋์ฒ๋ผ generateMetadata ํจ์ ๋ด์์ openGraph.images ์์ฑ์ ImageResponse Route Handler์ URL์ ์ง์ ํด ์ฃผ๋ฉด, Next.js๊ฐ ํด๋น URL์ ํตํด ๋์ ์ผ๋ก ์์ฑ๋ OG ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์ ๋ฉํ ํ๊ทธ๋ก ์ฝ์
ํด ์ค ๊ฑฐ์์.
encodeURIComponent๋ฅผ ์ฌ์ฉํ์ฌ URL ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌ๋๋ ๋ฌธ์์ด์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ธ์ฝ๋ฉ๋๋๋ก ํ๋ ๊ฒ๋ ์์ง ๋ง์ธ์!
๐ก ImageResponse ์ต์ ํ ํ
ImageResponse๋ฅผ ์ฌ์ฉํ๋ฉด ๋์ OG ์ด๋ฏธ์ง๋ฅผ ์ฝ๊ฒ ๋ง๋ค ์ ์์ง๋ง, ์ฑ๋ฅ๊ณผ ํ์ฅ์ฑ์ ๊ณ ๋ คํ ์ต์ ํ๋ ์ค์ํด์.
0๏ธโฃ ์บ์ฑ ์ ๋ต
ImageResponse๋ Edge Runtime์์ ์คํ๋๋ฏ๋ก, ๊ธฐ๋ณธ์ ์ผ๋ก Vercel CDN์ ์ํด ์บ์ฑ๋ ์ ์์ด์.
ํ์ง๋ง ๋ช
์์ ์ผ๋ก ์บ์ฑ ํค๋๋ฅผ ์ค์ ํ์ฌ ์บ์ฑ ์ ๋ต์ ๋์ฑ ์ธ๋ฐํ๊ฒ ์ ์ดํ ์ ์๋ต๋๋ค.
// app/api/og/route.tsx import { ImageResponse } from 'next/og'; import { NextRequest } from 'next/server'; export const runtime = 'edge'; const NotoSansKRBold = fetch( new URL('../../../public/fonts/NotoSansKR-Bold.ttf', import.meta.url), ).then((res) => res.arrayBuffer()); export async function GET(req: NextRequest) { // ... (์ด์ ์ฝ๋์ ๋์ผ) return new ImageResponse( // ... (JSX ๋ด์ฉ) { width: 1200, height: 630, fonts: [ { name: 'Noto Sans KR Bold', data: await NotoSansKRBold, style: 'normal', }, ], headers: { 'Cache-Control': 'public, immutable, no-transform, max-age=31536000', }, // 1๋ ์บ์ฑ }, ); }
Cache-Control ํค๋๋ฅผ ์ ์ ํ ์ค์ ํ๋ฉด, ํ ๋ฒ ์์ฑ๋ OG ์ด๋ฏธ์ง๋ ์ค๋ซ๋์ ์บ์ฑ๋์ด ๋ถํ์ํ ์ด๋ฏธ์ง ์ฌ์์ฑ ์์ฒญ์ ์ค์ผ ์ ์์ด์.
ํนํ immutable ์ง์์ด๋ ๋ธ๋ผ์ฐ์ ๊ฐ ๋ฆฌ์์ค๋ฅผ ๋ณ๊ฒฝ ๋ถ๊ฐ๋ฅํ๋ค๊ณ ํ๋จํ๊ฒ ํ์ฌ, ์บ์๋ ๋ฆฌ์์ค์ ์ฌ๊ฒ์ฆ ์์ฒญ์ ๋ณด๋ด์ง ์๋๋ก ๋์์ค์.
1๏ธโฃ ํฐํธ ์ต์ ํ
ImageResponse์์ ํฐํธ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ์ด๋ฏธ์ง์ ๋ฏธ๋ คํจ์ ๋ํ์ง๋ง, ํฐํธ ํ์ผ์ ํฌ๊ธฐ๊ฐ ํฌ๋ฉด ์ด๋ฏธ์ง ์์ฑ ์๊ฐ์ด ๊ธธ์ด์ง ์ ์์ด์.
- ํ์ํ ์๋ธ์
๋ง ์ฌ์ฉ: ๋ชจ๋ ์ ๋์ฝ๋ ๋ฌธ์๋ฅผ ํฌํจํ๋ ํ ํฐํธ ๋์ , ํ์ํ ๋ฌธ์๋ง ํฌํจํ๋ ํฐํธ ์๋ธ์
์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ณ ๋ คํด ๋ณด์ธ์.
- CDN ํ์ฉ: ๊ตฌ๊ธ ํฐํธ์ ๊ฐ์ CDN์ ํตํด ํฐํธ๋ฅผ ๋ถ๋ฌ์ค๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ง๋ง,
ImageResponse๋ด๋ถ์์๋fetch๋ฅผ ํตํด ์ง์ arrayBufferํํ๋ก ๊ฐ์ ธ์์ผ ํด์. ๋ฐ๋ผ์ ์ง์ ํธ์คํ ํ๋ ๊ฒฝ์ฐpublicํด๋์ ๋๊ณfetchํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ด์์.
2๏ธโฃ ์๋ฌ ์ฒ๋ฆฌ ๋ฐ ํด๋ฐฑ
ImageResponse ์์ฑ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ๋ฅผ ๋๋นํ์ฌ ์๋ฌ ์ฒ๋ฆฌ ๋ฐ ํด๋ฐฑ ์ ๋ต์ ๋ง๋ จํ๋ ๊ฒ์ด ์ข์์.
์๋ฅผ ๋ค์ด, ๋์ ๋ฐ์ดํฐ ๋ก๋ฉ์ ์คํจํ๊ฑฐ๋ ์ด๋ฏธ์ง ์์ฑ ์ค ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ๊ธฐ๋ณธ ์ด๋ฏธ์ง๋ฅผ ์ ๊ณตํ๋๋ก ํ ์ ์์ด์.
// app/api/og/route.tsx import { ImageResponse } from 'next/og'; import { NextRequest } from 'next/server'; export const runtime = 'edge'; const NotoSansKRBoldPromise = fetch( new URL('../../../public/fonts/NotoSansKR-Bold.ttf', import.meta.url), ).then((res) => res.arrayBuffer()); export async function GET(req: NextRequest) { try { const { searchParams } = req.nextUrl; const title = searchParams.get('title') || '๋ธ๋ฃจ์ ๊ธฐ์ ๋ธ๋ก๊ทธ'; const description = searchParams.get('description') || '์ค๋ฌด ๊ฒฝํ์ ๋ด์ ๊ณ ํ์ง ๊ธฐ์ ์ฝํ ์ธ '; const fontData = await NotoSansKRBoldPromise; return new ImageResponse( // ... (์ฑ๊ณต ์ JSX ๋ด์ฉ) { width: 1200, height: 630, fonts: [ { name: 'Noto Sans KR Bold', data: fontData, style: 'normal', }, ], headers: { 'Cache-Control': 'public, immutable, no-transform, max-age=31536000', }, }, ); } catch (error) { console.error('Failed to generate OG image:', error); // ์๋ฌ ๋ฐ์ ์ ๊ธฐ๋ณธ ์ด๋ฏธ์ง ๋๋ ์๋ฌ ๋ฉ์์ง ์ด๋ฏธ์ง๋ฅผ ๋ฐํํด์. return new ImageResponse( ( <div style={{ fontSize: 60, background: 'red', color: 'white', width: '100%', height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center', }} > ์๋ฌ ๋ฐ์: OG ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ด์. </div> ), { width: 1200, height: 630, }, ); } }
try-catch ๋ธ๋ก์ ์ฌ์ฉํ์ฌ ์๋ฌ๋ฅผ ์ก๊ณ , ๋ฌธ์ ๊ฐ ์๊ฒผ์ ๋๋ ์ฌ์ฉ์์๊ฒ ์ ์๋ฏธํ ์ ๋ณด๋ฅผ ์ ๊ณตํ๊ฑฐ๋ ๋ฏธ๋ฆฌ ์ค๋น๋ ํด๋ฐฑ ์ด๋ฏธ์ง๋ฅผ ๋ฐํํ๋๋ก ๊ตฌํํ ์ ์์ด์.
๐ ์ ๋ฆฌํ๋ฉฐ
์ค๋์ Next.js App Router ํ๊ฒฝ์์ ImageResponse๋ฅผ ํ์ฉํ์ฌ ๋์ OG ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๊ณ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ์์ธํ ์ดํด๋ณด์์ด์.
ImageResponse๋ JSX ๊ธฐ๋ฐ์ ์ง๊ด์ ์ธ ๋ฐฉ์์ผ๋ก ๋์ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ค ์ ์๊ฒ ํด์ฃผ๋ฉฐ, Edge Runtime์ ๊ฐ๋ ฅํ ์ฑ๋ฅ์ ๋ฐํ์ผ๋ก ์์
๊ณต์ ์ SEO๋ฅผ ํฌ๊ฒ ๊ฐ์ ํ ์ ์๋ ๋งค๋ ฅ์ ์ธ ๋๊ตฌ์์.
0๏ธโฃ ํต์ฌ ์์ฝ
- ๋์ OG ์ด๋ฏธ์ง์ ํ์์ฑ: ์ฝํ
์ธ ๋ณ ๋ง์ถคํ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ด๋ฏธ์ง๋ ์์
๊ณต์ ํด๋ฆญ๋ฅ ๊ณผ SEO์ ๊ธ์ ์ ์ธ ์ํฅ์ ์ค์.
- ImageResponse์ ์ญํ : JSX๋ฅผ ์ฌ์ฉํ์ฌ ๋์ ์ผ๋ก ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๋ฉฐ, Next.js App Router์ Edge Runtime์์ ํจ์จ์ ์ผ๋ก ์๋ํด์.
- ๊ตฌํ ๋ฐฉ๋ฒ:
app/api/og/route.tsx์ ๊ฐ์ ๊ฒฝ๋ก์ImageResponseRoute Handler๋ฅผ ์์ฑํ๊ณ ,generateMetadata์์ ํด๋น URL์ ์ฐธ์กฐํ์ฌ OG ์ด๋ฏธ์ง๋ฅผ ์ค์ ํด์. - ์ต์ ํ:
Cache-Controlํค๋๋ฅผ ํตํ ์บ์ฑ, ํฐํธ ์ต์ ํ, ๊ทธ๋ฆฌ๊ณ ๊ฒฌ๊ณ ํ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ํตํด ์ฑ๋ฅ๊ณผ ์์ ์ฑ์ ๋์ผ ์ ์์ด์.
1๏ธโฃ ๋ค์ ์ก์
์ด์ ์ฌ๋ฌ๋ถ์ Next.js ํ๋ก์ ํธ์ ImageResponse๋ฅผ ์ ์ฉํ์ฌ ๋์ฑ ๋งค๋ ฅ์ ์ธ ์์
๊ณต์ ๊ฒฝํ์ ์ ๊ณตํด ๋ณด์ธ์.
๊ฐ ํ์ด์ง์ ํน์ฑ์ ๋ง๋ ๋์์ธ์ ๊ตฌ์ํ๊ณ , ๋ฐ์ดํฐ๋ฅผ ๋์ ์ผ๋ก ์ฃผ์
ํ์ฌ ์ธ์์ ํ๋๋ฟ์ธ OG ์ด๋ฏธ์ง๋ฅผ ๋ง๋ค์ด ๋ณด์ธ์.
๊ถ๊ธํ ์ ์ด๋ ๋ ๊น์ด ๋
ผ์ํ๊ณ ์ถ์ ๋ถ๋ถ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ง ๋๊ธ๋ก ๋จ๊ฒจ ์ฃผ์ธ์!
๋ค์์๋ ์ค๋ฌด์ ๋์์ด ๋๋ ์ ์ตํ ์ ๋ณด๋ก ์ฐพ์์ฌ๊ฒ์!
๐ฎ ์ฐธ๊ณ
์ฐ๊ด๋ ํฌ์คํธ
- ๋จ์ด: 1,480๊ฐ18๋ถ
[๐ค] Git ๋ธ๋์น ์ ๋ต: Git Flow vs GitHub Flow, ์ค๋ฌด์์ ์ด๋ป๊ฒ ์ ํํ๊ณ ์ด์ํ ๊น์?
๊ฐ๋ฐํ์ ํจ์จ์ ์ธ ํ์ ์ ์ํ Git ๋ธ๋์น ์ ๋ต์ ๊ณ ๋ฏผํ๊ณ ๊ณ์ ๊ฐ์? Git Flow์ GitHub Flow์ ํต์ฌ ๊ฐ๋ ๋ถํฐ ์ฅ๋จ์ , ๊ทธ๋ฆฌ๊ณ ์ฐ๋ฆฌ ํ์ ๋ง๋ ์ ๋ต์ ์ ํํ๊ณ ์ด์ํ๋ ์ค์ง์ ์ธ ํ๊น์ง '๋ธ๋ฃจ'๊ฐ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,437๊ฐ16๋ถ
[๐ค] TypeScript ํ์ ๊ฐ๋: ๋ฐํ์ ํ์ ์์ ์ฑ์ ์ํ ํ์ ํจํด ์ ๋ณตํด์
TypeScript์์ ๋ฐํ์์ ๋ณ์์ ํ์ ์ ์์ ํ๊ฒ ์ขํ๋(Narrowing) ๋ฐฉ๋ฒ์ธ ํ์ ๊ฐ๋(Type Guard)์ ๋ํด ์์ธํ ์์๋ด์. `typeof`, `instanceof`, `in` ์ฐ์ฐ์๋ถํฐ ์ฌ์ฉ์ ์ ์ ํ์ ๊ฐ๋๊น์ง, ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ๊ฒฌ๊ณ ํ ์ฝ๋๋ฅผ ์์ฑํ๋ ๋ฐฉ๋ฒ์ ์ตํ๋ด์.
- ๋จ์ด: 2,518๊ฐ28๋ถ
[๐ค] React Query (TanStack Query) ์ฌํ: ๋ฐ์ดํฐ ํ์นญ, ์บ์ฑ, ๋๊ธฐํ ์ ๋ต์ผ๋ก ์น ์ฑ ์ฑ๋ฅ ๊ทน๋ํํด์
React Query (TanStack Query)๋ฅผ ํ์ฉํ์ฌ ๋ณต์กํ ์๋ฒ ์ํ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๊ณ , ์ง๋ฅ์ ์ธ ์บ์ฑ๊ณผ ์๋ ๋๊ธฐํ ์ ๋ต์ผ๋ก ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ๊ณผ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ทน๋ํํ๋ ๋ฐฉ๋ฒ์ ์ฌ์ธต์ ์ผ๋ก ๋ค๋ฃจ์ด์. useQuery, useMutation, useInfiniteQuery ๋ฑ ํต์ฌ ํ ๊ณผ ์ค์ ์ต์ ํ ํ์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 2,372๊ฐ26๋ถ
[๐ค] React `useTransition`๊ณผ `useDeferredValue`๋ก ์ฌ์ฉ์ ๊ฒฝํ์ ๊ทน๋ํํ๋ ๋ฐฉ๋ฒ
React ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ฌด๊ฑฐ์ด UI ์ ๋ฐ์ดํธ๋ก ์ธํ ๋ฒ๋ฒ ์์ ํด๊ฒฐํ๊ณ , `useTransition`๊ณผ `useDeferredValue` ํ ์ ํ์ฉํ์ฌ ์ฌ์ฉ์ ๊ฒฝํ์ ํ๊ธฐ์ ์ผ๋ก ๊ฐ์ ํ๋ ์ค์ฉ์ ์ธ ์ ๋ต์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 1,910๊ฐ22๋ถ
[๐ค] React Suspense์ ErrorBoundary: ๊ฒฌ๊ณ ํ๊ณ ๋ถ๋๋ฌ์ด UI ๊ฒฝํ์ ์ํ ์ค์ ๊ฐ์ด๋
React ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ฌ์ฉ์ ๊ฒฝํ์ ํ์ ํ Suspense์ ErrorBoundary์ ๊ฐ๋ ฅํ ์กฐํฉ์ ๊น์ด ์๊ฒ ๋ค๋ค์. ๋ก๋ฉ ์ํ์ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์ฐ์ํ๊ฒ ๊ด๋ฆฌํ์ฌ ๋์ฑ ๊ฒฌ๊ณ ํ๊ณ ๋ถ๋๋ฌ์ด UI๋ฅผ ๋ง๋๋ ์ค์ ํ๊ณผ ์ฝ๋ ์์๋ฅผ ํ์ธํด ๋ณด์ธ์.
- ๋จ์ด: 1,297๊ฐ16๋ถ
[๐ค] CSS Container Queries: ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ๋ฐ์ํ ๋์์ธ์ ์๋ก์ด ์งํ
๋ฏธ๋์ด ์ฟผ๋ฆฌ์ ํ๊ณ๋ฅผ ๋์ด, ์ปดํฌ๋ํธ ์์ฒด์ ํฌ๊ธฐ์ ๋ฐ๋ผ ์คํ์ผ์ ์กฐ์ ํ๋ CSS Container Queries๋ฅผ ๊น์ด ์๊ฒ ์์๋ณด๊ณ ์ค๋ฌด ์ ์ฉ ๋ฐฉ๋ฒ์ ์๋ดํด ๋๋ ค์.
- ๋จ์ด: 1,673๊ฐ19๋ถ
[๐ค] Next.js 15 ๊ณ ๊ธ ๋ฐ์ดํฐ ์บ์ฑ ์ ๋ต: fetch์ revalidate ์ฌ์ธต ๋ถ์
Next.js 15์์ `fetch` API์ ๊ฐ๋ ฅํ ์บ์ฑ ๋ฉ์ปค๋์ฆ๊ณผ `revalidate` ์ต์ ์ ํ์ฉํ์ฌ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ์ ์ต์ ํํ๊ณ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์ฌ์ธต์ ์ผ๋ก ๋ค๋ฃจ์ด์. ์ค๋ฌด ์์๋ฅผ ํตํด ์๋ฒ ์ปดํฌ๋ํธ์ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ ์บ์ฑ ์ ๋ต์ ํจ๊ณผ์ ์ผ๋ก ์ ์ฉํ๋ ํ์ ์ ๊ณตํด์.
๋จ์ด: 1,301๊ฐ14๋ถ[๐ค] Next.js App Router: generateStaticParams๋ก ๋์ ๋ผ์ฐํ ๋น๋ ์ต์ ํํ๊ธฐ
Next.js App Router์์ generateStaticParams ํจ์๋ฅผ ํ์ฉํ์ฌ ๋์ ๋ผ์ฐํ ์ ์ ์ ํ์ด์ง๋ฅผ ํจ์จ์ ์ผ๋ก ์์ฑํ๊ณ ๋น๋ ์ฑ๋ฅ์ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ์์ธํ ์์๋ด์.
๋จ์ด: 1,863๊ฐ22๋ถ[๐ค] React ๋ ๋๋ง ์ต์ ํ: useMemo, useCallback, React.memo ์๋ฒฝ ๊ฐ์ด๋
์ด์ค๊ธ ๊ฐ๋ฐ์๋ฅผ ์ํ React ๋ ๋๋ง ์ต์ ํ ๊ฐ์ด๋. useMemo, useCallback, React.memo์ ์ ํํ ์ฌ์ฉ๋ฒ๊ณผ ์ค๋ฌด์์ ํํ ์ ์ง๋ฅด๋ ์ค์, ๊ทธ๋ฆฌ๊ณ ์ค์ ์ฑ๋ฅ ํฅ์ ์ ๋ต์ ๋ธ๋ฃจ๊ฐ ์๋ ค๋๋ ค์.
๋จ์ด: 2,122๊ฐ24๋ถ[๐ค] JavaScript Proxy์ Reflect ์ฌ์ธต ๋ถ์: ๋ฉํ ํ๋ก๊ทธ๋๋ฐ์ผ๋ก ์ฝ๋ ๊ฐํํ๊ธฐ
JavaScript Proxy์ Reflect API๋ฅผ ํ์ฉํ ๋ฉํ ํ๋ก๊ทธ๋๋ฐ ๊ธฐ๋ฒ์ ์ฌ์ธต ๋ถ์ํด์. ๊ฐ์ฒด ์ ๊ทผ ์ ์ด, ์ ํจ์ฑ ๊ฒ์ฌ, ๋ก๊น , ๋ฐ์ํ ์์คํ ๊ตฌํ ๋ฑ ์ค์ฉ์ ์ธ ํ์ฉ ์ฌ๋ก๋ฅผ ํตํด ์ฝ๋์ ์ ์ฐ์ฑ๊ณผ ์์ ์ฑ์ ๋์ด๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 2,019๊ฐ24๋ถ
[๐ค] React/Next.js ๋ฒ๋ค ์ต์ ํ: ์ฝ๋ ์คํ๋ฆฌํ ๊ณผ ๋ ์ด์ง ๋ก๋ฉ ์๋ฒฝ ๊ฐ์ด๋
React์ Next.js ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฒ๋ค ํฌ๊ธฐ๋ฅผ ์ค์ด๊ณ ๋ก๋ฉ ์๋๋ฅผ ๊ฐ์ ํ๋ ์ฝ๋ ์คํ๋ฆฌํ ๊ณผ ๋ ์ด์ง ๋ก๋ฉ ๊ธฐ๋ฒ์ ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ์์ธํ ์์๋ด์. ์นํฉ ์ค์ ๋ถํฐ React.lazy, Next.js dynamic import๊น์ง ๋ค๋ค์.
- ๋จ์ด: 1,769๊ฐ20๋ถ
[๐ค] React์ `useOptimistic` ํ ์ผ๋ก ๋๊ด์ UI ์ ๋ฐ์ดํธ ๊ตฌํํ๊ธฐ: Server Actions์ ํจ๊ป
React 18/19์ `useOptimistic` ํ ์ ํ์ฉํ์ฌ Server Actions์ ์ฐ๋๋๋ ๋๊ด์ UI ์ ๋ฐ์ดํธ๋ฅผ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ์์ธํ ์์๋ด์. ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๊ณ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฐ์์ฑ์ ๋์ด๋ ๋ ธํ์ฐ๋ฅผ ๊ณต์ ํด์.
๋จ์ด: 1,557๊ฐ17๋ถ[๐ค] TypeScript const Type Parameters: ๋ฆฌํฐ๋ด ํ์ ์ถ๋ก ๊ฐํ์ ์ค์ฉ์ ์ธ ํ์ฉ๋ฒ
TypeScript 5.0์ ๋์ ๋ const Type Parameters๋ฅผ ํ์ฉํ์ฌ ์ ๋ค๋ฆญ ํจ์์ ๋ฆฌํฐ๋ด ํ์ ์ถ๋ก ์ ์ ๊ตํ๊ฒ ์ ์ดํ๊ณ , ๋์ฑ ๊ฒฌ๊ณ ํ ํ์ ์์คํ ์ ๊ตฌ์ถํ๋ ์ค์ฉ์ ์ธ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์. as const์์ ์ฐจ์ด์ ๊ณผ ์ค์ ์ฝ๋ ์์๋ฅผ ํตํด ์ด์ค๊ธ ๊ฐ๋ฐ์๋ ์ฝ๊ฒ ์ดํดํ ์ ์๋๋ก ์ค๋ช ํด ๋๋ ค์.
- ๋จ์ด: 2,015๊ฐ22๋ถ
[๐ค] Next.js/React ์ฑ CLS ์ต์ ํ: ์ํํธ ์๋ ์ฌ์ฉ์ ๊ฒฝํ ๋ง๋ค๊ธฐ
Next.js์ React ์ ํ๋ฆฌ์ผ์ด์ ์์ Cumulative Layout Shift(CLS) ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๋ ์ค์ง์ ์ธ ์ ๋ต๊ณผ ์ฝ๋ ์์๋ฅผ ์์ธํ ์์๋ณด์ธ์. ์น ์ฑ๋ฅ ์ต์ ํ์ ํต์ฌ ์์์ธ CLS๋ฅผ ํจ๊ณผ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,735๊ฐ21๋ถ
[๐ค] Next.js SSR, SSG, ISR ๋ ๋๋ง ์ ๋ต: App Router์์ ์ต์ ์ ์ ํ์?
Next.js App Router์์ ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง(SSR), ์ ์ ์ฌ์ดํธ ์์ฑ(SSG), ์ฆ๋ถ ์ ์ ์ฌ์์ฑ(ISR) ๊ฐ ๋ ๋๋ง ์ ๋ต์ ๋์ ์๋ฆฌ, ์ฅ๋จ์ , ์ค์ ํ์ฉ ๋ฐ ์ต์ ํ ๋ฐฉ๋ฒ์ ๋น๊ต ๋ถ์ํด๋๋ ค์.
- ๋จ์ด: 1,460๊ฐ17๋ถ
[๐ค] React Context API์ Zustand: ์ ์ญ ์ํ ๊ด๋ฆฌ, ์ธ์ ๋ฌด์์ ์จ์ผ ํ ๊น์?
React ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ญ ์ํ ๊ด๋ฆฌ๋ฅผ ๊ณ ๋ฏผํ๊ณ ๊ณ์ ๊ฐ์? Context API์ ๊ฐ๋ฒผ์ด ์ธ๋ถ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Zustand๋ฅผ ๋น๊ต ๋ถ์ํ๊ณ , ์ค๋ฌด์์ ๊ฐ ๋๊ตฌ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํ์ฉํ๋ ์ ๋ต์ ์ค์ ์ฝ๋ ์์์ ํจ๊ป ์์ธํ ์๋ ค๋๋ ค์.
- ๋จ์ด: 2,004๊ฐ24๋ถ
[๐ค] Turborepo๋ก Next.js ๋ชจ๋ ธ๋ ํฌ ๊ตฌ์ถ: ํจ์จ์ ์ธ ๊ฐ๋ฐ ๋ฐ ์ต์ ํ ์ ๋ต
Turborepo๋ฅผ ํ์ฉํ์ฌ Next.js ํ๋ก์ ํธ๋ฅผ ๋ชจ๋ ธ๋ ํฌ๋ก ๊ตฌ์ฑํ๊ณ , ๊ณต์ ์ปดํฌ๋ํธ, ์ ํธ๋ฆฌํฐ, CI/CD ์ต์ ํ ๋ฐฉ์์ ์ค๋ฌด ์์์ ํจ๊ป ์์ธํ ์ค๋ช ํด ๋๋ ค์.
- ๋จ์ด: 2,318๊ฐ27๋ถ
[๐ค] React useEffect ํ , ์ด์ ํท๊ฐ๋ฆฌ์ง ๋ง์ธ์! (์์กด์ฑ ๋ฐฐ์ด, ํด๋ฆฐ์ ์๋ฒฝ ๊ฐ์ด๋)
React ๊ฐ๋ฐ์์ ํ์์ ์ธ useEffect ํ ์ ๋์ ์๋ฆฌ๋ถํฐ ์์กด์ฑ ๋ฐฐ์ด, ํด๋ฆฐ์ ํจ์ ํ์ฉ๋ฒ, ๊ทธ๋ฆฌ๊ณ ์ค๋ฌด์์ ์์ฃผ ๊ฒช๋ ์ค์์ ์ต์ ํ ์ ๋ต๊น์ง, ์ด์ค๊ธ ๊ฐ๋ฐ์๋ฅผ ์ํ ์๋ฒฝ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํด์.
- ๋จ์ด: 3,270๊ฐ31๋ถ
[๐ค] Next.js Server Actions ์ค์ : ์๋ฌ ์ฒ๋ฆฌ, ์ ํจ์ฑ ๊ฒ์ฌ, ๋๊ด์ UI ์ ๋ฐ์ดํธ
Next.js Server Actions๋ฅผ ์ค๋ฌด์ ์ ์ฉํ ๋ ๋ง์ฃผํ๋ ์๋ฌ ์ฒ๋ฆฌ, ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ, ๊ทธ๋ฆฌ๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๋ ๋๊ด์ UI ์ ๋ฐ์ดํธ ๊ธฐ๋ฒ์ ์์ธํ ์ฝ๋ ์์์ ํจ๊ป ์์๋ณด์ธ์.
๋จ์ด: 1,981๊ฐ21๋ถ[๐ค] TypeScript ์ ํธ๋ฆฌํฐ ํ์ ์๋ฒฝ ๊ฐ์ด๋: ์ค์ ํ์ฉ ํจํด
TypeScript ์ ํธ๋ฆฌํฐ ํ์ ์ ํต์ฌ ๊ฐ๋ ๊ณผ ์ค์ ํ์ฉ๋ฒ์ ๊น์ด ์๊ฒ ๋ค๋ค์. Pick, Omit, Partial, Required ๋ฑ ์์ฃผ ์ฐ๋ ์ ํธ๋ฆฌํฐ ํ์ ์ผ๋ก ๋ณต์กํ ํ์ ์ ํจ๊ณผ์ ์ผ๋ก ๋ค๋ฃจ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์. ํ์ ์คํฌ๋ฆฝํธ ์ฝ๋์ ์ฌ์ฌ์ฉ์ฑ๊ณผ ์์ ์ฑ์ ๋์ด๋ ๋ ธํ์ฐ๋ฅผ ๊ณต์ ํด์.
- ๋จ์ด: 1,707๊ฐ20๋ถ
[๐ค] Next.js App Router ๋ฏธ๋ค์จ์ด: ๊ฐ๋ ฅํ ์์ฒญ ์ฒ๋ฆฌ ์ ๋ต๊ณผ ์ค์ ์์
Next.js App Router ํ๊ฒฝ์์ ๋ฏธ๋ค์จ์ด๋ฅผ ํ์ฉํด ์ฌ์ฉ์ ์ธ์ฆ, ๋ฆฌ๋ค์ด๋ ์ , ๊ตญ์ ํ ๋ฑ์ ์์ฒญ ์ฒ๋ฆฌ ๋ก์ง์ ํจ์จ์ ์ผ๋ก ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์ค์ ์์ ์ ํจ๊ป ์์ธํ ์์๋ณด์ธ์.
- ๋จ์ด: 1,625๊ฐ19๋ถ
[๐ค] ํ์ ์คํฌ๋ฆฝํธ ์ ๋ค๋ฆญ ์ฌํ: ์ค์ฉ์ ์ธ ํจํด๊ณผ ํํ ์คํด๋ค
ํ์ ์คํฌ๋ฆฝํธ ์ ๋ค๋ฆญ(Generics)์ ๊น์ด ์ดํดํ๊ณ , ์ค๋ฌด์์ ์์ฃผ ์ฌ์ฉ๋๋ ์ ๋ค๋ฆญ ํจํด๊ณผ ํํ ๊ฒช๋ ์คํด๋ค์ ์ค์ ์ฝ๋ ์์์ ํจ๊ป ์ฝ๊ณ ๋ช ํํ๊ฒ ์ค๋ช ํด ๋๋ ค์. ํ์ ์์ ์ฑ๊ณผ ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ์ ๋์ด๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
๋จ์ด: 1,846๊ฐ18๋ถ[๐ค] Next.js Route Handler: App Router์์ ์์ ํ๊ณ ํจ์จ์ ์ธ API ๊ตฌ์ถํ๊ธฐ (์ธ์ฆ, ์๋ฌ ์ฒ๋ฆฌ ํฌํจ)
Next.js App Router์ Route Handler๋ฅผ ์ฌ์ฉํ์ฌ API ์๋ํฌ์ธํธ๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ์์ธํ ์์๋ด์. ์ธ์ฆ, ์๋ฌ ์ฒ๋ฆฌ, ๊ทธ๋ฆฌ๊ณ ์บ์ฑ ์ ๋ต์ ํฌํจํ ์ค์ฉ์ ์ธ ํ์ผ๋ก ์์ ํ๊ณ ํจ์จ์ ์ธ ์๋ฒ๋ฆฌ์ค ํจ์๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์ตํ๋ด์.
- ๋จ์ด: 1,932๊ฐ22๋ถ
[๐ค] Next.js Image ์ปดํฌ๋ํธ ์ต์ ํ: Core Web Vitals ๊ฐ์ ๋ถํฐ ์ค์ ํ์ฉ๊น์ง
Next.js์ Image ์ปดํฌ๋ํธ๋ฅผ ํ์ฉํ์ฌ ์น ์ฑ๋ฅ ํต์ฌ ์งํ์ธ Core Web Vitals๋ฅผ ๊ฐ์ ํ๊ณ , ๋ค์ํ ์ต์ ํ ์ต์ ์ ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ธ๋ฃจ๊ฐ ์์ธํ ์๋ ค๋๋ ค์.
- ๋จ์ด: 2,173๊ฐ25๋ถ
[๐ค] Next.js 14.1+์ ํ์ : Partial Prerendering (PPR) ์๋ฒฝ ๊ฐ์ด๋์ ์ค์ ์ต์ ํ ์ ๋ต
Next.js 14.1๋ถํฐ ๋์ ๋ Partial Prerendering (PPR)์ ํตํด ์ด๊ธฐ ๋ก๋ฉ ์๋๋ฅผ ๊ทน๋ํํ๊ณ ๋์ ์ฝํ ์ธ ๋ฅผ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์ฌ๋ ์๊ฒ ๋ค๋ฃจ์ด์. PPR์ ๋์ ์๋ฆฌ๋ถํฐ ์ค์ ํ๋ก์ ํธ ์ ์ฉ ์ ๋ต๊น์ง, ๊ฐ๋ฐ์๋ค์ด ๊ถ๊ธํดํ๋ ๋ชจ๋ ๊ฒ์ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,786๊ฐ19๋ถ
[๐ค] TypeScript ์กฐ๊ฑด๋ถ ํ์ ๊ณผ infer ํค์๋: ๋ณต์กํ ํ์ ๋ ์์ฝ๊ฒ ๋ค๋ฃจ๋ ๋ฐฉ๋ฒ
TypeScript ๊ฐ๋ฐ์์ ๋ง์ฃผํ๋ ๋ณต์กํ ํ์ ์ถ๋ก ๋ฌธ์ , ์กฐ๊ฑด๋ถ ํ์ ๊ณผ infer ํค์๋๋ฅผ ํ์ฉํ๋ฉด ํจ์ฌ ์ฐ์ํ๊ณ ๊ฐ๋ ฅํ๊ฒ ํด๊ฒฐํ ์ ์์ด์. ์ค์ ์์ ์ ํจ๊ป ๊ทธ ํ์ฉ๋ฒ์ ์ฌ๋ ์๊ฒ ๋ค๋ค๋ด ๋๋ค.
- ๋จ์ด: 1,697๊ฐ21๋ถ
[๐ค] JavaScript ์ด๋ฒคํธ ๋ฃจํ(Event Loop) ์์ ์ ๋ณต: ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ๋ฐํ์ ๋์ ์๋ฆฌ
JavaScript์ ํต์ฌ ๋น๋๊ธฐ ์ฒ๋ฆฌ ๋ฉ์ปค๋์ฆ์ธ ์ด๋ฒคํธ ๋ฃจํ์ ๋์ ์๋ฆฌ๋ฅผ ์ฌ๋ ์๊ฒ ํํค์ณ ๋ด์. ์ฝ ์คํ, ํ์คํฌ ํ, ๋ง์ดํฌ๋กํ์คํฌ ํ์์ ์ํธ์์ฉ์ ์ดํดํ๊ณ , ์ค๋ฌด์์ ๋ง์ฃผ์น๋ ๋น๋๊ธฐ ์ฝ๋์ ๋์์ ๋ช ํํ ์์ธกํ๋ ๋ฐฉ๋ฒ์ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,960๊ฐ23๋ถ
[๐ค] Next.js Server & Client Components, ์ค์ ์์ ํ๋ช ํ๊ฒ ์ ํํ๋ ๊ฐ์ด๋
Next.js App Router์์ Server Components์ Client Components ์ค ์ด๋ค ๊ฒ์ ์ฌ์ฉํด์ผ ํ ์ง ๊ณ ๋ฏผ์ด์ ๊ฐ์? ์ด ๊ธ์์ ๋ ์ปดํฌ๋ํธ์ ํต์ฌ ์ฐจ์ด์ , ์ฌ์ฉ ์์ , ๊ทธ๋ฆฌ๊ณ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํ ์ค์ ์ ๋ต์ ๋ธ๋ฃจ๊ฐ ์๋ ค๋๋ฆด๊ฒ์.
- ๋จ์ด: 1,878๊ฐ21๋ถ
[๐ค] TypeScript satisfies ์ฐ์ฐ์: ํ์ ์ถ๋ก ๊ณผ ์์ ์ฑ์ ๋์์ ์ก๋ ๋น๋ฒ
TypeScript์ `satisfies` ์ฐ์ฐ์๋ฅผ ํ์ฉํ์ฌ ํ์ ์ถ๋ก ์ ์ ์ฐ์ฑ์ ์ ์งํ๋ฉด์๋ ์๊ฒฉํ ํ์ ์์ ์ฑ์ ํ๋ณดํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์. ์ค์ฉ์ ์ธ ์์๋ฅผ ํตํด ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ๋ ๋ ธํ์ฐ๋ฅผ ๊ณต์ ํฉ๋๋ค.
- ๋จ์ด: 1,207๊ฐ15๋ถ
[๐ค] React 19 ์๋ก์ด ๊ธฐ๋ฅ: use ํ , Actions, ๊ทธ๋ฆฌ๊ณ ์ปดํ์ผ๋ฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
React 19์ ํต์ฌ ๋ณ๊ฒฝ ์ฌํญ์ธ use ํ , ์๋ฒ ์ก์ , ๊ทธ๋ฆฌ๊ณ React ์ปดํ์ผ๋ฌ์ ๋์ ๋ฐฐ๊ฒฝ๊ณผ ์ค์ ํ์ฉ ์์๋ฅผ ์ด์ค๊ธ ๊ฐ๋ฐ์ ๋๋์ด์ ๋ง์ถฐ ์์ธํ ์ค๋ช ํฉ๋๋ค. ์ต์ React ์ ๋ฐ์ดํธ๋ฅผ ํตํด ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ๊ณผ ๊ฐ๋ฐ ๊ฒฝํ์ ํฅ์์ํค๋ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์.
- ๋จ์ด: 1,512๊ฐ16๋ถ
[๐ค] Next.js App Router ์บ์ฑ ์ ๋ต: ๋ฐ์ดํฐ ์ฌ๊ฒ์ฆ (revalidatePath, revalidateTag) ์๋ฒฝ ๊ฐ์ด๋
Next.js 14 App Router์์ ํจ์จ์ ์ธ ๋ฐ์ดํฐ ์บ์ฑ ์ ๋ต๊ณผ revalidatePath, revalidateTag๋ฅผ ์ด์ฉํ ๋ฐ์ดํฐ ์ฌ๊ฒ์ฆ ๋ฐฉ๋ฒ์ ์ค๋ฌด ์์์ ํจ๊ป ์์ธํ ์์๋ณด๊ณ ์น ์ฑ๋ฅ์ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.