๐Ÿ“– TIL - Next.js์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ๋“ค

์Š˜ยท2025๋…„ 3์›” 18์ผ

๐Ÿ“– TIL

๋ชฉ๋ก ๋ณด๊ธฐ
75/89

๐Ÿ”„ ํŽ˜์ด์ง€ ์ด๋™ ๋ฐฉ์‹

  • Link ์ปดํฌ๋„ŒํŠธ๋Š” ์ •๋ง ํŽธ๋ฆฌํ•˜๊ฒŒ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๋‹ค. href์— ๊ฒฝ๋กœ๋งŒ ๋„ฃ์–ด์ฃผ๋ฉด ๊ทธ ํŽ˜์ด์ง€๋ฅผ ๋ฏธ๋ฆฌ ๋ถˆ๋Ÿฌ์™€์ค˜์„œ(prefetching) ํด๋ฆญํ–ˆ์„ ๋•Œ ๋ฐ”๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ์Œ
  • ํ™”๋ฉด์— ๋ณด์ด๋Š” ๋งํฌ๋“ค์€ ์•Œ์•„์„œ ๋ฏธ๋ฆฌ ๋กœ๋“œํ•ด๋‘ฌ์„œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํ›จ์”ฌ ์ข‹์•„์ง
  • ํŽ˜์ด์ง€ ์ „์ฒด๋ฅผ ์ƒˆ๋กœ๊ณ ์นจ ์—†์ด ์ด๋™ํ•  ์ˆ˜ ์žˆ์–ด์„œ ์•ฑ์ฒ˜๋Ÿผ ๋ถ€๋“œ๋Ÿฌ์šด ์ „ํ™˜ ํšจ๊ณผ๊ฐ€ ์ƒ๊น€
import Link from 'next/link'

export default function Navigation() {
  return (
    <nav>
      <Link href="/">ํ™ˆ</Link>
      <Link href="/about">์†Œ๊ฐœ</Link>
    </nav>
  )
}

๐Ÿงญ useRouter - ์ฝ”๋“œ๋กœ ํŽ˜์ด์ง€ ์ด๋™ํ•˜๊ธฐ

  • ๋ฒ„ํŠผ ํด๋ฆญ์ด๋‚˜ ํผ ์ œ์ถœ ํ›„์— ํŽ˜์ด์ง€๋ฅผ ์ด๋™์‹œํ‚ค๊ณ  ์‹ถ์„ ๋•Œ ์ •๋ง ์œ ์šฉํ•œ ํ›…
  • ์ฝ”๋“œ์—์„œ ์›ํ•˜๋Š” ํƒ€์ด๋ฐ์— "์ด ํŽ˜์ด์ง€๋กœ ๊ฐ€!" ํ•˜๊ณ  ๋ช…๋ นํ•  ์ˆ˜ ์žˆ์–ด์„œ ํŽธ๋ฆฌํ•จ

Next.js 13๋ถ€ํ„ฐ๋Š” ์ž„ํฌํŠธ ๊ฒฝ๋กœ๊ฐ€ ๋ฐ”๋€Œ์—ˆ๋‹ค (next/navigation)

import { useRouter } from 'next/navigation'

export default function MyButton() {
  const router = useRouter()
  
  return (
    <button onClick={() => router.push('/dashboard')}>
      ๋Œ€์‹œ๋ณด๋“œ๋กœ ์Š~โœจ
    </button>
  )
}

์ž์ฃผ ์“ฐ๋Š” ๊ธฐ๋Šฅ๋“ค

  1. ํŽ˜์ด์ง€ ์ด๋™: router.push('/path') - ์ƒˆ ํŽ˜์ด์ง€๋กœ ์ด๋™, router.replace('/path') - ํžˆ์Šคํ† ๋ฆฌ ๋‚จ๊ธฐ์ง€ ์•Š๊ณ  ์ด๋™
  2. ๋’ค๋กœ๊ฐ€๊ธฐ/์•ž์œผ๋กœ๊ฐ€๊ธฐ: router.back(), router.forward() - ๋ธŒ๋ผ์šฐ์ € ํ™”์‚ดํ‘œ ๋ฒ„ํŠผ๊ณผ ๊ฐ™์€ ํšจ๊ณผ
  3. ์ƒˆ๋กœ๊ณ ์นจ: router.refresh() - ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜์ง€๋งŒ ์ƒํƒœ๋Š” ์œ ์ง€
  4. ๋ฏธ๋ฆฌ ๋กœ๋”ฉ: prefetch ๋ฉ”์„œ๋“œ๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ๊ณง ๋ฐฉ๋ฌธํ•  ๊ฒƒ ๊ฐ™์€ ํŽ˜์ด์ง€ ๋ฏธ๋ฆฌ ์ค€๋น„ํ•ด๋‘๊ธฐ

๐Ÿ”Œ ๋ผ์šฐํŠธ ํ•ธ๋“ค๋Ÿฌ vs ์„œ๋ฒ„ ์•ก์…˜ - ๋ฐฑ์—”๋“œ ๊ธฐ๋Šฅ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•!

๐Ÿ“ก ๋ผ์šฐํŠธ ํ•ธ๋“ค๋Ÿฌ (Route Handler) - ๋‚˜๋งŒ์˜ API ๋งŒ๋“ค๊ธฐ

  • app ํด๋” ์•ˆ์— ํด๋” ๋งŒ๋“ค๊ณ  route.ts ํŒŒ์ผ ํ•˜๋‚˜ ์ถ”๊ฐ€
  • ์˜ˆ์ „ pages ํด๋”์˜ API ๋ผ์šฐํŠธ๋ž‘ ๋น„์Šท

์–ธ์ œ ์“ฐ๋ฉด ์ข‹์„๊นŒ?

  1. ํด๋ผ์ด์–ธํŠธ์—์„œ API ํ‚ค ๊ฐ™์€ ๋ฏผ๊ฐํ•œ ์ •๋ณด ์ˆจ๊ธฐ๊ณ  ์‹ถ์„ ๋•Œ (๋ณด์•ˆ!)
  2. ๋‹ค๋ฅธ ์„œ๋น„์Šค๋‚˜ ์•ฑ์—์„œ๋„ ์“ธ ์ˆ˜ ์žˆ๋Š” API๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ์„ ๋•Œ
// app/api/hello/route.ts
export async function GET() {
  // ์—ฌ๊ธฐ์„œ ๋ฌด์Šจ ์ž‘์—…์ด๋“  ํ•  ์ˆ˜ ์žˆ์Œ!
  return Response.json({ message: '์•ˆ๋…•ํ•˜์„ธ์š”!' })
}

โš™๏ธ ์„œ๋ฒ„ ์•ก์…˜ (Server Action) - ํผ ์ฒ˜๋ฆฌ๊ฐ€ ๋„ˆ๋ฌด ์‰ฌ์›Œ์ง!

  • 'use server'` ํ•œ ์ค„๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค
  • ํผ ์ œ์ถœ ์ฒ˜๋ฆฌ๊ฐ€ ์ •๋ง ๊ฐ„๋‹จํ•ด์ง€๊ณ , ๋ฐฑ์—”๋“œ ์ฝ”๋“œ๋ž‘ ํ”„๋ก ํŠธ์—”๋“œ ์ฝ”๋“œ๋ฅผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์„ž์„ ์ˆ˜ ์žˆ๋‹ค

์–ธ์ œ ์“ฐ๋ฉด ์ข‹์„๊นŒ?

  1. ํผ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์„œ DB์— ์ €์žฅํ•˜๊ณ  ์‹ถ์„ ๋•Œ
  2. ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ์—†์ด ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐฑ์‹ ํ•˜๊ณ  ์‹ถ์„ ๋•Œ
// ์„œ๋ฒ„ ์•ก์…˜ ์ •์˜ - ์ด ํ•จ์ˆ˜๋Š” ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋จ!
'use server'

export async function addItem(formData) {
  const item = formData.get('item')
  // DB์— ์ €์žฅํ•˜๋Š” ์ฝ”๋“œ (๋น„๋ฐ€ ํ‚ค๊ฐ€ ์žˆ์–ด๋„ ์•ˆ์ „ํ•จ!)
  await db.items.create({ data: { name: item } })
  // ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ๊ฐฑ์‹  - ์ƒˆ๋กœ๊ณ ์นจ ์—†์ด!
  revalidatePath('/items')
}

// ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ์“ฐ๋Š” ๊ฑด ์ •๋ง ์‰ฌ์›€
export default function AddItemForm() {
  return (
    <form action={addItem}>
      <input name="item" placeholder="ํ•  ์ผ ์ถ”๊ฐ€ํ•˜๊ธฐ" type="text" />
      <button type="submit">์ถ”๊ฐ€</button>
    </form>
  )
}

๐Ÿ–ผ๏ธ Image ์ปดํฌ๋„ŒํŠธ - ์ด๋ฏธ์ง€ ์ตœ์ ํ™”์˜ ๋งˆ๋ฒ•!

Next.js์˜ Image ์ปดํฌ๋„ŒํŠธ๋Š” ์ผ๋ฐ˜ img ํƒœ๊ทธ๋ณด๋‹ค ํ›จ์”ฌ ๋งŽ์€ ๊ธฐ๋Šฅ์„ ์ž๋™์œผ๋กœ ์ œ๊ณตํ•ด์คŒ:

  • ์ž๋™์œผ๋กœ ์ด๋ฏธ์ง€ ํฌ๊ธฐ๋ฅผ ์ตœ์ ํ™”ํ•˜๊ณ  ์ตœ์‹  ํฌ๋งท(WebP, AVIF)์œผ๋กœ ๋ณ€ํ™˜ํ•ด์ค˜์„œ ๋กœ๋”ฉ ์†๋„๊ฐ€ ๋นจ๋ผ์ง
  • ํ™”๋ฉด์— ๋ณด์ผ ๋•Œ๋งŒ ๋กœ๋“œํ•˜๋Š” ์ง€์—ฐ ๋กœ๋”ฉ ๊ธฐ๋Šฅ์ด ๊ธฐ๋ณธ์œผ๋กœ ์ ์šฉ๋จ (์Šคํฌ๋กค ์„ฑ๋Šฅ ํ–ฅ์ƒ!)
  • ์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ค‘์— ๋ ˆ์ด์•„์›ƒ์ด ํ”๋“ค๋ฆฌ๋Š” ํ˜„์ƒ(CLS)์„ ๋ฐฉ์ง€ํ•ด์ค˜์„œ UX๊ฐ€ ํ›จ์”ฌ ์ข‹์•„์ง
import Image from 'next/image'
 
export default function Page() {
  return (
    <Image
      src="/profile.png"
      width={500}
      height={500}
      alt="์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ด๋ฏธ์ง€"
      // ์ด๋ ‡๊ฒŒ๋งŒ ์จ๋„ ์ž๋™์œผ๋กœ ์ตœ์ ํ™”๊ฐ€ ์ ์šฉ๋จ!
    />
  )
}

๐Ÿ”’ remotePatterns ์„ค์ • - ์™ธ๋ถ€ ์ด๋ฏธ์ง€ ์‚ฌ์šฉํ•˜๊ธฐ

์™ธ๋ถ€ ์‚ฌ์ดํŠธ์˜ ์ด๋ฏธ์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์•ฝ๊ฐ„์˜ ์„ค์ •์ด ํ•„์š”ํ•œ๋ฐ, ๋ณด์•ˆ์ƒ์˜ ์ด์œ ๋กœ ์–ด๋–ค ์‚ฌ์ดํŠธ์˜ ์ด๋ฏธ์ง€๋ฅผ ์‚ฌ์šฉํ• ์ง€ ๋ช…์‹œํ•ด์•ผ ํ•จ:

// next.config.js์— ์ถ”๊ฐ€ํ•˜๊ธฐ
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com', // ์—ฌ๊ธฐ์— ์ด๋ฏธ์ง€ ๊ฐ€์ ธ์˜ฌ ์‚ฌ์ดํŠธ ๋„๋ฉ”์ธ ๋„ฃ๊ธฐ
        port: '',
        pathname: '/account123/**', // ํŠน์ • ๊ฒฝ๋กœ๋งŒ ํ—ˆ์šฉํ•˜๊ฑฐ๋‚˜ '/**'๋กœ ๋ชจ๋“  ๊ฒฝ๋กœ ํ—ˆ์šฉ
      },
    ],
  },
}

์ฒ˜์Œ์—” ๊ท€์ฐฎ์•„ ๋ณด์ด์ง€๋งŒ, ์ด๋Ÿฐ ์ œํ•œ์ด ์žˆ์–ด์„œ ์•…์˜์ ์ธ ์‚ฌ์ดํŠธ์˜ ์ด๋ฏธ์ง€๊ฐ€ ์šฐ๋ฆฌ ์•ฑ์— ๋กœ๋“œ๋˜๋Š” ๊ฑธ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Œ!

๐ŸŽจ ์Šคํƒ€์ผ๋ง - Tailwind CSS๊ฐ€ ๋Œ€์„ธ!

Next.js๋Š” Tailwind CSS์™€ ์ฐฐ๋–ก๊ถํ•ฉ์ž„:

  • ํ”„๋กœ์ ํŠธ ์ƒ์„ฑํ•  ๋•Œ Tailwind ์˜ต์…˜ ์„ ํƒํ•˜๋ฉด ๋ชจ๋“  ์„ค์ •์ด ์ž๋™์œผ๋กœ ์™„๋ฃŒ๋จ (reset CSS๋„ ํฌํ•จ!)
  • ํด๋ž˜์Šค ์ด๋ฆ„์„ ์ž๋™์œผ๋กœ ์ตœ์ ํ™”ํ•ด์„œ ๋ฒˆ๋“ค ํฌ๊ธฐ๋ฅผ ์ค„์—ฌ์คŒ
  • JIT(Just-In-Time) ์ปดํŒŒ์ผ๋Ÿฌ ๋•๋ถ„์— ๊ฐœ๋ฐœํ•  ๋•Œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ๋ฐ”๋กœ ๋ฐ˜์˜๋ผ์„œ ๊ฐœ๋ฐœ ์†๋„๊ฐ€ ๋นจ๋ผ์ง

๐Ÿ“ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ (Metadata) - SEO ์ ์ˆ˜ ์˜ฌ๋ฆฌ๊ธฐ!

๊ตฌ๊ธ€ ๊ฒ€์ƒ‰๊ฒฐ๊ณผ์™€ ์†Œ์…œ๋ฏธ๋””์–ด ๊ณต์œ ์— ํ‘œ์‹œ๋  ์ •๋ณด๋ฅผ ์‰ฝ๊ฒŒ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Œ:

์ •์  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ

import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'test', // ํƒญ์— ํ‘œ์‹œ๋  ์ œ๋ชฉ
  description: 'description test', // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์— ํ‘œ์‹œ๋  ์„ค๋ช…
}
 
export default function Page() {
  return <div>๋‚ด์šฉ์€ ์—ฌ๊ธฐ์—!</div>
}

๋™์  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ

import type { Metadata, ResolvingMetadata } from 'next'
 
type Props = {
  params: { id: string }
  searchParams: { [key: string]: string | string[] | undefined }
}
 
export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  // ์˜ˆ๋ฅผ ๋“ค์–ด ์ƒํ’ˆ ํŽ˜์ด์ง€๋ผ๋ฉด ์ƒํ’ˆ ID๋กœ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™€์„œ
  const id = params.id
  const product = await fetch(`https://api.example.com/products/${id}`).then((res) => res.json())
 
  // ํŽ˜์ด์ง€๋งˆ๋‹ค ๋‹ค๋ฅธ ์ œ๋ชฉ๊ณผ ์ด๋ฏธ์ง€๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Œ!
  return {
    title: `${product.title}`,
    openGraph: {
      images: [`/products/${id}.jpg`],
      description: product.description,
    },
  }
}
 
export default function ProductPage({ params }: Props) {
  return <div>์ƒํ’ˆ ์ •๋ณด๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋จ</div>
}

๐Ÿ”‘ ์˜ค๋Š˜ ๋ฐฐ์šด ๊ฒƒ ์š”์•ฝ

  • Link ์ปดํฌ๋„ŒํŠธ๋กœ ํŽ˜์ด์ง€ ์ด๋™์ด ๋น ๋ฅด๊ณ  ๋ถ€๋“œ๋Ÿฌ์›Œ์ง (prefetching ๋•๋ถ„!)
  • useRouter๋กœ ์ฝ”๋“œ์—์„œ ์›ํ•˜๋Š” ์‹œ์ ์— ํŽ˜์ด์ง€ ์ด๋™ ์ œ์–ด ๊ฐ€๋Šฅ
  • ๋ผ์šฐํŠธ ํ•ธ๋“ค๋Ÿฌ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ API ๋งŒ๋“ค ์ˆ˜ ์žˆ์Œ (api ํด๋”์— route.ts๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋!)
  • ์„œ๋ฒ„ ์•ก์…˜์œผ๋กœ ํผ ์ฒ˜๋ฆฌ๊ฐ€ ์—„์ฒญ ์‰ฌ์›Œ์ง (ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ ์™”๋‹ค๊ฐ”๋‹ค ์•ˆ ํ•ด๋„ ๋จ)
  • Image ์ปดํฌ๋„ŒํŠธ๋กœ ์ด๋ฏธ์ง€๊ฐ€ ์ž๋™ ์ตœ์ ํ™”๋˜์–ด ์‚ฌ์ดํŠธ ์„ฑ๋Šฅ UP
  • Tailwind CSS๋Š” Next.js์™€ ํ•จ๊ป˜ ์“ฐ๋ฉด ์„ค์ • ๊ฑฑ์ • ์—†์Œ
  • ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ API๋กœ SEO์™€ ์†Œ์…œ ๊ณต์œ  ์ตœ์ ํ™” ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Œ

์ด ๊ธฐ๋Šฅ๋“ค ๋•๋ถ„์— Next.js๋กœ ๊ฐœ๋ฐœํ•˜๋ฉด ์„ฑ๋Šฅ ์ข‹๊ณ  ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๋›ฐ์–ด๋‚œ ์›น์‚ฌ์ดํŠธ๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Œ! ๐Ÿš€

profile
์ฃผ๋‹ˆ์–ด ํ”„๋ก ํŠธ์—”๋“œ ์„ฑ์žฅ๊ธฐ ๊ธฐ๋ก๊ธฐ๋ก

0๊ฐœ์˜ ๋Œ“๊ธ€