๐Ÿ“– TIL - Next.js ๋ผ์šฐํŒ…๊ณผ ๋ Œ๋”๋ง ๋ฐฉ์‹์— ๋Œ€ํ•œ ์ •๋ฆฌ

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

๐Ÿ“– TIL

๋ชฉ๋ก ๋ณด๊ธฐ
74/90

๐Ÿ—‚๏ธ ํด๋” ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ…

Next.js๋Š” app ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด์˜ ํด๋” ๊ตฌ์กฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ URL ๊ฒฝ๋กœ๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค. layout, loading, error ๋“ฑ์˜ ํŠน์ˆ˜ ํŒŒ์ผ์€ ๊ฐ™์€ ๋””๋ ‰ํ† ๋ฆฌ ๋ฐ ํ•˜์œ„ ๋””๋ ‰ํ† ๋ฆฌ์— ์ž๋™์œผ๋กœ ์ ์šฉ๋œ๋‹ค.

๐Ÿ”„ ๋™์  ๋ผ์šฐํŒ…

์ •ํ™•ํ•œ ์„ธ๊ทธ๋จผํŠธ ์ด๋ฆ„์„ ๋ฏธ๋ฆฌ ์•Œ์ง€ ๋ชปํ•˜๊ณ  ๋™์  ๋ฐ์ดํ„ฐ๋กœ ๊ฒฝ๋กœ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋Š” ๋™์  ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ๋™์  ์„ธ๊ทธ๋จผํŠธ๋Š” ๋Œ€๊ด„ํ˜ธ๋กœ ๋ฌถ์–ด ๋งŒ๋“ ๋‹ค. ์˜ˆ: detail/[id]/page.tsx
  • ์ด๋ฅผ ํ†ตํ•ด detail/1/page.tsx, detail/2/page.tsx ๋“ฑ ๋‹ค์–‘ํ•œ ๊ฒฝ๋กœ ์ƒ์„ฑ ๊ฐ€๋Šฅ

๐Ÿ› ๏ธ generateStaticParams

Next.js์—์„œ generateStaticParams ํ•จ์ˆ˜๋Š” ๋™์  ๋ผ์šฐํŠธ ์ค‘์—์„œ ๋นŒ๋“œ ์‹œ์ ์— ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•  ํŠน์ • ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

// app/faq/[category]/page.js
export async function generateStaticParams() {
  // FAQ ์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก
  const categories = [
    { slug: 'account-issues' },
    { slug: 'billing-questions' },
    { slug: 'product-features' },
    { slug: 'troubleshooting' }
  ];
  
  return categories.map((category) => ({
    category: category.slug,
  }));
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋นŒ๋“œ ๊ณผ์ •์—์„œ:
1. Next.js๊ฐ€ generateStaticParams ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰
2. ๋ฐ˜ํ™˜๋œ ๋งค๊ฐœ๋ณ€์ˆ˜ ๋ชฉ๋ก(4๊ฐ€์ง€ ์นดํ…Œ๊ณ ๋ฆฌ)์„ ํ™•์ธ
3. ๊ฐ ๋งค๊ฐœ๋ณ€์ˆ˜์— ๋Œ€ํ•ด ํŽ˜์ด์ง€๋ฅผ ๋ฏธ๋ฆฌ ๋ Œ๋”๋งํ•˜์—ฌ HTML ํŒŒ์ผ ์ƒ์„ฑ
4. ์‚ฌ์šฉ์ž ์š”์ฒญ ์‹œ ์„œ๋ฒ„์—์„œ ๋‹ค์‹œ ๋ Œ๋”๋งํ•  ํ•„์š” ์—†์ด ๋ฐ”๋กœ ์ œ๊ณตํ•˜์—ฌ ๋กœ๋”ฉ ์†๋„ ํ–ฅ์ƒ

๐Ÿ“Œ URL ํŒŒ๋ผ๋ฏธํ„ฐ ์ ‘๊ทผ ๋ฐฉ๋ฒ•

ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๋Š” ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค:

  1. ์ปดํฌ๋„ŒํŠธ์˜ props๋กœ ์ ‘๊ทผ:

    export default function Page({ params }) {
      const { id } = params;
      return <div>ID: {id}</div>;
    }
  2. useParams ํ›… ์‚ฌ์šฉ:

    'use client'
    import { useParams } from 'next/navigation';
    
    export default function Page() {
      const params = useParams();
      return <div>ID: {params.id}</div>;
    }

๊ฐœ์ธ์ ์œผ๋กœ๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋Š” ์ฒซ ๋ฒˆ์งธ ๋ฐฉ๋ฒ•(props)์ด ๋” ํšจ์œจ์ ์ด๋ผ๊ณ  ์ƒ๊ฐํ•œ๋‹ค. ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ณ€ํ™˜ํ•˜์ง€ ์•Š์•„๋„ ๋˜๊ณ , ์ถ”๊ฐ€ import ์—†์ด ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ฝ”๋“œ๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐฉ์‹์œผ๋กœ ํŽ˜์ด์ง€๋ฅผ ์ด๋™ํ•˜๋Š” ๋ฐฉ๋ฒ•:

<Link href="/detail">์ƒ์„ธ ํŽ˜์ด์ง€๋กœ</Link>
  • ์ผ๋ฐ˜ <a> ํƒœ๊ทธ: ์ „์ฒด ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉฐ ์„œ๋ฒ„์—์„œ ์ƒˆ HTML ํŒŒ์ผ์„ ๋ฐ›์•„์˜ด
  • <Link> ์ปดํฌ๋„ŒํŠธ: ํ”„๋ฆฌํŒจ์น˜(๋ฏธ๋ฆฌ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ด) ๋ฐฉ์‹์œผ๋กœ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ „์ฒด ์ƒˆ๋กœ๊ณ ์นจ ์—†์ด ํ™”๋ฉด ์ „ํ™˜

๐Ÿ—ƒ๏ธ ๋ผ์šฐํŠธ ๊ทธ๋ฃน (Route Groups)

๊ด„ํ˜ธ () ๋กœ ๊ฐ์‹ผ ํด๋”๋Š” URL ๊ฒฝ๋กœ์— ํฌํ•จ๋˜์ง€ ์•Š์œผ๋ฉฐ, ํ•˜์œ„ ํด๋”๋งŒ ๊ฒฝ๋กœ์— ํฌํ•จ๋œ๋‹ค.

app/
โ”œโ”€โ”€ (shop)/           # URL์— ํฌํ•จ๋˜์ง€ ์•Š์Œ
โ”‚   โ”œโ”€โ”€ layout.tsx    # shop ๊ทธ๋ฃน ์ „์šฉ ๋ ˆ์ด์•„์›ƒ
โ”‚   โ”œโ”€โ”€ products/     # /products ๊ฒฝ๋กœ
โ”‚   โ””โ”€โ”€ categories/   # /categories ๊ฒฝ๋กœ
โ””โ”€โ”€ layout.tsx        # ๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ

์žฅ์ :

  • ๊ด€์‹ฌ์‚ฌ๋ณ„๋กœ ๋ผ์šฐํŠธ๋ฅผ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ
  • ๊ฐ ๊ทธ๋ฃน์— ๋ณ„๋„์˜ ๋ ˆ์ด์•„์›ƒ ์ ์šฉ ๊ฐ€๋Šฅ
  • ๊ฐ™์€ URL ๋ ˆ๋ฒจ์˜ ํŽ˜์ด์ง€๋“ค์— ์„œ๋กœ ๋‹ค๋ฅธ ๋ ˆ์ด์•„์›ƒ ์ ์šฉ ๊ฐ€๋Šฅ

์˜ˆ๋ฅผ ๋“ค์–ด, /page-a์™€ /page-b ๊ฒฝ๋กœ๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ๋ ˆ์ด์•„์›ƒ์„ ์‚ฌ์šฉํ•˜๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค:

app/
โ”œโ”€โ”€ (with-layout)/
โ”‚   โ”œโ”€โ”€ layout.js    # ์ด ๋ ˆ์ด์•„์›ƒ์€ ์ด ํด๋” ์•ˆ์˜ ํŽ˜์ด์ง€์—๋งŒ ์ ์šฉ
โ”‚   โ””โ”€โ”€ page-a/
โ”‚       โ””โ”€โ”€ page.js  # /page-a ๊ฒฝ๋กœ, ๋ ˆ์ด์•„์›ƒ ์ ์šฉ
โ”œโ”€โ”€ (without-layout)/
โ”‚   โ””โ”€โ”€ page-b/
โ”‚       โ””โ”€โ”€ page.js  # /page-b ๊ฒฝ๋กœ, ๋‹ค๋ฅธ ๋ ˆ์ด์•„์›ƒ ์ ์šฉ
โ””โ”€โ”€ layout.js        # ๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ

๐Ÿ”„ ๋ณ‘๋ ฌ ๋ผ์šฐํŠธ(Parallel Routes)

@ ๊ธฐํ˜ธ๋กœ ์‹œ์ž‘ํ•˜๋Š” ํด๋”๋กœ ํŠน์ • ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ๋ ˆ์ด์•„์›ƒ ์ ์šฉ์„ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค:

app/
โ”œโ”€โ”€ @with-layout/
โ”‚   โ”œโ”€โ”€ layout.js
โ”‚   โ””โ”€โ”€ page-a/
โ”‚       โ””โ”€โ”€ page.js  # /page-a, ๋ ˆ์ด์•„์›ƒ ์ ์šฉ
โ”œโ”€โ”€ @without-layout/
โ”‚   โ””โ”€โ”€ page-b/
โ”‚       โ””โ”€โ”€ page.js  # /page-b, ๋ ˆ์ด์•„์›ƒ ์ ์šฉ ์•ˆ๋จ
โ””โ”€โ”€ layout.js        # ์กฐ๊ฑด๋ถ€๋กœ ๋ ˆ์ด์•„์›ƒ ์ ์šฉ ๋กœ์ง ํฌํ•จ

๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ์—์„œ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง:

// app/layout.js
'use client'
import { usePathname } from 'next/navigation';

export default function RootLayout({ children, withLayout, withoutLayout }) {
  const pathname = usePathname();
  
  if (pathname.startsWith('/page-b')) {
    return <>{withoutLayout}</>;
  }
  
  return (
    <html>
      <body>
        <Header />
        <Sidebar />
        <main>{withLayout || children}</main>
        <Footer />
      </body>
    </html>
  );
}

๐Ÿ”’ ๋น„๊ณต๊ฐœ ํด๋”

์–ธ๋”์Šค์ฝ”์–ด(_)๋กœ ์‹œ์ž‘ํ•˜๋Š” ํด๋”๋Š” URL ๊ฒฝ๋กœ์—์„œ ์™„์ „ํžˆ ์ œ์™ธ๋œ๋‹ค. ์ด๋Š” ๊ณต๊ฐœ์ ์œผ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ๋‚˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐ ์œ ์šฉํ•˜๋‹ค.

app/
โ”œโ”€โ”€ _utils/           # URL ๊ฒฝ๋กœ์—์„œ ์ œ์™ธ๋จ
โ”œโ”€โ”€ _components/      # URL ๊ฒฝ๋กœ์—์„œ ์ œ์™ธ๋จ
โ””โ”€โ”€ products/         # /products ๊ฒฝ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

๐Ÿ”„ ๋ Œ๋”๋ง ๋ฐฉ์‹๊ณผ ์บ์‹ฑ ์ „๋žต

Next.js๋Š” ๋„ค ๊ฐ€์ง€ ์ฃผ์š” ๋ Œ๋”๋ง ๋ฐฉ์‹์„ ์ง€์›ํ•œ๋‹ค:

1. ๐Ÿ—๏ธ SSG (Static Site Generation)

  • ๋นŒ๋“œ ์‹œ์ ์— ํŽ˜์ด์ง€๋ฅผ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•˜์—ฌ Full Route Cache์— ์ €์žฅ
  • ์‚ฌ์šฉ์ž ์š”์ฒญ ์‹œ ๋ฏธ๋ฆฌ ๋ Œ๋”๋ง๋œ HTML์„ ์ฆ‰์‹œ ์ œ๊ณต
  • ๋ฐ์ดํ„ฐ๊ฐ€ ์ž์ฃผ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š” ํŽ˜์ด์ง€์— ์ ํ•ฉ
  • ๊ธฐ๋ณธ ๋ Œ๋”๋ง ๋ฐฉ์‹์œผ๋กœ, ๋ณ„๋„ ์„ค์ • ์—†์ด ์ ์šฉ๋จ

2. ๐Ÿ”„ ISR (Incremental Static Regeneration)

  • ๋นŒ๋“œ ์‹œ ํ”„๋ฆฌ๋ Œ๋”๋ง ํ›„, ์ผ์ • ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ํŽ˜์ด์ง€ ์žฌ์ƒ์„ฑ

  • ์„ค์ • ๋ฐฉ๋ฒ•:

    1) Route Segment Config ์‚ฌ์šฉ:

    export const revalidate = 5; // 5์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ์žฌ๊ฒ€์ฆ

    2) fetch ์˜ต์…˜ ์‚ฌ์šฉ:

    fetch('https://api.example.com/data', { next: { revalidate: 5 } })

3. ๐Ÿ–ฅ๏ธ SSR (Server-Side Rendering)

  • ์‚ฌ์šฉ์ž ์š”์ฒญ๋งˆ๋‹ค ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์‹คํ–‰๋˜์–ด HTML ์ƒ์„ฑ

  • ์„ค์ • ๋ฐฉ๋ฒ•:

    1) Route Segment Config ์‚ฌ์šฉ:

    export const dynamic = 'force-dynamic'

    2) fetch ์˜ต์…˜ ์‚ฌ์šฉ:

    fetch('https://api.example.com/data', { cache: 'no-store' })

    3) ๋‹ค์Œ์˜ ๊ฒฝ์šฐ ์ž๋™์œผ๋กœ SSR ์ ์šฉ:

    • ๋™์  ๋ผ์šฐํŒ… ์‚ฌ์šฉ ์‹œ (/detail/[id]/page.tsx)
    • Dynamic Function ์‚ฌ์šฉ ์‹œ (cookies(), headers() ๋“ฑ)

4. ๐Ÿ’ป CSR (Client-Side Rendering)

  • "use client" ์ง€์‹œ์–ด๊ฐ€ ์žˆ๋Š” ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ˆ˜ํ–‰
  • ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰๋˜๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ๋ Œ๋”๋ง
  • React ํ›…, ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ, window ๊ฐ์ฒด ๋“ฑ์„ ์‚ฌ์šฉํ•  ๋•Œ ํ•„์š”
  • ๋ชจ๋ฒ” ์‚ฌ๋ก€: ํŽ˜์ด์ง€ ์ „์ฒด๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค๊ธฐ๋ณด๋‹ค, ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค

๐Ÿ’ง Hydration

  • ์„œ๋ฒ„์—์„œ ์ƒ์„ฑ๋œ ์ •์  HTML์ด ํด๋ผ์ด์–ธํŠธ์—์„œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ†ตํ•ด ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•˜๊ฒŒ ๋ณ€ํ™˜๋˜๋Š” ๊ณผ์ •
  • ์ž‘๋™ ๋ฐฉ์‹:
    1. ์„œ๋ฒ„์—์„œ ์ƒ์„ฑํ•œ HTML์„ ๋จผ์ € ํ™”๋ฉด์— ๋ณด์—ฌ์คŒ(๋น ๋ฅธ TTV - Time To View)
    2. ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋กœ๋“œ๋˜์–ด ์‹คํ–‰
    3. DOM์— ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์—ฐ๊ฒฐ๋˜๊ณ  ์ƒํƒœ๊ฐ€ ์ดˆ๊ธฐํ™”๋จ(TTI - Time To Interact)
    4. ์ด๋กœ์จ ์›นํŽ˜์ด์ง€๊ฐ€ ์‚ฌ์šฉ์ž์™€ ์ƒํ˜ธ์ž‘์šฉ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๊ฐ€ ๋จ

์ด๋Š” ์ƒ๊ธฐ ์—†๋˜ HTML ํ”„๋ ˆ์ž„์— ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰์„ ํ†ตํ•ด ์ƒ๋™๊ฐ์„ ๋ถˆ์–ด๋„ฃ๋Š” ๊ณผ์ •์œผ๋กœ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

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

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