Skip to content
/ next Public

Description of Next.js latest version (version 15, WIP) ๐Ÿ“š

Notifications You must be signed in to change notification settings

8dong/next

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

19 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Next.js (v15.0.2)

Next.js v15๋ถ€ํ„ฐ๋Š” react ๋ฐ react-dom ์ตœ์†Œ ๋ฒ„์ „์ด v19์ž…๋‹ˆ๋‹ค.

{
  "name": "nextjs",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "15.0.2",
    "react": "19.0.0-rc-0bc30748-20241028",
    "react-dom": "19.0.0-rc-0bc30748-20241028"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "typescript": "^5"
  },
  "packageManager": "yarn@4.2.2"
}

Routing

App Directory

Next v13๋ถ€ํ„ฐ app ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ์— ๋”ฐ๋ผ ๋ผ์šฐํŒ…๋˜๋ฉฐ ๊ฐ ํŒŒ์ผ์˜ ์—ญํ• (๊ธฐ๋Šฅ)๊ณผ ์ปจ๋ฒค์…˜์ด ์ง€์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋“ค์€ RSC(React Server Component)๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ RCC(React Client Component)๋กœ ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํŒŒ์ผ ์ตœ์ƒ๋‹จ์— "use client";๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

// app/page.tsx
'use client' // RCC ์ปดํฌ๋„ŒํŠธ ์„ ์–ธ

export default function Page() {
  return <main>Content,,,</main>
}

Dynamic Routes

๋””๋ ‰ํ† ๋ฆฌ๋ช…์„ "[folderName]"์ฒ˜๋Ÿผ ๋Œ€๊ด„ํ˜ธ๋กœ ๊นœ์‹ผ ๊ฒฝ์šฐ ๊ธฐ์กด ๋‹ค์ด๋‚˜๋ฏน ๋ผ์šฐํŒ…์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์ด๋‚˜๋ฏน ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๋Š” page.tsx, layout.tsx, route.ts, generateMetadata ํ•จ์ˆ˜์— params prop์œผ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.

// app/blog/[slug]/page.tsx
type Params = Promise<{ slug: string }>

interface PageProps {
  params?: Params // ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ ๊ฐ’ params[folderName]์œผ๋กœ ์ ‘๊ทผ
}

export default function Page({ params }: PageProps) {
  /**
   * URL: "/blog/b" , params: { slug: 'b' }
   * URL: "/blog/a" , params: { slug: 'a' }
   **/

  return <main>Content,,,</main>
}

๋‹ค์ด๋‚˜๋ฏน ๋ผ์šฐํŒ…์ด๋ž€ URL ๊ฒฝ๋กœ๊ฐ’์œผ๋กœ ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ ๋‚ด ๋ Œ๋”๋ง๋  ์ •๋ณด๋ฅผ ๋™์ ์œผ๋กœ ๊ฒฐ์ •ํ•˜์—ฌ ํŽ˜์ด์ง€๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ์ด๋Š” ํ•˜๋‚˜์˜ ํŽ˜์ด์ง€๋ฅผ ์žฌ์‚ฌ์šฉํ•˜์—ฌ ์—ฌ๋Ÿฌ ์ฝ˜ํ…์ธ ๋ฅผ ๋™์ž‘ํ•˜๊ธฐ ์œ„ํ•ด์„œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

Catch-All Routes

๋””๋ ‰ํ† ๋ฆฌ ๋ช…์„ "[...forderName]"์œผ๋กœ ์ƒ์„ฑํ•˜๊ฒŒ ๋˜๋ฉด ๋งค์นญ๋˜๋Š” ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ Catch-All Segments๊ฐ€ ํ™œ์„ฑํ™” ๋ฉ๋‹ˆ๋‹ค.

  • Route: "/app/shop/[...slug]/page.tsx" -> URL: "/shop/a" -> params: { slug: ['a'] }

  • Route: "/app/shop/[...slug]/page.tsx" -> URL: "/shop/a/b" -> params: { slug: ['a', 'b'] }

  • Route: "/app/shop/[...slug]/page.tsx" -> URL: "/shop/a/b/c" -> params: { slug: ['a', 'b', 'c'] }

์œ„ ์˜ˆ์ œ์ฒ˜๋Ÿผ Catch-All Routes๋Š” params๋กœ ์—ฌ๋Ÿฌ ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ ๊ฐ’๋“ค์„ ์š”์†Œ๋กœ ๊ฐ–๋Š” ๋ฐฐ์—ด๋กœ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค.

Optional Catch-All Routes

Catch-All Segments๋Š” params๊ฐ€ 1๊ฐœ ์ด์ƒ์ธ ๊ฒฝ์šฐ์—๋งŒ ํ™œ์„ฑํ™” ๋˜์ง€๋งŒ "[[...forderName]]"์œผ๋กœ ๋””๋ ‰ํ† ๋ฆฌ๋ช…์„ ์ƒ์„ฑํ•˜๋ฉด ํ•ด๋‹น Catch-All Segments๋Š” params๊ฐ€ 0๊ฐœ์ธ ๊ฒฝ์šฐ์—๋„ ํ™œ์„ฑํ™” ๋ฉ๋‹ˆ๋‹ค.

  • Route: "/app/shop/[[...slug]]/page.tsx" -> URL: "/shop" -> params: { }

์œ„ ์˜ˆ์ œ์ฒ˜๋Ÿผ params๊ฐ€ 0๊ฐœ์ธ ๊ฒฝ์šฐ params๋Š” ๋นˆ ๊ฐ์ฒด๊ฐ€ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.

Route Groups

์ผ๋ฐ˜์ ์œผ๋กœ app๋””๋ ‰ํ† ๋ฆฌ ๋‚ด ๋””๋ ‰ํ† ๋ฆฌ๋ช…๋“ค์€ ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๋กœ URL ๊ฒฝ๋กœ์™€ ๋งคํ•‘๋ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋””๋ ‰ํ† ๋ฆฌ๋ช…์„ "(folderName)"์ฒ˜๋Ÿผ ์†Œ๊ด„ํ˜ธ๋กœ ๊ฐ์‹ผ ๊ฒฝ์šฐ ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๋กœ ์„ค์ •๋˜์ง€ ์•Š์œผ๋ฉฐ ์‹ค์ œ ๋ผ์šฐํŒ…์—์„œ ํ•ด๋‹น ๊ฒฝ๋กœ๋Š” ๋ฌด์‹œ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, "app/(marketing)/about" ๊ฒฝ๋กœ์— ์ƒ์„ฑ๋œ ํŽ˜์ด์ง€๋Š” "/(marketing)/about"์—์„œ "(marketing)"์ด ๋ฌด์‹œ๋œ "/about"์œผ๋กœ ๋ผ์šฐํŒ…๋ฉ๋‹ˆ๋‹ค.

์ฆ‰, ์†Œ๊ด„ํ˜ธ๋กœ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋งŒ๋“ค์–ด ๊ฒฝ๋กœ๋“ค์˜ ๊ทธ๋ฃน์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, layout.tsx๋‚˜ template.tsx๋ฅผ ํ†ตํ•ด URL ๊ฒฝ๋กœ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๊ณ  ๊ฒฝ๋กœ ๊ทธ๋ฃน๋ณ„ layout์„ ์„ค์ •ํ•  ๋•Œ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Parallel Routes

๋””๋ ‰ํ† ๋ง๋ช…์„ "@folderName"๋กœ ์ž‘์„ฑํ•œ ๊ฒฝ์šฐ ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๊ฐ€ ์•„๋‹Œ ์Šฌ๋กฏ์œผ๋กœ ์„ค์ •๋˜๋ฉฐ ์‹ค์ œ๋กœ ํ•ด๋‹น ๊ฒฝ๋กœ๋Š” ๋ฌด์‹œ๋ฉ๋‹ˆ๋‹ค.

ํŒจ๋Ÿฌ๋  ๋ผ์šฐํŒ…์€ ํ•˜๋‚˜์˜ ๋ ˆ์ด์•„์›ƒ์— ์—ฌ๋Ÿฌ page๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด์„œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

app
โ”œโ”€โ”€ page.tsx
โ”‚
โ”œโ”€โ”€ layout.tsx
โ”‚
โ””โ”€โ”€ @team
    โ””โ”€โ”€ settings
        โ””โ”€โ”€ page.tsx

์œ„ ๊ตฌ์กฐ์ฒ˜๋Ÿผ "/app/@team/settings/page.tsx"๊ฐ€ ์กด์žฌํ•  ๋•Œ "/"์—์„œ "/settigns"๋กœ ์ด๋™ํ•˜๊ฒŒ ๋˜๋ฉด "/app/page.tsx"๊ฐ€ ๋ Œ๋”๋ง๋œ ์ƒํƒœ์—์„œ "/app/@team/settgins/page.tsx"๋„ ๋ณ‘๋ ฌ์ ์œผ๋กœ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ URL์€ "/settings"์ด์ง€๋งŒ "/app/page.tsx"๊ฐ€ ๋ Œ๋”๋ง๋œ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ "/app/@team/settings/page.tsx"๋„ ๋ณ‘๋ ฌ๋กœ ๋ Œ๋”๋ง ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

"@forderName/page.tsx"์—์„œ exportํ•œ ์ปดํฌ๋„ŒํŠธ์˜ ๊ฒฝ์šฐ @forderName์™€ ๋™์ผํ•œ ๋ ˆ๋ฒจ์— ์žˆ๋Š” layout ์ปดํฌ๋„ŒํŠธ prop์œผ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋™์ผํ•œ ๋ ˆ๋ฒจ์— ์—†๋‹ค๋ฉด ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์ƒ์œ„ layout.tsx์˜ Layout ์ปดํฌ๋„ŒํŠธ์—๊ฒŒ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ์ „๋‹ฌ๋˜๋Š” prop ๋„ค์ด๋ฐ์€ ๋””๋ ‰ํ† ๋ฆฌ๋ช…(forderName)์œผ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.


์˜ˆ๋ฅผ ๋“ค์–ด, "/app/@modal/page.tsx"๊ฐ€ export defaultํ•œ ์ปดํฌ๋„ŒํŠธ๋Š” "/app/layout.tsx"๊ฐ€ export defaultํ•œ Layout ์ปดํฌ๋„ŒํŠธ์— modal prop์œผ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.

// app/layout.tsx
import { ReactNode } from 'react'

interface LayoutProps {
  children: ReactNode // "app/page.tsx"์—์„œ export defaultํ•œ ์ปดํฌ๋„ŒํŠธ
  modal?: ReactNode // "app/@modal/page.tsx"์—์„œ export defaultํ•œ ์ปดํฌ๋„ŒํŠธ
}

export default function Layout({ children, modal }: LayoutProps) {
  <html lang="ko">
    <body>
      {children}
      {modal}
    </body>
  </html>
}

ํŽ˜๋Ÿฌ๋  ๋ผ์šฐํŒ…์˜ ์ฃผ์˜ํ•  ์ ์œผ๋กœ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • Soft Navigation ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ Next๋Š” ํ˜„์žฌ ์ผ์น˜ํ•˜๋Š” ์Šฌ๋กฏ์ด ์—†๋”๋ผ๋ฉด ์ด์ „ ํ™œ์„ฑํ™”๋œ ์Šฌ๋กฏ ํ™œ์„ฑ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์š”์ฒญํ•œ ๊ฒฝ๋กœ์— ๋Œ€ํ•œ ํŽ˜์ด์ง€๊ฐ€ ์—†๋”๋ผ๋„ 404 ์—๋Ÿฌ๋ฅผ ํ‘œ์‹œํ•˜์ง€ ์•Š๊ณ  ์ด์ „์— ํ™œ์„ฑํ™”๋œ ํŽ˜์ด์ง€๋ฅผ ๊ณ„์† ํ‘œ์‹œํ•ด์ค๋‹ˆ๋‹ค.
app
โ”œโ”€โ”€ page.tsx
โ”‚
โ”œโ”€โ”€ layout.tsx
โ”‚
โ”œโ”€โ”€ @team
โ”‚   โ””โ”€โ”€ default.tsx
โ”‚   โ””โ”€โ”€ settings
โ”‚       โ””โ”€โ”€ page.tsx
โ”‚
โ””โ”€โ”€ @analytics
    โ””โ”€โ”€ page.tsx

app ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๊ฐ€ ์œ„์™€ ๊ฐ™๊ณ  ์ดˆ๊ธฐ ๊ฒฝ๋กœ๊ฐ€ "/"์ผ ๋•Œ "/app/@analytics/page.tsx"๊ฐ€ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. "/app/@team/page.tsx"๋Š” ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ "/app/@team/default.tsx"๊ฐ€ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.

์ดํ›„ "/"์—์„œ "/settings"๋กœ Soft Navigation ์‚ฌ์šฉํ–ˆ๋‹ค๋ฉด "/app/@team/settings/page.tsx"์ด ํ™œ์„ฑํ™” ๋ฉ๋‹ˆ๋‹ค. ์ด๋•Œ "/app/@analytics/settings/page.tsx"๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ์กด์— ํ™œ์„ฑํ™”๋œ "/app/@analytics/page.tsx" ํ™œ์„ฑ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. "/app/settings/page.tsx" ๋˜ํ•œ ์กด์žฌํ•˜์ง€ ์•Š์ง€๋งŒ 404 ์—๋Ÿฌ๋ฅผ ํ‘œ์‹œํ•˜์ง€ ์•Š๊ณ  ๊ธฐ์กด์— ํ™œ์„ฑํ™”๋œ ํŽ˜์ด์ง€("/app/page.tsx") ๋ Œ๋”๋ง์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ์‹ค์ œ ๊ฒฝ๋กœ๋Š” "/settings"์ธ๋ฐ ๋ Œ๋”๋ง๋œ ํŽ˜์ด์ง€๋Š” "/app/page.tsx"์ด๋ฉฐ "/app/@analytics/page.tsx"์™€ "/app/@team/settings/page.tsx"๊ฐ€ ํ™œ์„ฑ๋œ ์ƒํƒœ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

์Šฌ๋กฏ๊ณผ ์„ธ๊ทธ๋จผํŠธ๋Š” ์„œ๋กœ ๋…๋ฆฝ์ ์ž…๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋งํ•ด, ์‹ค์ œ ๊ฒฝ๋กœ์™€ ๋งค์นญ๋˜๋Š” ์„ธ๊ทธ๋จผํŠธ์™€ ์Šฌ๋กฏ ์ด๋ฆ„์ด ๋™์ผํ•˜๋”๋ผ๋„ ์„œ๋กœ ์ถฉ๋Œํ•˜์ง€ ์•Š๊ณ  ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, "/app/settings/page.tsx"์™€ "/app/@team/settings/page.tsx" ๋‘˜ ๋‹ค ์กด์žฌํ•˜๋”๋ผ๋„ ์„œ๋กœ ์ถฉ๋Œํ•˜์ง€ ์•Š์œผ๋ฉฐ ์‹ค์ œ ๊ฒฝ๋กœ๊ฐ€ "/settings"์ผ ๋•Œ ๋‘˜ ๋‹ค ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

  • Hard Navigation ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ Next๋Š” ๋จผ์ € ๋งค์นญ๋˜๋Š” ์Šฌ๋กฏ์„ ํ™•์ธํ•˜๊ณ  ๋งŒ์•ฝ ์—†๋Š” ๊ฒฝ์šฐ์—๋Š” ์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์˜ฌ๋ผ๊ฐ€๋ฉด์„œ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด default.tsx๊ฐ€ export defaultํ•œ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ default.tsx ํŒŒ์ผ์กฐ์ฐจ ์—†๋‹ค๋ฉด 404์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
app
โ”œโ”€โ”€ page.tsx
โ”‚
โ”œโ”€โ”€ layout.tsx
โ”‚
โ”œโ”€โ”€ @team
โ”‚   โ””โ”€โ”€ default.tsx
โ”‚   โ””โ”€โ”€ settings
โ”‚       โ””โ”€โ”€ page.tsx
โ”‚
โ””โ”€โ”€ @analytics
    โ””โ”€โ”€ page.tsx

app ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๊ฐ€ ์œ„์™€ ๊ฐ™์„ ๋•Œ "/settings"๋กœ Hard Navigationํ•˜๋Š” ๊ฒฝ์šฐ "/app/settings/page.tsx"๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์•„ 404์—๋Ÿฌ๋ฅผ ํ‘œ์‹œํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ "/app/settings/page.tsx"๊ฐ€ ์žˆ๋‹ค ํ•˜๋”๋ผ๋„ "/app/@analytics/settings/page.tsx"์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— "/app/@analytics/default.tsx" ๋ Œ๋”๋ง์„ ์‹œ๋„ํ•˜๋Š”๋ฐ, ํ•ด๋‹น ํŒŒ์ผ์กฐ์ฐจ ์กด์žฌํ•˜์ง€ ์•Š์•„ 404์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ์‹œํ‚ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์ฆ‰, Hard Navigationํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ์ด์ „์— ํ™œ์„ฑํ™”๋œ ์Šฌ๋กฏ์ด๋‚˜ ๋ Œ๋”๋ง๋œ ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๊ฐ–๊ณ  ์žˆ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— Soft Navigation๊ณผ ๋‹ค๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

Intercepting Routes

๋””๋ ‰ํ† ๋ฆฌ๋ช…์„ "(...)fortuneName", "(..)fortuneName" ํ˜น์€ "(.)fortuneName"๋กœ ์ž‘์„ฑํ•œ ๊ฒฝ์šฐ ์ธํ„ฐ์…‰ํŒ… ๋ผ์šฐํŠธ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

  • (.): ๋™์ผํ•œ ๋ ˆ๋ฒจ ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ธํ„ฐ์…‰ํŒ…

  • (..): ํ•˜๋‚˜์˜ ์ƒ์œ„ ๋ ˆ๋ฒจ ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ธํ„ฐ์…‰ํŒ…

  • (..)(..): ๋‘ ๊ฐœ์˜ ์ƒ์œ„ ๋ ˆ๋ฒจ ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ธํ„ฐ์…‰ํŒ…

  • (...): app ๋””๋ ‰ํ† ๋ฆฌ ๋ ˆ๋ฒจ์˜ ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ธํ„ฐ์…‰ํŒ…


์ฃผ์˜ํ•  ์ ์œผ๋กœ Route Groups, Slot(Parallel Routes), Private Routes ๋“ฑ URL ๊ฒฝ๋กœ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š” ๊ฒƒ๋“ค์€ ๋ฌด์‹œ๋˜์–ด ์ธํ„ฐ์…‰ํŒ…๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, ํŒŒ์ผ ์‹œ์Šคํ…œ์ด ์•„๋‹Œ ๋ผํˆฌํŠธ ์„ธ๊ทธ๋จผํŠธ๋งŒ์„ ๊ณ ๋ คํ•˜์—ฌ ์ธํ„ฐ์…‰ํŒ…ํ•ฉ๋‹ˆ๋‹ค.

app
โ”œโ”€โ”€ page.tsx
โ”‚
โ”œโ”€โ”€ layout.tsx
โ”‚
โ”œโ”€โ”€ dashborad
โ”‚   โ””โ”€โ”€ (..i)
โ”‚       โ””โ”€โ”€ page.tsx
โ”‚
โ””โ”€โ”€ i
    โ””โ”€โ”€ page.tsx

app ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๊ฐ€ ์œ„์™€ ๊ฐ™์„ ๋•Œ "/i"๋กœ ์ด๋™ํ•˜๋Š” ๊ฒฝ์šฐ "app/i/page.tsx"๊ฐ€ ์•„๋‹Œ "app/dashboard/(..i)/page.tsx"๊ฐ€ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

์ธํ„ฐ์…‰ํŒ… ๋ผ์šฐํŠธ๋Š” Soft Navigation์˜ ๊ฒฝ์šฐ์—๋งŒ ์ธํ„ฐ์…‰ํŒ…๋˜๋ฉฐ Soft Navigation์ด ์•„๋‹Œ ๊ฒฝ์šฐ ๊ธฐ์กด ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, "/i"๋กœ Hard Navigation์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋™ํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” "app/i/page.tsx"๊ฐ€ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

Parallel Routes & Intercepting Routes

Parallel Routes๋‚ด Intercepting Routes๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ๋…๋ฆฝ๋œ URL ๊ฒฝ๋กœ๋ฅผ ๊ฐ–๋Š” ๋ชจ๋‹ฌ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋…๋ฆฝ๋œ URL ๊ฒฝ๋กœ๋ฅผ ๊ฐ–๋Š” ๋ชจ๋‹ฌ์˜ ๊ฒฝ์šฐ URL์„ ํ†ตํ•ด ๋ชจ๋‹ฌ ๋‚ด์šฉ์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, Hard Navigation์„ ์‚ฌ์šฉํ•˜๋”๋ผ๋„ ๊ทธ ์ƒํƒœ๊ฐ€ ์œ ์ง€๋˜๊ณ , ๋’ค๋กœ ๊ฐ€๊ธฐ ํ˜น์€ ์•ž์œผ๋กœ ๊ฐ€๊ธฐ๋ฅผ ํ†ตํ•ด ๋ชจ๋‹ฌ์„ ์—ด๊ฑฐ๋‚˜ ๋‹ซ์„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

app
โ”œโ”€โ”€ page.tsx
โ”‚
โ”œโ”€โ”€ layout.tsx
โ”‚
โ””โ”€โ”€ @modal
    โ””โ”€โ”€ login
        โ””โ”€โ”€ page.tsx

๋งŒ์•ฝ ์œ„์™€ ๊ฐ™์€ app ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง„ ๊ฒฝ์šฐ, "/login"์œผ๋กœ Soft Navigation ํ•œ๋‹ค๋ฉด "/app/@modal/login/page.tsx"๊ฐ€ ํ™œ์„ฑํ™” ๋˜์ง€๋งŒ, Hard Navigation์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” 404 ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. Hard Navigation ํ•œ๋‹ค๋ฉด ์‹ค์ œ ๊ฒฝ๋กœ์™€ ๋งค์นญ๋˜๋Š” ํŽ˜์ด์ง€๋ฅผ ๋ Œ๋”๋งํ•˜๊ฒŒ ๋˜๋Š”๋ฐ ์œ„ ๊ตฌ์กฐ์˜ ๊ฒฝ์šฐ "/app/login/page.tsx" ํŒŒ์ผ์ด ์กด์žฌํ•˜์ง€ ์•Š์•„ 404 ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

app
โ”œโ”€โ”€ page.tsx
โ”‚
โ”œโ”€โ”€ layout.tsx
โ”‚
โ”œโ”€โ”€ @modal
|   โ””โ”€โ”€ login
|       โ””โ”€โ”€ page.tsx
|
โ””โ”€โ”€ login
    โ””โ”€โ”€ page.tsx

๊ทธ๋ ‡๋‹ค๋ฉด ์œ„์™€ ๊ฐ™์€ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์งˆ ๋•Œ Hard Navigation์„ ์‚ฌ์šฉํ•˜๋ฉด 404์—๋Ÿฌ๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š์ง€๋งŒ, Soft Navigation๊ณผ Hard Navigation ๋‘˜ ๋‹ค "/app/login/page.tsx"๊ฐ€ ๋ Œ๋”๋ง๋˜๊ณ  "/app/@modal/login/page.tsx"๊ฐ€ ๋ณ‘๋ ฌ ๋ผ์šฐํŒ…๋ฉ๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ ๋ชจ๋‹ฌ์˜ ๊ฒฝ์šฐ ๊ธฐ์กด ํŽ˜์ด์ง€ ๋ Œ๋”๋ง์„ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ชจ๋‹ฌ์„ ํ‘œ์‹œํ•ด์ฃผ์–ด์•ผ ํ•˜์ง€๋งŒ ์œ„์˜ ๊ฒฝ์šฐ ๊ธฐ์กด ํŽ˜์ด์ง€ ๋ Œ๋”๋ง์„ ์œ ์ง€ํ•˜์ง€ ๋ชปํ•˜๊ณ  "/app/login/page.tsx"๊ฐ€ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.


๊ตฌํ˜„ํ•˜๋ ค๋˜ ๋™์ž‘์€ Soft Navigation์œผ๋กœ "/login"์œผ๋กœ ์ด๋™ํ•˜๊ฒŒ ๋˜๋ฉด ๊ธฐ์กด ๋ Œ๋”๋ง๋œ ํŽ˜์ด์ง€์— ๋ณ‘๋ ฌ๋กœ "/app/@modal/login/page.tsx"๊ฐ€ ๋ Œ๋”๋ง๋˜๊ณ , Hard Navigationํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” 404 ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฉด์„œ "/app/login/page.tsx"๋งŒ ๋ Œ๋”๋ง๋˜๋„๋ก ํ•˜๋ ค๋ฉด Parallel Routes์™€ Intercepting Routes๋ฅผ ๊ฐ™์ด ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// app/@modal/(.)login/page.tsx

import Modal from '@/shard/components/Modal'

export default function Page() {
  return (
    <Modal>
      <form>Login Form,,,</form>
    </Modal>
  )
}
// app/login/page.tsx

export default function Page() {
  return <form>Login Form,,,</form>
}

"app/login/page.tsx"์™€ "app/@modal/(.)login/page.tsx"๋ฅผ ์œ„์™€ ๊ฐ™์ด ์ƒ์„ฑํ•ด์ค๋‹ˆ๋‹ค.

app
โ”œโ”€โ”€ page.tsx
โ”‚
โ”œโ”€โ”€ layout.tsx
โ”‚
โ”œโ”€โ”€ @modal
|   โ””โ”€โ”€ (.)login
|       โ””โ”€โ”€ page.tsx
|
โ””โ”€โ”€ login
    โ””โ”€โ”€ page.tsx

๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๊ฐ€ ์œ„์™€ ๊ฐ™์„ ๋•Œ Soft Navigation์„ ์‚ฌ์šฉํ•˜์—ฌ "/login"์œผ๋กœ ์ด๋™ํ•˜๋Š” ๊ฒฝ์šฐ "/app/@modal/(.)login/page.tsx"๊ฐ€ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค. ์ถ”๊ฐ€์ ์œผ๋กœ Hard Navigationํ•˜์—ฌ ์ด๋™ํ•œ ๊ฒฝ์šฐ์—๋Š” "/app/login/page.tsx"๋งŒ์ด ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

layout.tsx

layout.tsx ํŒŒ์ผ์—์„œ export default๋œ Layout ์ปดํฌ๋„ŒํŠธ๋Š” props๋กœ page.tsx๊ฐ€ export defaultํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ children prop์œผ๋กœ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค. ๋‹ค์ด๋‚˜๋ฏน ๋ผ์šฐํŠธ์˜ ๊ฒฝ์šฐ ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ ๊ฐ’์„ ์ถ”๊ฐ€์ ์œผ๋กœ params prop๋„ ์ „๋‹ฌ๋ฐ›์œผ๋ฉฐ ์ด๋Š” ๋™์  ๋ผ์šฐํŒ…์— ๋Œ€ํ•œ ๊ฒฝ๋กœ๊ฐ’์„ ๊ฐ์ฒด ํ˜•ํƒœ๋กœ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค. ๋™์ผํ•œ ๋ ˆ๋ฒจ์— ํŽ˜๋Ÿฌ๋  ๋ผ์šฐํŠธ๊ฐ€ ์กด์žฌํ•œ๋‹ค๋ฉด ์ถ”๊ฐ€์ ์œผ๋กœ ํ•ด๋‹น ์Šฌ๋กฏ๋ช…์œผ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ prop์œผ๋กœ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค.

app ๋””๋ ‰ํ† ๋ฆฌ์˜ ๋ฃจํŠธ ๋ ˆ๋ฒจ์—๋Š” ํ•„์ˆ˜์ ์œผ๋กœ ํ•˜๋‚˜์˜ layout.tsx("app/layout.tsx") ํŒŒ์ผ์ด ํ•„์š”ํ•˜๋ฉฐ, ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋Š” html๊ณผ body ํƒœ๊ทธ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

app ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด ๊ฐ ํ•˜์œ„ ๋””๋ ‰ํ† ๋ฆฌ ๋งˆ๋‹ค ํ•˜๋‚˜์”ฉ ์„ค์ • ๊ฐ€๋Šฅํ•˜๋ฉฐ ๋งŒ์•ฝ ํ•˜์œ„ ๋””๋ ‰ํ† ๋ฆฌ์— layout.tsx๊ฐ€ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ ์ƒ์œ„ layout.tsx๊ฐ€ ํ•˜์œ„ layout.tsx๋ฅผ ํฌํ•จํ•˜๋Š” ํ˜•์‹์œผ๋กœ ์ ์šฉ(nested layout)๋ฉ๋‹ˆ๋‹ค.

์ฃผ์˜ํ•  ์ ์œผ๋กœ๋Š” ํ•˜์œ„ ๊ฒฝ๋กœ๋กœ Soft Navigation์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ Layout ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฆฌ๋ Œ๋”๋ง ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋ฆฌ๋ Œ๋”๋ง์ด ํ•„์š”ํ•œ Layout ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ layout.tsx ๋Œ€์‹  template.tsx ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

// app/layout.tsx
import { ReactNode } from 'react'

type Params = Promise<{ slug: string }>

interface LayoutProps {
  children: ReactNode // app/page.tsx์—์„œ export default๋œ ์ปดํฌ๋„ŒํŠธ
  params?: Params // ๋‹ค์ด๋‚˜๋ฏน ๋ผ์šฐํŒ…๋˜๋Š” ๊ฒฝ์šฐ ๋™์  ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ ๊ฐ’
}

export default function Layout({ children }: LayoutProps) {
  return (
    <html>
      <body>{children}</body>
    </html>
  )
}

template.tsx

template.tsx๋Š” layout.tsx์™€ ์œ ์‚ฌํ•˜๊ฒŒ ํŽ˜์ด์ง€ ๋ ˆ์ด์•„์›ƒ ์—ญํ• ์„ ํ•˜์ง€๋งŒ Layout ์ปดํฌ๋„ŒํŠธ์™€๋Š” ๋‹ค๋ฅด๊ฒŒ Soft Navigationํ•˜๋Š” ๊ฒฝ์šฐ ๋งค๋ฒˆ ์ƒˆ๋กญ๊ฒŒ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

template.tsx์™€ layout.tsx๋ฅผ ๊ฐ™์ด ์‚ฌ์šฉํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด Layout ์ปดํฌ๋„ŒํŠธ ํ•˜์œ„์— Template ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋˜๊ณ , Template ์ปดํฌ๋„ŒํŠธ ํ•˜์œ„์— Page ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ Layout๊ณผ ์œ ์‚ฌํ•œ ์—ญํ• ์„ ํ•˜๋ฏ€๋กœ ํ•˜๋‚˜๋งŒ ์‚ฌ์šฉํ•˜๋ฉฐ, ๋˜๋„๋ก ํŠน๋ณ„ํ•œ ์ด์œ ๊ฐ€ ์—†๋‹ค๋ฉด Layout ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

<Layout>
  {/* Note that the template is given a unique key. */}
  <Template key={routeParam}>{children}</Template>
</Layout>

Template ์ปดํฌ๋„ŒํŠธ๋Š” ์•„๋ž˜์™€ ์ฒ˜๋Ÿผ ๋งค๋ฒˆ ๋ Œ๋”๋ง์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ์— ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • "use client"๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ์„ค์ •ํ•˜๊ณ  useEffect๋‚˜ useStateํ›…์— ์˜์กดํ•˜๋Š” ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • Next ๊ธฐ์กด ํ”„๋ ˆ์ž„์›Œํฌ ๋™์ž‘์„ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, Layout ์ปดํฌ๋„ŒํŠธ ํ•˜์œ„์— Suspense ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋”๋ผ๋„ fallback ์ปดํฌ๋„ŒํŠธ๋Š” ์ฒ˜์Œ์—๋งŒ ํ‘œ์‹œ๋˜๊ณ  ์ดํ›„ ๋ผ์šฐํŒ…๋˜๋”๋ผ๋„ ํ‘œ์‹œ๋˜์ง€ ์•Š์ง€๋งŒ Template ์ปดํฐ๋„ŒํŠธ๋Š” ๋ผ์šฐํŒ…๋  ๋•Œ๋งค๋‹ค ํ‘œ์‹œํ•ด์ค๋‹ˆ๋‹ค.

page.tsx

page.tsx์—์„œ export defaultํ•œ ์ปดํฌ๋„ŒํŠธ๋Š” layout.tsx(or template.tsx)์—์„œ export defaultํ•œ ์ปดํฌ๋„ŒํŠธ children prop์œผ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, ์‹ค์ œ ํŽ˜์ด์ง€ ์ปจํ…์ธ ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.

๋””๋ ‰ํ† ๋ฆฌ๋งˆ๋‹ค page.tsx ํŒŒ์ผ์„ ํ•„์ˆ˜์ด๋ฉฐ, ๋งŒ์•ฝ page.tsxํŒŒ์ผ์ด ์—†๋‹ค๋ฉด ํ•ด๋‹น ๊ฒฝ๋กœ๋กœ ๋ผ์šฐํŒ…๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Page ์ปดํฌ๋„ŒํŠธ๋Š” ๋™์  ๋ผ์šฐํŠธ์˜ ๊ฒฝ์šฐ ๋™์  ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ ๊ฐ’์„ params prop์œผ๋กœ ์ „๋‹ฌ๋ฐ›์œผ๋ฉฐ, ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์— ๋Œ€ํ•œ ์ •๋ณด๋Š” searchParams prop์œผ๋กœ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค.

// app/page.tsx
type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>

interface PageProps {
  params?: Params // ๋™์  ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ์— ๋Œ€ํ•œ ์ •๋ณด
  searchParams?: SearchParams // ์ฟผ๋ฆฌ์ŠคํŠธ๋ง ๊ฐ’์— ๋Œ€ํ•œ ์ •๋ณด
}

export default function Page({ params, searchParams }: PageProps) {
  return <section>Page Content,,,</section>
}

default.tsx

default.tsx ํŒŒ์ผ์ด export defaultํ•œ ์ปดํฌ๋„ŒํŠธ๋Š” Slot(Parallel Routes)์— ๋Œ€ํ•œ fallback ์ปดํฌ๋„ŒํŠธ ์ž…๋‹ˆ๋‹ค.

ํŽ˜๋Ÿฌ๋  ๋ผ์šฐํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ํ•˜์œ„ ๊ฒฝ๋กœ๊ฐ€ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” default.tsx ํŒŒ์ผ์„ ์ƒ์œ„ ๊ฒฝ๋กœ์— ์ถ”๊ฐ€ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์ผ์น˜ํ•˜๋Š” ์Šฌ๋กฏ์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๋•Œ Hard Navigationํ•˜๋Š” ๊ฒฝ์šฐ Layout ์ปดํฌ๋„ŒํŠธ๊ฐ€ 404 ์—๋Ÿฌ๋ฅผ ํ‘œ์‹œํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ํŽ˜๋Ÿฌ๋  ๋ผ์šฐํŠธ๊ฐ€ "/app/@team/settings/page.tsx"์—๋งŒ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ "/"์œผ๋กœ ๋ผ์šฐํŒ…๋˜๋Š” ๊ฒฝ์šฐ "/app/@team/page.tsx"๊ฐ€ expoprt defaultํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฐพ์ง€ ๋ชปํ•ด 404์—๋Ÿฌ๊ฐ€ ๋‚˜์˜ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ "/app/@team/default.tsx"๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ด์™€ ๊ฐ™์€ ์ƒํ™ฉ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

loading.tsx

app ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด loading.tsx ํŒŒ์ผ์ด export defaultํ•œ ์ปดํฌ๋„ŒํŠธ๋Š” Next๊ฐ€ ์ž๋™์œผ๋กœ Page ์ปดํฌ๋„ŒํŠธ๋ฅผ Suspense๋กœ ๋ž˜ํ•‘ํ•˜๊ณ  fallback prop์— loading.tsx๊ฐ€ export defaultํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ „๋‹ฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

Loading ์ปดํฌ๋„ŒํŠธ๋Š” Page ์ปดํฌ๋„ŒํŠธ ๋‚ด์šฉ์„ ๋กœ๋“œํ•˜๋Š” ๋™์•ˆ Loading ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•˜๊ณ , ์ดํ›„ ๋ Œ๋”๋ง์ด ์™„๋ฃŒ๋˜๋ฉด Page ์ปดํฌ๋„ŒํŠธ๋กœ ๊ต์ฒด์‹œ์ผœ์ค๋‹ˆ๋‹ค.

<Layout>
  <Suspense fallback={<Loading />}>
    <Page />
  </Suspense>
</Layout>

์œ„์™€ ๊ฐ™์ด Page ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ž๋™์œผ๋กœ Suspense ์ปดํฌ๋„ŒํŠธ๋กœ ๋ž˜ํ•‘๋˜๋ฉฐ fallback prop์œผ๋กœ Loading ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ „๋‹ฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, Loading ์ปดํฌ๋„ŒํŠธ๋Š” ํŽ˜์ด์ง€ ์ „์ฒด์— ๋Œ€ํ•œ fallback ์ปดํฌ๋„ŒํŠธ๋กœ์„œ ๋™์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์ฃผ์˜ํ•  ์ ์œผ๋กœ๋Š” Loading ์ปดํฌ๋„ŒํŠธ๋Š” Hard Navigation์˜ ๊ฒฝ์šฐ์—๋งŒ ํ‘œ์‹œ๋˜๋ฉฐ Soft Navigation์˜ ๊ฒฝ์šฐ์—๋Š” ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ฆ‰, loading.tsx๋Š” ์ „์ฒด ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ์—๋งŒ ์ž๋™์œผ๋กœ ๋™์ž‘ํ•˜๋ฉฐ Soft Navigation์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ์ˆ˜๋™์œผ๋กœ ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

error.tsx

error.tsx ํŒŒ์ผ์ด export defaultํ•œ ์ปดํฌ๋„ŒํŠธ๋Š” Next๊ฐ€ ์ž๋™์œผ๋กœ Page ์ปดํฌ๋„ŒํŠธ๋ฅผ ErrorBoundary ์ปดํฌ๋„ŒํŠธ๋กœ ๋ž˜ํ•‘ํ•˜๊ณ  fallback prop์— error.tsx๊ฐ€ export defaultํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ „๋‹ฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

<Layout>
  <ErrorBoudnary fallback={<Error />}>
    <Page />
  </ErrorBoudnary>
</Layout>

Error ์ปดํฌ๋„ŒํŠธ๋Š” ํ•„์ˆ˜์ ์œผ๋กœ "use client";๋ฅผ ์ž‘์„ฑํ•˜์—ฌ RCC๋กœ์„œ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ์—๋Ÿฌ ๋ชจ๋‘๋ฅผ ์บ์น˜ํ•˜๊ธฐ ์œ„ํ•ด์„œ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋Š” ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ์–‘์ชฝ์—์„œ ์‹คํ–‰๋˜์ง€๋งŒ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

Error ์ปดํฌ๋„ŒํŠธ๋Š” props๋กœ error์™€ reset์„ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค. error๋กœ ๋ฐœ์ƒํ•œ ์—๋Ÿฌ ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌ๋ฐ›์œผ๋ฉฐ, reset ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜๋ฉด Next๋Š” ๋ฆฌ๋ Œ๋”๋ง์„ ์‹œ๋„ํ•˜๋ฉฐ, ๋ฆฌ๋ Œ๋”๋ง ์„ฑ๊ณต์‹œ ์„ฑ๊ณตํ•œ ๋ Œ๋”๋ง ๊ฒฐ๊ณผ๋กœ ๊ต์ฒด์‹œ์ผœ์ค๋‹ˆ๋‹ค.

'use client' // Error ์ปดํฌ๋„ŒํŠธ๋Š” ํ•„์ˆ˜์ ์œผ๋กœ RCC๋กœ ์„ค์ •

interface ErrorProps {
  error: Error
  reset: () => void
}

export default function Error({ error, reset }: ErrorProps) {
  return (
    <section>
      An Error occurred: {error.message}<br />
      <button onClick={() => reset()}>Retry</button>
    </section>
  )
}

global-error.tsx

error.tsx๋Š” layout.tsx๋‚˜ template.tsx์—์„œ ๋ฐœ์ƒํ•œ ์—๋Ÿฌ๋Š” ์บ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ํ•ด๋‹น ์—๋Ÿฌ๋ฅผ ์บ์น˜ํ•˜๋ ค๋ฉด global-error.tsx๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ „๋‹ฌ๋ฐ›๋Š” props๋Š” error.tsx์˜ Error ์ปดํฌ๋„ŒํŠธ์™€ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

not-found.tsx

not-found.tsx ํŒŒ์ผ์€ ๋งค์นญ๋˜๋Š” ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์— ํ‘œ์‹œํ•  ์ปดํฌ๋„ŒํŠธ ์ž…๋‹ˆ๋‹ค.

์ถ”๊ฐ€์ ์œผ๋กœ "next/navigation"์˜ notFound ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด not-found.tsx ํŒŒ์ผ์—์„œ export default๋œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

Route Handlers

Route Handlers๋Š” ์š”์ฒญ ํ•˜๋ฉด ์‘๋‹ต์œผ๋กœ ํŽ˜์ด์ง€๊ฐ€ ์•„๋‹Œ JSON์„ ์‘๋‹ต์œผ๋กœ ์ „๋‹ฌํ•ด์ฃผ๋Š” API ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, Route Handler๋Š” ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜๋กœ ์ฟ ํ‚ค๋‚˜ ํ—ค๋”์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Route Handlers๋Š” ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ ์š”์ฒญ ๊ฐ์ฒด(NextRequest)๋ฅผ ์ „๋‹ฌ๋ฐ›๊ณ , ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ๋Š” ๋™์  ๋ผ์šฐํŒ…์˜ ๊ฒฝ์šฐ params๋ผ๋Š” ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ–๋Š” ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌ๋ฐ›์œผ๋ฉฐ params ํ”„๋กœ๋Ÿฌํ‹ฐ๋Š” ๋™์  ๊ฒฝ๋กœ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๊ฐ์ฒด๋กœ์„œ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค.

Route Handlers๋Š” "app/api" ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ดํ›„ ์ค‘์ฒฉ๋œ ๋””๋ ‰ํ† ๋ฆฌ๋ช…์ด path๊ฐ’ ์ผ๋ถ€๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ํŒŒ์ผ ๋„ค์ด๋ฐ์€ "route.ts"๋กœ ์ถ”๊ฐ€ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, "app/api/items/[id]/route.ts"๋ผ๋Š” ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์š”์ฒญํ•  ๋•Œ๋Š” "/api/items/1"๋กœ ์š”์ฒญํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

Route Handlers๊ฐ€ ์ง€์›ํ•˜๋Š” HTTP Method๋กœ๋Š” GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS์„ ์ง€์›ํ•˜๋ฉฐ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. route.ts ํŒŒ์ผ์€ ์ง€์›ํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ช…์„ ๊ฐ–๋Š” ํ•จ์ˆ˜๋ฅผ exportํ•ด์•ผ ํ•˜๋ฉฐ async ํ•จ์ˆ˜๋กœ ์ •์˜ํ•˜์—ฌ API๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// app/api/route.ts
import { NextRequest, NextResponse } from 'next/server'

type Params = Promise<{ slug: string }>

export async function GET(request: NextRequest, context: { params?: Params }) {
  try {
    const requestBody = await request.json() // ์š”์ฒญ body ๊ฐ’

    const responseBody = { success: true }

    return NextResponse.json(responseBody, { status: 200 })
  } catch (error) {
    return NextResponse.json('Fail to fetch data', { status: 500 })
  }
}

Middleware

๋ฏธ๋“ค์›จ์–ด๋ž€ ์‹ค์ œ ์š”์ฒญ์„ ๊ฑฐ์น˜๊ธฐ ์ „์— ํŠน์ • ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ๊ฒฝ๋กœ์— "middleware.ts"๋ผ๋Š” ๋„ค์ด๋ฐ์œผ๋กœ ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ฃผ์˜ํ•  ์ ์œผ๋กœ ํŽ˜์ด์ง€๋‚˜ API๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋ชจ๋“  ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์š”์ฒญ๋“ค๋„ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๊ฑฐ์น˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

middleware ํ•จ์ˆ˜๋Š” async ํ•จ์ˆ˜๋กœ ์ •์˜ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ธ์ˆ˜๋กœ ์š”์ฒญ ๊ฐ์ฒด(NextRequest)๋ฅผ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค.

import { NextRequest, NextResponse } from 'next/server'

export async function middleware(request: NextReqeust) {
  return NextResponse.redirect(new URL('/home', request.url))
}

export const config = {
  matcher: ['/about/:path', '/dashboard/:path']
}

๋ฏธ๋“ค์›จ์–ด ์„ค์ •์˜ ๊ฒฝ์šฐ์—๋Š” config๋ผ๋Š” ๊ฐ์ฒด๋ฅผ exportํ•˜์—ฌ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, config.matcher๋ฅผ ํ†ตํ•ด ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‹คํ–‰ํ•  ํŠน์ • ๊ฒฝ๋กœ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์œ„ ์˜ˆ์ œ์˜ ๊ฒฝ์šฐ์—๋Š” "/about" ์ดํ•˜ ๋ชจ๋“  ๊ฒฝ๋กœ์™€ "/dashboard" ์ดํ•˜ ๋ชจ๋“  ๊ฒฝ๋กœ์— ๋Œ€ํ•ด์„œ ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. matcher๋Š” ์ •๊ทœํ‘œํ˜„์‹์œผ๋กœ๋„ ์ž‘์„ฑ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์˜ํ•  ์ ์œผ๋กœ๋Š” config ๊ฐ์ฒด ์ž์ฒด๋Š” ๋นŒ๋“œํƒ€์ž„์— ์‹คํ–‰๋˜์–ด ์ ์šฉ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋™์ ์ธ ๊ฐ’์€ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

NextRequest & NextResponse

"next/server"๊ฐ€ ์ œ๊ณตํ•˜๋Š” NextRequest์™€ NextResponse๋Š” Web Request/Response API๋ฅผ ํ™•์žฅํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

import { NextRequest, NextResponse } from 'next/server'

const request = new NextRequest()
const response = NextResponse.next()

// ์š”์ฒญ/์‘๋‹ต cookie ๊ฐ’์„ set ์‹œ์ผœ์ค๋‹ˆ๋‹ค.
request.cookies.set('key', 'value')
response.cookies.set('key', 'value')

// ๋งค์นญ๋œ cookie ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
// ๋งค์นญ๋œ cookie๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ undefined๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ์—ฌ๋Ÿฌ ๊ฐœ๊ฐ€ ๋งค์นญ๋œ ๊ฒฝ์šฐ ์ฒซ ๋ฒˆ์งธ๋กœ ๋งค์นญ๋œ cookie๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
request.cookies.get('key')
response.cookies.get('key')

// ๋งค์นญ๋œ ๋ชจ๋“  cookie ๊ฐ’์„ ๋ฐฐ์—ด์— ๋‹ด์•„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
request.cookies.getAll('key')
response.cookies.getAll('key')

// ๋งค์นญ๋œ cookie ๊ฐ’์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
// ๋ฐ˜ํ™˜๊ฐ’์€ ์ œ๊ฑฐ ์„ฑ๊ณต ์—ฌ๋ถ€๋ฅผ ๋ถˆ๋ฆฌ์–ธ ๊ฐ’์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
request.cookies.delete('key')
response.cookies.delete('key')

// ๋งค์นญ๋œ cookie ๊ฐ’ ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ๋ถˆ๋ฆฌ์–ธ ๊ฐ’์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
request.cookies.has('key')

// ์š”์ฒญ์˜ Set-Cookie ํ—ค๋”๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
request.cookies.clear()

// URL ๋„๋ฉ”์ธ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
request.nextUrl.baseUrl

// URL path๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
request.nextUrl.pathname

// URL ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง ๊ฐ’์„ ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
request.nextUrl.searchParams

// JSON์„ body๋กœ ๊ฐ–๋Š” ์‘๋‹ต์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
NextResponse.json({ success: true }, { status: 200 })

// ํŠน์ • URL๋กœ redirect์‹œํ‚ค๋Š” ์‘๋‹ต์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
// ํด๋ผ์ด์–ธํŠธ์ธก์—์„œ ํ•ด๋‹น ์‘๋‹ต์„ ์ „๋‹ฌ๋ฐ›๊ฒŒ ๋˜๋ฉด "/home" ๊ฒฝ๋กœ๋กœ ์ด๋™ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
NextResponse.redirect(new URL('/home', reqeust.url))

// rewrite ๋ฉ”์„œ๋“œ๋Š” route handler์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ณ , next middleware์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
// redirect์™€๋Š” ๋‹ค๋ฅด๊ฒŒ ์š”์ฒญํ•œ URL path๊ฐ’์€ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ , ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋‚˜ route handler๋กœ ์š”์ฒญ์„ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
NextResponse.rewrite(new URL('/proxy', reqeust.url))

// next ๋ฉ”์„œ๋“œ๋Š” route handler์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ณ , next middleware์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
// next ๋ฉ”์„œ๋“œ๋Š” ์š”์ฒญ์„ ์ค‘๋‹จํ•˜์ง€ ์•Š๊ณ  ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋„˜๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, ๋ฏธ๋“ค์›จ์–ด ์ดํ›„ ์‹ค์ œ ์š”์ฒญ์„ ์ด์–ด์„œ ์ง„ํ–‰ํ•˜๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.
NextResponse.next()

Route Segment Config

layout.tsx, page.tsx, Route Handlers์—๋Š” Route Segment ์˜ต์…˜์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Route Segment ์˜ต์…˜์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ Next ์„œ๋ฒ„์— ์บ์‹ฑ๋˜์–ด ์žˆ๋Š” Data Cache์™€ Full Route Cache๋ฅผ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

dynamic

  • "auto"(default): Next๊ฐ€ ํŽ˜์ด์ง€์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํ…Œ์ดํ„ฐ ์†Œ์Šค์™€ fetch ์š”์ฒญ์„ ๋ถ„์„ํ•˜์—ฌ ์ž๋™์œผ๋กœ ํŽ˜์ด์ง€ ์ƒ์„ฑ ๋ฐฉ์‹์„ ์ •์  ํ˜น์€ ๋™์ ์œผ๋กœ ๊ฒฐ์ •ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  • "force-dynamic": Next๊ฐ€ ํŽ˜์ด์ง€ ์ƒ์„ฑ ๋ฐฉ์‹์„ ๋™์  ์ƒ์„ฑ์œผ๋กœ ๊ฐ•์ œํ•˜๋ฉฐ(Full Route Cache), fetch ์š”์ฒญ์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์บ์‹ฑํ•˜์ง€ ์•Š๊ณ  ์–ธ์ œ๋‚˜ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค(Data Cache). ์ฆ‰, ํŽ˜์ด์ง€ ์š”์ฒญํ•  ๋•Œ๋งˆ๋‹ค fetch๋กœ ๊ฐ€์ ธ์˜จ ์ตœ์‹  ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒ์„ฑํ•œ ํŽ˜์ด์ง€๋ฅผ ํด๋ผ์ด์–ธํŠธ์ธก์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

  • "force-static": Next๊ฐ€ ํŽ˜์ด์ง€ ์ƒ์„ฑ ๋ฐฉ์‹์„ ์ •์  ์ƒ์„ฑ์œผ๋กœ ๊ฐ•์ œํ•˜๋ฉฐ(Full Route Cache), fetch ์š”์ฒญ์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ๋„ ์–ธ์ œ๋‚˜ ์บ์‹ฑํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค(Data Cache). ์ฆ‰, fetch์— ๋Œ€ํ•œ ์‘๋‹ต๊ฐ’์„ Next ์„œ๋ฒ„์— ์บ์‹ฑ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜์—ฌ ๋นŒ๋“œ ํƒ€์ž„๋•Œ ์ƒ์„ฑํ•œ ํŽ˜์ด์ง€๋ฅผ ํด๋ผ์ด์–ธํŠธ์ธก์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

export const dynamic = "force-dynamic"

export const dynamic = "force-static"

// ,,,

dynamicParams

  • true(default): ๋‹ค์ด๋‚˜๋ฏน ๋ผ์šฐํŒ… ์ฒ˜๋ฆฌ๊ฐ€ ๋Ÿฐํƒ€์ž„์— ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์š”์ฒญ๋œ ๊ฒฝ๋กœ๋กœ ๋™์ ์œผ๋กœ ํ•ด์„๋˜์–ด ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

  • false: ๋™์  ๊ฒฝ๋กœ๊ฐ€ ๋Ÿฐํƒ€์ž„์ด ์•„๋‹Œ ๋นŒ๋“œ ํƒ€์ž„๋•Œ ๊ฒฐ์ •๋˜๊ธฐ ๋•Œ๋ฌธ์— generateStaticParams๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ exportํ•˜์—ฌ ๋นŒ๋“œ ํƒ€์ž„์— ์ƒ์„ฑ๋  ๋™์  ๊ฒฝ๋กœ ์ •๋ณด๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

export const dynamicParams = false

type Params = Promise<{ slug: string }>

interface PageProps {
  params?: Params
}

export default function Page({ params }: PageProps) {
  // ,,,
}

export async function generateStaticParams() {
  const paths = [{ params: { slug: 'post-1' } }, { params: { slug: 'post-2' } }]

  return { paths, fallback: false }
}

revalidate

Data Cache์™€ Full Route Cache์˜ ์บ์‹ฑ ์ง€์†์‹œ๊ฐ„์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • false(default): fetch์˜ ์‘๋‹ต ๋ฐ์ดํ„ฐ๋Š” Next ์„œ๋ฒ„์˜ Data Cache์— ์บ์‹ฑ๋œ ๋ฐ์ดํŠธ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๊ณ , ํŽ˜์ด์ง€๋Š” ๋นŒ๋“œ ํƒ€์ž„๋•Œ ์ƒ์„ฑํ•œ ํŽ˜์ด์ง€๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  • number: Next ์„œ๋ฒ„์˜ Data Cache์— ์บ์‹ฑ๋œ fetch ์‘๋‹ต๊ฐ’๊ณผ Full Route Cache์˜ ์ง€์†์‹œ๊ฐ„์„ ์ดˆ ๋‹จ์œ„๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

export const revalidate = 3600 // 60 seconds

// ,,,

fetchCache

fetch ์š”์ฒญ์— ๋Œ€ํ•œ ์บ์‹ฑ ์š”์ฒญ์„ ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ ๋ณ„๋กœ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋งํ•ด, fetch์˜ Data Cache ์„ค์ •์„ ๋ผ์šฐํŠธ๋ณ„๋กœ ์ง€์ •ํ•˜๋Š” ์˜ต์…˜์ž…๋‹ˆ๋‹ค.

  • "auto"(default): cache ์˜ต์…˜์„ ๋ช…์‹œํ•˜์ง€ ์•Š์€ fetch์— ๋Œ€ํ•ด cache ์˜ต์…˜์„ "no-store"์œผ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • "default-cache": fetch์˜ cache ์˜ต์…˜์ด ๋ช…์‹œ๋˜์–ด ์žˆ์ง€ ์•Š์€ fetch์— ๋Œ€ํ•ด cache ์˜ต์…˜์„ "force-cache"๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • "force-cache": ๋ชจ๋“  fetch์˜ Data Cache ์‚ฌ์šฉ์„ ๊ฐ•์ œํ•˜๋ฉฐ fetch์˜ cache ์˜ต์…˜์„ "force-cache" ์„ค์ •์œผ๋กœ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.

  • "only-cache": ๋ชจ๋“  fetch์˜ Data Cache ์‚ฌ์šฉ์„ ๊ฐ•์ œํ•˜๋ฉฐ fetch์˜ cache ์˜ต์…˜์„ "force-cache" ์„ค์ •์œผ๋กœ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค. ์ถ”๊ฐ€์ ์œผ๋กœ "no-store"๋ฅผ ์‚ฌ์šฉํ•˜๋Š” fetch์— ๋Œ€ํ•ด์„œ๋Š” ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค,

  • "default-no-store": fetch์˜ cache ์˜ต์…˜์ด ๋ช…์‹œ๋˜์–ด ์žˆ์ง€ ์•Š์€ fetch์— ๋Œ€ํ•ด cache ์˜ต์…˜์„ "no-store"๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • "force-no-store": ๋ชจ๋“  fetch์˜ Data Cache๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉฐ fetch์˜ cache ์˜ต์…˜์„ "no-store" ์„ค์ •์œผ๋กœ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.

  • "only-no-store": ๋ชจ๋“  fetch์˜ Data Cache๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉฐ fetch์˜ cache ์˜ต์…˜์„ "no-store" ์„ค์ •์œผ๋กœ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค. ์ถ”๊ฐ€์ ์œผ๋กœ "force-cache"๋ฅผ ์‚ฌ์šฉํ•˜๋Š” fetch์— ๋Œ€ํ•ด์„œ๋Š” ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

Linking and Navigating

Link

"next/link"๊ฐ€ ์ œ๊ณตํ•˜๋Š” Link ์ปดํฌ๋„ŒํŠธ๋Š” html a ํƒœ๊ทธ๋ฅผ ํ™•์žฅํ•œ ์ปดํฌ๋„ŒํŠธ๋กœ์„œ prefetching ๊ธฐ๋Šฅ๊นŒ์ง€ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Link ์ปดํฌ๋„ŒํŠธ๋Š” RSC์™€ RCC ๋‘˜ ๋‹ค ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Link ์ปดํฌ๋„ŒํŠธํŠธ๋Š” a ํƒœ๊ทธ๋ฅผ ํ™•์žฅํ•œ ์ปดํฌ๋„ŒํŠธ๋กœ a ํƒœ๊ทธ์— ์ž‘์„ฑ ๊ฐ€๋Šฅํ•œ ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋“ค์„ ๊ทธ๋Œ€๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import Link from 'next/link'

export default function Page() {
  return (
    <>
      <Link href='/item' />
    </>
  )
}

useRouter

"next/navigation"๊ฐ€ ์ œ๊ณตํ•˜๋Š” userRouter ํ›…์ด ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด์„œ Soft Navigating์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. useRouter๋Š” ๋ฆฌ์•กํŠธ ํ›…์œผ๋กœ RCC์—์„œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

'use client'

import { useRouter } from 'next/navigation'

export default function Page() {
  const router = useRouter()

  // History stack์— ํ•˜๋‚˜์˜ ์Šคํƒ์„ push ํ•˜๊ณ  ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.
  router.push('/items')

  // ํ˜„์žฌ URL์— ๋Œ€ํ•ด refresh๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  router.refresh()

  // ํŠน์ • URL์„ prefetchingํ•˜์—ฌ ๋” ๋น ๋ฅธ Navigating์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  router.prefetch('/item')

  // History stack์—์„œ ํ•˜๋‚˜์˜ ์Šคํƒ์„ pop ํ•˜๊ณ  ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.
  router.back()

  // History stack์—์„œ ํ•˜๋‚˜์˜ ์Šคํƒ ์•ž์œผ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.
  router.forward()

  // ,,,
}

permanentRedirect

"next/navigation"์ด ์ œ๊ณตํ•˜๋Š” permanentRedirect ํ•จ์ˆ˜๋Š” RCC, RSC, Route Handler, Server Action ๋ชจ๋‘ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

import { permanentRedirect } from 'next/navigation'

export default function Page() {
  permanentRedirect('/login', { type: 'replace' })

  // ,,,
}

permanentRedirect ํ•จ์ˆ˜ ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ๋Š” URL์„ ์ „๋‹ฌํ•˜๊ณ  ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ๋Š” ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ type ํ”„๋กœํผํ‹ฐ์— "replace"(default) ํ˜น์€ "push"๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์˜ํ•  ์ ์œผ๋กœ Server Actions์—์„œ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ์—๋Š” type์˜ default๊ฐ€ push๋กœ ๋™์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

redirect

"next/navigation"์ด ์ œ๊ณตํ•˜๋Š” redirect ํ•จ์ˆ˜๋Š” RSC, Route Handler, Server Action์—์„œ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

import { redirect } from 'next/navigation'

export default function Page() {
  redirect('/login', { type: 'replace' })

  // ,,,
}

redirect ํ•จ์ˆ˜ ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ๋Š” URL์„ ์ „๋‹ฌํ•˜๊ณ  ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ๋Š” ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ type ํ”„๋กœํผํ‹ฐ์— "replace"(default) ํ˜น์€ "push"๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

notFound

"next/navigation"์ด ์ œ๊ณตํ•˜๋Š” notFound ํ•จ์ˆ˜ ํ˜ธ์ถœ ์‹œ not-found.tsx ํŒŒ์ผ์—์„œ export default๋œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

Functions

cookie(Dynamic Function)

"next/headers"๊ฐ€ ์ œ๊ณตํ•˜๋Š” cookies ํ•จ์ˆ˜๋Š” RSC, Route Handler, Server Action์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ•จ์ˆ˜๋กœ ์š”์ฒญ ๊ฐ์ฒด์˜ ์ฟ ํ‚ค ๊ฐ’์„ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { cookies } from 'next/headers'

export default async function Page() {
  // ์š”์ฒญ ๊ฐ์ฒด์— ๋Œ€ํ•œ ์ฟ ํ‚ค
  const cookieStore = await cookies()

  // ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•œ ์ฟ ํ‚ค ์ด๋ฆ„๊ณผ ๋งค์นญ๋œ ์ฟ ํ‚ค๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋งค์นญ๋œ ์ฟ ํ‚ค๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ undeinfed๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  // ๋งค์นญ๋œ ๊ฒฐ๊ณผ๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ์ธ ๊ฒฝ์šฐ์—๋„ ํ•˜๋‚˜๋งŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  cookieStore.get('key')

  // ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•œ ์ฟ ํ‚ค ์ด๋ฆ„๊ณผ ๋งค์นญ๋œ ์ฟ ํ‚ค๊ฐ’๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  // get ๋ฉ”์„œ๋“œ์™€๋Š” ๋‹ค๋ฅด๊ฒŒ ๋งค์นญ๋œ ๋ชจ๋“  ์ฟ ํ‚ค๊ฐ’๋“ค์„ ์š”์†Œ๋กœ ๊ฐ–๋Š” ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  cookieStore.getAll('key')

  // ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•œ ์ฟ ํ‚ค ์ด๋ฆ„๊ณผ ๋งค์นญ๋œ ์ฟ ํ‚ค๊ฐ’ ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ๋ถˆ๋ฆฌ์–ธ ๊ฐ’์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  cookieStore.has('key')

  // ,,,
}
import { cookies } from 'next/headers'

export async function GET() {
  // ์š”์ฒญ ๊ฐ์ฒด์— ๋Œ€ํ•œ ์ฟ ํ‚ค
  const cookieStore = await cookies()

  // ์š”์ฒญ ๊ฐ์ฒด์˜ ์ฟ ํ‚ค๊ฐ’์„ setํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  // ์ฃผ์˜ํ•  ์ ์œผ๋กœ set ๋ฉ”์„œ๋“œ๋Š” Server Action์™€ Route Handler์—์„œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  cookieStore.set('key', 'value')
  cookieStore.set({ name: 'key', value: 'value' })

  // ์š”์ฒญ ๊ฐ์ฒด์˜ ์ฟ ํ‚ค๊ฐ’์„ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  // ์ฃผ์˜ํ•  ์ ์œผ๋กœ delete ๋ฉ”์„œ๋“œ๋Š” Server Action์™€ Route Handler์—์„œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  cookieStore.delete('key')

  // ,,,
}

headers(Dynamic Function)

"next/headers"๊ฐ€ ์ œ๊ณตํ•˜๋Š” headers ํ•จ์ˆ˜๋Š” RSC, Route Handler, Server Action์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ•จ์ˆ˜๋กœ ์š”์ฒญ ํ—ค๋” ๊ฐ’์„ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

headers ํ•จ์ˆ˜๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ—ค๋” ๊ฐ’์€ ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ set, delete์™€ ๊ฐ™์€ ๋™์ž‘์€ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

import { headers } from 'next/headers'

export default async function Page() {
  // ์š”์ฒญ ๊ฐ์ฒด์— ๋Œ€ํ•œ ํ—ค๋”
  const headersList = await headers()

  // headers ๊ฐ’์„ key, value๋กœ ๊ฐ–๋Š” ์ดํ„ฐ๋ ˆ์ดํ„ฐ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  headersList.entries()

  // ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•œ ์ฝœ๋ฐฑ์€ key, value๋กœ ๊ฐ–๋Š” ๊ฐ์ฒด๋ฅผ ์ธ์ˆ˜๋กœ ์ „๋‹ฌ๋ฐ›์•„ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
  headersList.forEach()

  // ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•œ key์™€ ๋งค์นญ๋œ value๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  headersList.get();

  // ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•œ key์™€ ๋งค์นญ๋œ value ์—ฌ๋ถ€๋ฅผ ๋ถˆ๋ฆฌ์–ธ ๊ฐ’์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  headersList.has();

  // key ๊ฐ’์„ ๊ฐ–๋Š” ์ดํ„ฐ๋ ˆ์ดํ„ฐ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  headersList.keys();

  // value ๊ฐ’์„ ๊ฐ–๋Š” ์ดํ„ฐ๋ ˆ์ดํ„ฐ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  headersList.values();
}

fetch

Next.js๋Š” Web fetch API๋ฅผ ํ™•์žฅํ•˜์—ฌ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. fetch ์ž์ฒด๋Š” Web API๋กœ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์ง€๋งŒ Next์—์„œ๋Š” RSC, Server Actions, Route Handlers ๋“ฑ ๋ชจ๋‘ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

export default async function Page() {
  // no-store: ๊ธฐ๋ณธ ์บ์‹ฑ ๋™์ž‘์ด๋ฉฐ ํ•ญ์ƒ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค (Data Cache ๋น„ํ™œ์„ฑ)
  await fetch('https://,,,', { cache: 'no-store' })

  // force-cache: ์ด์ „์— ์š”์ฒญํ•˜์—ฌ ๋ฐ›์€ ์‘๋‹ต ๋ฐ์ดํ„ฐ๊ฐ€ Next ์„œ๋ฒ„์— ์บ์‹ฑ๋˜์–ด ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ์žฌ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค (Data Cache ํ™œ์„ฑ)
  // ์ด๋Š” ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜์˜ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  await fetch('https://,,,', { cache: 'force-cache' })

  // revalidate ์˜ต์…˜์„ ํ†ตํ•ด cache life time์„ ๋ช…์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  // ์ดˆ ๋‹จ์œ„ ์ˆซ์ž๊ฐ’์„ ์ž‘์„ฑํ•˜์—ฌ cache life time์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, revalidate ๊ฐ’์„ 0์€ no-store์ด๋ฉฐ ์–‘์ˆ˜๊ฐ’์„ ์ž‘์„ฑํ•œ ๊ฒฝ์šฐ force-cache๋ฅผ ์•”์‹œํ•ฉ๋‹ˆ๋‹ค.
  await fetch('https://,,,', { next: { revalidate: 10 } })

  // ,,,
}

์ฆ‰, Next๋Š” fetch ์š”์ฒญ์— cache ์˜ต์…˜์„ ๋ช…์‹œํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๊ธฐ๋ณธ์ ์œผ๋กœ "no-store"๊ฐ€ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ ์šฉ๋˜์–ด Data Cache๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

fetch ์š”์ฒญ์— Data Cache ํ™œ์„ฑ์„ ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • fetch์˜ cache ์˜ต์…˜์„ "force-cache"๋กœ ์„ค์ •

  • Route Segment Config์˜ dynamic์„ "force-static"์œผ๋กœ ์„ค์ •

  • Route Segment Config์˜ fetchCache๋ฅผ "force-cache", "only-cache" ํ˜น์€ "default-cache"๋กœ ์„ค์ •

๋งŒ์•ฝ dynamic ์˜ต์…˜์„ "force-dynamic"์œผ๋กœ ์„ค์ •ํ•˜๊ณ  fetchCache๋ฅผ "force-cache", "only-cache" ํ˜น์€ "default-cache"๋กœ ์„ค์ •ํ•˜๋ฉด ํŽ˜์ด์ง€๋Š” ๋งค๋ฒˆ ๋™์ ์œผ๋กœ ์ƒ์„ฑ๋˜์ง€๋งŒ fetch ์š”์ฒญ์„ Next ์„œ๋ฒ„์— ์บ์‹ฑ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค๊ฒŒ ๋˜๊ณ , dynamic ์˜ต์…˜์„ "force-static" fetchCache์˜ต์…˜์„ "force-no-store", "only-no-store" ํ˜น์€ "default-no-store"๋กœ ์„ค์ •ํ•˜๋ฉด ํŽ˜์ด์ง€๋Š” ์ •์ ์œผ๋กœ ์ƒ์„ฑ๋˜์ง€๋งŒ fetch ์š”์ฒญ ๋ฐ์ดํ„ฐ๋Š” ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

revalidatePath

"next/cache"๊ฐ€ ์ œ๊ณตํ•˜๋Š” revalidatePath ํ•จ์ˆ˜๋Š” ํŠน์ • ๋ผ์šฐํŠธ์— ๋Œ€ํ•œ Data Cache์™€ Full Route Cache ๋ชจ๋‘ ๋ฌดํšจํ™”ํ•˜๊ณ  ์žฌ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

revalidatePath๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ํ•ด๋‹น ๊ฒฝ๋กœ์—์„œ Next ์„œ๋ฒ„์— ์บ์‹ฑ๋œ fetch ์‘๋‹ต ๋ฐ์ดํ„ฐ์™€ ๋นŒ๋“œ ํƒ€์ž„๋•Œ ์ƒ์„ฑ๋˜์–ด ์บ์‹ฑ๋œ ํŽ˜์ด์ง€๋ฅผ ๋ฌดํšจํ™”์‹œํ‚ต๋‹ˆ๋‹ค. ์ดํ›„ ๋‹ค์Œ ๋ฒˆ์— ์‚ฌ์šฉ์ž๊ฐ€ ํ•ด๋‹น ๊ฒฝ๋กœ๋ฅผ ์š”์ฒญํ•œ ๊ฒฝ์šฐ Next๊ฐ€ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ ํŽ˜์ด์ง€๋ฅผ ๋‹ค์‹œ ์ƒ์„ฑํ•˜์—ฌ ์‘๋‹ต์œผ๋กœ ์ „๋‹ฌํ•ด์ค๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์—์„œ ์ƒˆ๋กญ๊ฒŒ ์ƒ์„ฑ๋œ ์ •์  ํŒŒ์ผ๊ณผ ๋ฐ์ดํ„ฐ๊ฐ€ ์บ์‹œ์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

revalidatePath ํ•จ์ˆ˜๋Š” Route Handlers๋‚˜ Server Actions์—์„œ ํ˜ธ์ถœ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

import { NextRequest, NextResponse } from 'next/server'
import { revalidatePath } from 'next/cache'

export async function POST(request: NextRequest) {
  const { path } = request.nextUrl.searchParams('path')

  if (path) {
    revalidatePath(path, 'page')

    return NextResponse.json({ revalidated: true, now: Date.now() })
  } else {
    return NextResponse.json({ revalidated: false, now: Date.now() })
  }
}

revalidatePath ํ•จ์ˆ˜ ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ๋Š” ๋ฌดํšจํ™”ํ•  ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ , ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ๋Š” "layout" ํ˜น์€ "page"๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

  • "layout": "layout" ์ „๋‹ฌ ์‹œ ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•œ ๋ผ์šฐํŠธ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ํ•˜์œ„ ๋ผ์šฐํŠธ๊นŒ์ง€ ๋ชจ๋‘ ์žฌ๊ฒ€์ฆํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  • "page"(default): "page" ์ „๋‹ฌ ์‹œ ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•œ ๋ผ์šฐํŠธ๋งŒ ์žฌ๊ฒ€์ฆํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค,

์ฆ‰, "layout"์ด "page"๋ณด๋‹ค ์žฌ๊ฒ€์ฆํ•˜๋Š” ๋ฒ”์œ„๊ฐ€ ๋” ํฝ๋‹ˆ๋‹ค.

useParams

"next/navigation"์ด ์ œ๊ณตํ•˜๋Š” useParams ํ›…์€ ๋™์  ๋ผ์šฐํŒ…ํ•˜๋Š” ๊ฒฝ์šฐ ๋™์ ์œผ๋กœ ๊ฒฐ์ •๋œ path๊ฐ’์„ ๊ฐ์ฒด ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

'use client'

import { useParams } from 'next/navigation'

export default function ClientComponent() {
  const params = useParams()

  // ,,,
}

usePathname

"next/navigation"์ด ์ œ๊ณตํ•˜๋Š” usePathname ํ›…์€ ํ˜„์žฌ URL์˜ path ๊ฐ’์„ string์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

'use client'

import { usePathname } from 'next/navigation'

export default function ClientComponent() {
  const pathanem = usePathname()

  // ,,,
}

useSearchParams

"next/navigation"์ด ์ œ๊ณตํ•˜๋Š” useSearchParams ํ›…์€ ํ˜„์žฌ URL์˜ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง ์ •๋ณด๋ฅผ ์ฝ๊ธฐ ์ „์šฉ์ธ URLSearchParams ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

'use client'

import { useSearchParams } from 'next/navigation'

export default function ClientComponent() {
  const searchParams = useSearchParams()

  // ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์˜ value ๊ฐ’์„ ์š”์†Œ๋กœ ๊ฐ–๋Š” ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  searchParams.getAll()

  // ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์˜ key ๊ฐ’์„ ์š”์†Œ๋กœ ๊ฐ–๋Š” ์ดํ„ฐ๋ ˆ์ดํ„ฐ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  searchParams.keys()

  // ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์˜ value ๊ฐ’์„ ์š”์†Œ๋กœ ๊ฐ–๋Š” ์ดํ„ฐ๋ ˆ์ดํ„ฐ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  searchParams.values()

  // ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์˜ key, value ๊ฐ’์„ ์š”์†Œ๋กœ ๊ฐ–๋Š” ๋ฐฐ์—ด์„ ์š”์†Œ๋กœ ๊ฐ–๋Š” ์ดํ„ฐ๋ ˆ์ดํ„ฐ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  searchParams.entries()

  // ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•œ ์ฝœ๋ฐฑ์€ key, value ๊ฐ’์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ „๋‹ฌ๋ฐ›์œผ๋ฉฐ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
  searchParams.forEach((key, value) => {
    // ,,,
  })

  // ,,,
}

useSelectedlayoutSegment && useSelectedlayoutSegments

"next/navigation"์ด ์ œ๊ณตํ•˜๋Š” useSelectedLayoutSegment ํ›…์€ ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ํ•œ ๋‹จ๊ณ„ ํ•˜์œ„ ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. useSelectedLayoutSegments ํ›…์€ ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ๋ชจ๋“  ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํฌ ๊ฐ’์„ ์š”์†Œ๋กœ ๊ฐ–๋Š” ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ํ˜„์žฌ URL path ๊ฐ’์ด "/blog/hello-world"์ธ ๊ฒฝ์šฐ useSelectedLayoutSegment ํ›…์€ "hello-world"๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , useSelectedLayoutSegments ํ›…์€ ["blog", "hello-world"]๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

Server Actions

Server Actions์€ React์— ๋‚ด์žฅ๋œ ๊ธฐ๋Šฅ์ด๋ฉฐ form ์ œ์ถœ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Server Actions์€ ํ•จ์ˆ˜์ด๋ฉฐ ์ปดํฌ๋„ŒํŠธ ๋‚ด ์ง์ ‘ ์ •์˜ํ•˜๊ฑฐ๋‚˜ ๋ณ„๋„์˜ ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ importํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Server Actions ํ•จ์ˆ˜๋ฅผ form ํƒœ๊ทธ์˜ action ์–ดํŠธ๋ฆฌ๋ทฐํŠธ์— ์ „๋‹ฌํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Server Actions ํ•จ์ˆ˜๋Š” ์ธ์ˆ˜๋กœ FormData ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค. ์ฃผ์˜ํ•  ์ ์œผ๋กœ formData ๊ฐ์ฒด๋กœ ํผ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” form ๋‚ด๋ถ€ ๊ฐ input๋“ค์€ name ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋ฅผ ๊ฐ–๊ณ  ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํผ ๋ฐ์ดํ„ฐ๋“ค์„ ์ ‘๊ทผํ•  ๋•Œ input์˜ name ์–ดํŠธ๋ฆฌ๋ทฐํŠธ ๊ฐ’์œผ๋กœ ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.

์ถ”๊ฐ€์ ์œผ๋กœ Server Action ํ•จ์ˆ˜ async ํ•จ์ˆ˜๋กœ ์ •์˜๋˜์–ด์•ผ ํ•˜๋ฉฐ, ํ•จ์ˆ˜ ์ฝ”๋“œ ๋ธ”๋ก ์ตœ์ƒ๋‹จ์— "use server" ์„ ์–ธ๋ฌธ์„ ์ž‘์„ฑํ•ด์ฃผ์–ด์•ผ ํ•˜๋ฉฐ ๋งŒ์•ฝ ๋ถ„๋ฆฌ๋œ ํŒŒ์ผ๋กœ ์ •์˜๋œ ๊ฒฝ์šฐ ํŒŒ์ผ ์ตœ์ƒ๋‹จ์— "use server"๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Server Actions ํ•จ์ˆ˜๋Š” Next ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜์ด๋ฏ€๋กœ ํด๋ผ์ด์–ธํŠธ์ธก ๋กœ์ง์€ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์œผ๋ฉฐ Server Action ํ•จ์ˆ˜ ์ž์ฒด๋„ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ๋‚ด์—์„œ๋Š” ์ •์˜ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

export default function Page() {
  // Server Action
  async function create(formData: FormData) {
    'use server'

    // ,,,
  }

  return <form action={create}>,,,</form>
}

useFormStatus

useFormStatus ํ›…์€ "react-dom"์ด ์ œ๊ณตํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ ํ›…์œผ๋กœ form ์ œ์ถœ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. useFormStatus ํ›…์„ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” form ํƒœ๊ทธ๋ฅผ ๊ฐ–๋Š” ์ปดํฌ๋„ŒํŠธ ์ž์‹์œผ๋กœ ์ž‘์„ฑ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

'use client'

import { useFormState } from 'react-dom'

export default function SubmitButton() {
  const { pending } = useFormState()

  return (
    <button type='submit' disabled={pending}>
      Add
    </button>
  )
}

useActionState

useActionState ํ›…์€ "react"๊ฐ€ ์ œ๊ณตํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ ํ›…์œผ๋กœ Server Actions๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ’์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. useFormStatus ํ›…๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ form ํƒœ๊ทธ๋ฅผ ๊ฐ–๋Š” ์ปดํฌ๋„ŒํŠธ ๋‚ด ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

useActionState ํ›… ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ๋Š” Server Actions ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•˜๊ณ  ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ๋Š” Server Actions๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ’์˜ ์ดˆ๊ธฐ๊ฐ’์„ ์ „๋‹ฌํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•˜๋Š” Server Actions์€ ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ ์ด์ „ Server Actions๊ฐ€ ๋ฐ˜ํ™˜ํ•œ ๊ฐ’์„ ์ „๋‹ฌ๋ฐ›์œผ๋ฉฐ, ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ๋Š” FormData ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค.

useActionState ํ›…์€ ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ๋ฐฐ์—ด์˜ ์ฒซ ๋ฒˆ์งธ ์š”์†Œ๋Š” Server Actions๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ’, ๋‘ ๋ฒˆ์งธ ์š”์†Œ๋Š” React๊ฐ€ ์ œ์–ดํ•˜๋Š” Server Actions๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ๋‘ ๋ฒˆ์งธ ์š”์†Œ๋กœ ๋ฐ˜ํ™˜ํ•œ Server Action์„ form ํƒœ๊ทธ์˜ action ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋กœ ์ „๋‹ฌํ•˜๋ฉด React๊ฐ€ Server Actions๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ’์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

'use client'

import { useActionState } from 'react'

import { createUser } from '@/app/actions'

const initState = {
  message: ''
}

export default function SignUp() {
  const [state, formAction] = useActionState(createUser, initState);

  return <form action={formAction}>,,,</form>
}

Caching

Request Memoization

Request Memoization์€ ๋™์ผํ•œ ๋ผ์šฐํŠธ์—์„œ ๋™์ผํ•œ ์„ค์ •์„ ๊ฐ–๋Š” fetch Request๋ฅผ ์ค‘๋ณต ์š”์ฒญํ•˜์ง€ ์•Š๊ณ  ๋‹จ์ผ ์š”์ฒญ์œผ๋กœ ์ „๋‹ฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, ๋™์ผํ•œ ๋ผ์šฐํŠธ ๋‚ด ์—ฌ๋Ÿฌ ๋‹ค๋ฅธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋™์ผํ•œ ์„ค์ •์„ ๊ฐ™๋Š” fetch๋ฅผ ํ˜ธ์ถœํ•˜๋”๋ผ๋„ ๋‹จ์ผ ์š”์ฒญ์œผ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ํŽ˜์ด์ง€ ์ฒซ ๋ Œ๋”๋ง ์‹œ์ ์—๋งŒ ์ด๋ฃจ์–ด์ง‘๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, Layout๊ณผ Page ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋กœ ์ž‘์„ฑ๋˜์—ˆ๊ณ , ๋‘ ์ปดํฌ๋„ŒํŠธ ๋ชจ๋‘ ๋™์ผํ•œ ์„ค์ •์„ ๊ฐ–๋Š” fetch ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜๋ฉด ์š”์ฒญ์ด ๋‘ ๋ฒˆ ์ „๋‹ฌ๋˜์ง€ ์•Š๊ณ  ๋‹จ์ผ ์š”์ฒญ์œผ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.

Request Memoization์€ GET ๋ฉ”์„œ๋“œ์ธ fetch์—๋งŒ ์ ์šฉ๋˜๋ฉฐ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ์˜ fetch์—๋งŒ ํ•ด๋‹น๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, Route Handlers์—์„œ์˜ fetch๋Š” ํ•ด๋‹น๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Data Cache

Data Cache๋ž€ Next๊ฐ€ fetch ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜จ ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ Next ์„œ๋ฒ„์ธก์— ์บ์‹ฑํ•˜๊ณ  ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, Next ์„œ๋ฒ„์—์„œ ๋ฐฑ์—”๋“œ๋กœ ๋ณด๋‚ด๋Š” ์š”์ฒญ์— ๋Œ€ํ•ด์„œ Next ์„œ๋ฒ„๊ฐ€ ํ•ด๋‹น ์š”์ฒญ์— ๋Œ€ํ•œ ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ์บ์‹ฑํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋ช…์‹œ์ ์œผ๋กœ Next ์„œ๋ฒ„์— ์บ์‹ฑ๋œ ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ๋ฌดํšจํ™”ํ•˜๊ณ  ์žฌ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•ด ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • Route Segment Config: layout.tsx, page.tsx, Route Handlers์— dynamic, revalidate, fetchCache ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ผ์šฐํŠธ ์ „์ฒด์— ๋Œ€ํ•œ Data Cache ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • revalidatePath('/,,,', 'page' | 'layout'): Route Handlers๋‚˜ Server Actions์—์„œ ํŠน์ • ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ „๋‹ฌํ•˜์—ฌ Data Cache๋ฅผ ๋ฌดํšจํ™”ํ•˜๊ณ  ์žฌ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • fetch('https://,,,', { cache: 'no-store' }): cache ์˜ต์…˜์— "no-store"๋ฅผ ์„ค์ •ํ•˜์—ฌ Data Cache๋ฅผ ๋น„ํ™œ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • fetch('https://,,,', { next: { revalidate: number }}): next.revalidate ์˜ต์…˜์œผ๋กœ Data Cache์˜ life time์„ ์ดˆ๋‹จ์œ„๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์–ด๋– ํ•œ ์„ค์ •๋„ ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ fetch์˜ Data Cache๋Š” ๋น„ํ™œ์„ฑ์ด ๊ธฐ๋ณธ๊ฐ’์ž…๋‹ˆ๋‹ค.

Full Route Cache

๊ธฐ๋ณธ์ ์œผ๋กœ Next๋Š” ๋นŒ๋“œ ํƒ€์ž„๋•Œ ํŽ˜์ด์ง€๋ฅผ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•˜๊ณ  ๋ Œ๋”๋ง๋œ ๊ฒฐ๊ณผ๋ฅผ Next ์„œ๋ฒ„์— ์บ์‹ฑํ•ฉ๋‹ˆ๋‹ค. ์ดํ›„ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•˜๋ฉด Next ์„œ๋ฒ„์— ์บ์‹ฑ๋œ ๋ Œ๋”๋ง ๊ฒฐ๊ณผ๋ฅผ ์ „๋‹ฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋งŒ์•ฝ ํŽ˜์ด์ง€ ์ƒ์„ฑ ๋ฐฉ์‹์„ ๋ช…์‹œ์ ์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • Route Segments Config: layout.tsx, page.tsx, Route Handlers์— dynamic ํ˜น์€ revalidate ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ํŽ˜์ด์ง€ ์ƒ์„ฑ ๋ฐฉ์‹์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • revalidatePath('/,,,', 'page' | 'layout'): Route Handlers๋‚˜ Server Actions์—์„œ ํŠน์ • ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ํŽ˜์ด์ง€ ์ƒ์„ฑ ์‹œ์ ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Dynamic Functions(cookies, headers), searchParams prop ํ˜น์€ Data Cache๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ํ•ด๋‹น ํŽ˜์ด์ง€๋Š” Full Route Cache๊ฐ€ ๋™์ž‘ํ•˜์ง€ ์•Š๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, Full Route Cache๊ฐ€ ๋™์ž‘ํ•˜๋Š” ๊ฒฝ์šฐ๋Š” Data Cache๊ฐ€ ์‚ฌ์šฉํ•˜๋ฉฐ Dynamic Functions๋‚˜ searchParams porp์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์—๋งŒ Full Route Cache๊ฐ€ ๋™์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

Route Cache

Next๋Š” ํด๋ผ์ด์–ธํŠธ์ธก ์ธ๋ฉ”๋ชจ๋ฆฌ์— page.tsx๊ฐ€ export defaultํ•œ Page ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์บ์‹ฑํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, Route Cache๋Š” Next ์„œ๋ฒ„๊ฐ€ ์•„๋‹Œ ํด๋ผ์ด์–ธํŠธ์ธก์—์„œ ์ด๋ฃจ์–ด์ง€๋Š” ์บ์‹ฑ์ž…๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ Route Cache๋Š” ๋น„ํ™œ์„ฑ ์ƒํƒœ์ด๋ฉฐ ์ด๋ฅผ ํ™œ์„ฑํ™”์‹œํ‚ค๊ธฐ ์œ„ํ•ด์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ์„ค์ •์ด ์ถ”๊ฐ€์ ์œผ๋กœ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30,
      static: 180,
    },
  },
}
 
module.exports = nextConfig
  • dynamic: Link์˜ prefetch ์†์„ฑ์ด ์ง€์ •๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ false๋กœ ์„ค์ •๋œ Page ์ปดํฌ๋„ŒํŠธ์˜ Route Cache ์‹œ๊ฐ„์„ ์ดˆ๋‹จ์œ„๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

  • static: Link์˜ prefetch ์†์„ฑ์ด true๋กœ ์„ค์ •๋˜์—ˆ๊ฑฐ๋‚˜ router.prefetchํ•œ Page ์ปดํฌ๋„ŒํŠธ์˜ Route Cache ์‹œ๊ฐ„์„ ์ดˆ๋‹จ์œ„๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

Optimization

Image Optimization

"next/image"๊ฐ€ ์ œ๊ณตํ•˜๋Š” Image ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ†ตํ•ด ์ด๋ฏธ์ง€๋ฅผ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์‚ฌ์ด์ฆˆ ์ตœ์ ํ™”: Next ์„œ๋ฒ„๋Š” ์ž๋™์œผ๋กœ ์ด๋ฏธ์ง€ ํŒŒ์ผ์˜ ํฌ๊ธฐ๋‚˜ ํฌ๋งท์„ ์ตœ์ ํ™”ํ•˜์—ฌ ์ œ๊ณตํ•ด์ค๋‹ˆ๋‹ค.

  • CLS ๋ฐฉ์ง€: ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋“œ๋  ๋•Œ layout shift ํ˜„์ƒ์„ ๋ฐฉ์ง€์‹œ์ผœ์ค๋‹ˆ๋‹ค.

  • lazy loading: ์ „๋ฐ˜์ ์œผ๋กœ ์ด๋ฏธ์ง€ ๋กœ๋“œ๋Š” lazy load๊ฐ€ ์ ์šฉ๋˜์–ด ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ๊ฐ„์„ ๋‹จ์ถ•์‹œํ‚ต๋‹ˆ๋‹ค. ์ฆ‰, ์ด๋ฏธ์ง€๊ฐ€ viewport์— ํ‘œ์‹œ๋  ๋•Œ๋งŒ ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œํ•˜๊ณ  ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

Remote Images

ํ”„๋กœ์ ํŠธ์˜ public ํด๋” ๋‚ด ํŒŒ์ผ๋“ค์€ ๋นŒ๋“œ๋œ ์ดํ›„ ๋ฃจํŠธ ๊ฒฝ๋กœ์— ์กด์žฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, "public/images/logo.png"๋ผ๋Š” ํŒŒ์ผ์€ ๋นŒ๋“œ๋œ ์ดํ›„์—๋Š” "images/logo.png" ๊ฒฝ๋กœ์— ์กด์žฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฏ€๋กœ ์‹ค์ œ src props์— ์ž‘์„ฑ๋  ๊ฒฝ๋กœ ๋˜ํ•œ "images/logo.png"๋กœ ์ž‘์„ฑํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Remote Images๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ๋ฐ˜๋“œ์‹œ width์™€ height๋ฅผ ๋ช…์‹œํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋นŒ๋“œ ํƒ€์ž„์— ํ•ด๋‹น ์ด๋ฏธ์ง€์˜ width, height ๊ฐ’์„ Next๋Š” ์•Œ์ง€ ๋ชปํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ง์ ‘ ๋ช…์‹œํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


์™ธ๋ถ€ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” width, height ๊ฐ’์„ ๋ช…์‹œํ•ด์ฃผ์–ด์•ผ ํ•˜๋ฉฐ ์ถ”๊ฐ€์ ์œผ๋กœ next.config.jsํŒŒ์ผ์— ์™ธ๋ถ€ ๊ฒฝ๋กœ์˜ ๋„๋ฉ”์ธ์„ ๋ช…์‹œํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

// next.config.js

module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 's3.amazonaws.com',
        port: '',
        pathname: '/my-bucket/**'
      }
    ]
  }
};

Local Images

์ด๋ฏธ์ง€ ํŒŒ์ผ์„ importํ•˜์—ฌ src์— ์ž‘์„ฑํ•œ ๊ฒฝ์šฐ Next๊ฐ€ ๋นŒ๋“œ ํƒ€์ž„์— ์ž๋™์œผ๋กœ width, height ๊ฐ’์„ ๊ณ„์‚ฐํ•˜์—ฌ ์ ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ width, height ๊ฐ’์„ ๋ช…์‹œํ•ด์ค„ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

Props

  • src: ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋‚˜ importํ•œ ํŒŒ์ผ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

  • alt: ๋Œ€์ฒด ํ…์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

  • width: ๋ Œ๋”๋ง๋  ์ด๋ฏธ์ง€์˜ ๊ฐ€๋กœ ์‚ฌ์ด์ฆˆ(px)๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

  • height: ๋ Œ๋”๋ง๋  ์ด๋ฏธ์ง€์˜ ์„ธ๋กœ ์‚ฌ์ด์ฆˆ(px)๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

fill props๊ฐ€ true ํ˜น์€ Local Images๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” width, height๋ฅผ ์ƒ๋žตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • fill: ๋ถ€๋ชจ ์š”์†Œ ํฌ๊ธฐ์— ๋งž๊ฒŒ ์ž๋™์œผ๋กœ ์ด๋ฏธ์ง€ ํฌ๊ธฐ ์กฐ์ • ์—ฌ๋ถ€๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ๋ถ€๋ชจ ์š”์†Œ์—๋Š” position ํ”„๋กœํผํ‹ฐ ๊ฐ’์œผ๋กœ "relative", "absolute", "fixed" ๊ฐ’ ์ค‘ ํ•˜๋‚˜๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

fill props๋กœ true๋ฅผ ์ž‘์„ฑํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ์ด๋ฏธ์ง€์˜ "position" ๊ฐ’์ด ์ž๋™์œผ๋กœ "absolute"๋กœ ์„ค์ •๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ถ€๋ชจ ์š”์†Œ์˜ "position"๊ฐ’์„ "relative", "absolute", "fixed" ๊ฐ’ ์ค‘ ํ•˜๋‚˜ ์ž‘์„ฑํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  • szies: fill props์ด๋‚˜ ๋ฐ˜์‘ํ˜• ์ด๋ฏธ์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์— ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉ๋˜๋ฉฐ Next๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•œ srcset ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋กœ๋“œ๋  ์ด๋ฏธ์ง€ ํฌ๊ธฐ๋ฅผ ๊ฒฐ์ •ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

    • ๊ฐ’์œผ๋กœ๋Š” ๋ฏธ๋””์–ด ์ฟผ๋ฆฌ ๊ฐ’์„ ์ž‘์„ฑํ•˜๋ฉฐ, ๋””ํดํŠธ ๊ฐ’์œผ๋กœ๋Š” "100vw"๊ฐ€ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, ๋””๋ฐ”์ด์Šค์˜ viewport์˜ ๋„ˆ๋น„๊ฐ€ 100vw์ผ ๋•Œ์˜ srcset ์ •๋ณด๋ฅผ ํ™•์ธํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.

srcset์— ๋Œ€ํ•œ ์ •๋ณด๋Š” Next๊ฐ€ ์ž๋™์œผ๋กœ ์„ค์ •ํ•˜๊ฒŒ ๋˜๋ฉฐ, ์ด๋ฅผ ์ˆ˜๋™์œผ๋กœ ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” next.config.js์—์„œ images.deviceSizes, images.imageSizes๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. fill props ๊ฐ’์ด false์ธ ๊ฒฝ์šฐ์—๋Š” images.imageSizes๋ฅผ ํ†ตํ•ด ์ƒ์„ฑํ•˜๊ณ , true์ธ ๊ฒฝ์šฐ์—๋Š” images.deviceSizes๋ฅผ ํ†ตํ•ด ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

  • quality: ์ด๋ฏธ์ง€์˜ ํ€„๋ฆฌํ‹ฐ๋ฅผ 1์—์„œ 100์‚ฌ์ด ๊ฐ’์œผ๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ 75๋กœ ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • priority: ์ด๋ฏธ์ง€ ๋กœ๋“œ preload ์—ฌ๋ถ€๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ false์ด๋ฉฐ, true๋กœ ์„ค์ •ํ•œ ๊ฒฝ์šฐ lazy loading ๋˜ํ•œ ๋น„ํ™œ์„ฑํ™” ๋ฉ๋‹ˆ๋‹ค.

  • placeholder: ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋“œ๋˜๋Š” ๋™์•ˆ ํ‘œ์‹œํ•  placeholder ์„ค์ •์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

    • ๊ธฐ๋ณธ๊ฐ’์€ "empty"์ด๋ฉฐ, ๋ Œ๋”๋ง๋˜๋Š” ๋™์•ˆ ํ•ด๋‹น ์˜์—ญ์„ ๋นˆ ์˜์—ญ์œผ๋กœ ๋น„์›Œ๋‘ก๋‹ˆ๋‹ค.

    • "blur"๋ฅผ ์ž‘์„ฑํ•œ ๊ฒฝ์šฐ์—๋Š” blurDataURL props๋„ ํ•จ๊ป˜ ์ž‘์„ฑํ•ด์ฃผ์–ด์•ผ ํ•˜๋ฉฐ blurDataURL์— base64๋กœ ์ธ์ฝ”๋”ฉ๋œ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด ํ•ด๋‹น ์ด๋ฏธ์ง€๋ฅผ ํ‘œ์‹œํ•ด์ค๋‹ˆ๋‹ค.

  • blurDataURL: placeholder props๊ฐ’์ด "blur"์ธ ๊ฒฝ์šฐ์— ์‚ฌ์šฉ๋˜๋ฉฐ base64๋กœ ์ธ์ฝ”๋”ฉ๋œ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋“œ๋˜๋Š” ๋™์•ˆ ํ‘œ์‹œํ•  ์ด๋ฏธ์ง€๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

MetaData

Next๋Š” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•œ Metadata API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

Static Metadata

layout.tsx ํ˜น์€ page.tsx์—์„œ metadata๋ผ๋Š” ์ด๋ฆ„์˜ ๋ณ€์ˆ˜๋ฅผ exportํ•˜์—ฌ ์ •์  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// page.tsx

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: ',,,',
  description: ',,,'
};

export default function Page() {
  // ,,,
}

Dynamic Metadata

"next"๊ฐ€ ์ œ๊ณตํ•˜๋Š” generateMetadata ๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ exportํ•จ์œผ๋กœ์จ ๋™์  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. generateMetadata ํ•จ์ˆ˜๋Š” async ํ•จ์ˆ˜๋กœ ์ •์˜ํ•ด์•ผํ•˜๋ฉฐ ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌ๋ฐ›๊ณ  ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ๋Š” ์ƒ์œ„ ๊ฒฝ๋กœ์— ๋Œ€ํ•œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌ๋ฐ›์Šต๋‹ˆ๋‹ค.

์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ ์ „๋‹ฌ๋ฐ›๋Š” ๊ฐ์ฒด์—๋Š” params, searchParams ํ”„๋กœํผํ‹ฐ๊ฐ€ ์กด์žฌํ•˜๋ฉฐ ์ด๋Š” ๋™์  ๋ผ์šฐํŒ… ์„ธ๊ทธ๋จผํŠธ์™€ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๊ฐ–๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

// page.tsx
import type { Metadata, ResolvingMetadata } from 'next'

type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>

type Props = {
  params: Params,
  searchParams: SearchParams
};

export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata) {
  return {
    title: ',,,',
    description: ',,,'
  };
}

export default function Page({ params, searchParams }: Props) {
  // ,,,
}

About

Description of Next.js latest version (version 15, WIP) ๐Ÿ“š

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published