C
Next.js/Routing/Lesson 09

Dynamic Routing — [slug] · [...slug] · generateStaticParams

35 min·theory
This chapter
1/3
TypeScript

Dynamic Routing — [slug] · [...slug] · generateStaticParams

💡 Why Learn This? — URL Patterns Are Folder Structures

🎯 Dynamic segments are essential for handling URLs like /users/1, /users/2, and /users/3 with a single page file.
💼 The Pages Router pattern of `[slug].tsx` + `getStaticPaths` becomes cleaner in the App Router as `[slug]/page.tsx` + `generateStaticParams`.
Understanding the difference between `[...slug]` (catch-all) and `[[...slug]]` (optional catch-all) lets you handle multi-level paths like `/docs/getting-started/intro` with a single file.
🔗 `params` is always a string (or string[]) — TypeScript tells you exactly. If you forget to convert to a number, the compiler will catch it.
🏢 실무에서는
codemaster40 itself is built with a double dynamic route `app/study/[category]/[slug]/page.tsx`. A single URL like `/study/javascript/promise` is handled by one file, and at build time `generateStaticParams` pre-generates all possible (category, slug) combinations statically — giving you both SEO and performance.

3 Types of Dynamic Segments + generateStaticParams

1. Single Dynamic Segment — [slug]

code
app/users/[id]/page.tsx  →  /users/1, /users/2, ...
tsx
export default async function UserPage({
  params,
}: {
  params: { id: string }; // ★ always a string
}) {
  const userId = Number(params.id); // convert to number yourself
  // ...
}

2. Catch-all — [...slug]

code
app/docs/[...slug]/page.tsx  →  /docs/a, /docs/a/b, /docs/a/b/c
tsx
export default async function DocPage({
  params,
}: {
  params: { slug: string[] }; // ★ always an array
}) {
  // /docs/getting-started/intro → slug = ['getting-started', 'intro']
  const fullPath = params.slug.join('/');
}

Note: catch-all requires at least one segment. /docs alone will not match.

3. Optional Catch-all — [[...slug]]

code
app/docs/[[...slug]]/page.tsx  →  /docs, /docs/a, /docs/a/b
tsx
export default async function DocPage({
  params,
}: {
  params: { slug?: string[] }; // ★ optional — undefined when empty
}) {
  if (!params.slug) return <div>Docs Home</div>;
  // ...
}

4. generateStaticParams — Static Generation at Build Time

tsx
// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api/posts').then(r => r.json());
  return posts.map((p: { slug: string }) => ({ slug: p.slug }));
  // each returned object is built as one page
}

export default async function PostPage({
  params,
}: {
  params: { slug: string };
}) {
  const post = await fetch(`https://api/posts/${params.slug}`).then(r => r.json());
  return <article>{post.content}</article>;
}
  • Fetches the list of possible slugs at build time and generates static HTML.
  • Serves the same purpose as getStaticPaths in the Pages Router.
  • Slugs not returned are SSR-rendered on first request and cached (dynamicParams defaults to true).

5. Nested Dynamic Segments — [category]/[slug]

code
app/study/[category]/[slug]/page.tsx
→ /study/javascript/promise
→ /study/nextjs/app-router-basics
tsx
export async function generateStaticParams() {
  const all: { category: string; slug: string }[] = [];
  for (const cat of CATEGORIES) {
    const lessons = await getLessons(cat);
    lessons.forEach((l) => all.push({ category: cat, slug: l.slug }));
  }
  return all;
}

export default async function LessonPage({
  params,
}: {
  params: { category: string; slug: string };
}) {
  // both fields in params are inferred precisely
}
💻 🅰️ Pages Router — getStaticPaths + getStaticProps
// ❌ Pages Router — [slug].tsx + getStaticPaths + getStaticProps

// 📁 pages/posts/[slug].tsx
import type { GetStaticPaths, GetStaticProps } from 'next';

interface Post { slug: string; title: string; content: string; }

export const getStaticPaths: GetStaticPaths = async () => {
  const posts: Post[] = await fetch('https://api/posts').then(r => r.json());
  return {
    paths: posts.map((p) => ({ params: { slug: p.slug } })),
    fallback: 'blocking', // SSR + cache for non-existent slug requests
  };
};

export const getStaticProps: GetStaticProps<{ post: Post }> = async (ctx) => {
  const slug = ctx.params?.slug as string;
  const post = await fetch(`https://api/posts/${slug}`).then(r => r.json());
  if (!post) return { notFound: true };
  return { props: { post }, revalidate: 60 };
};

export default function PostPage({ post }: { post: Post }) {
  return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}

// Disadvantages:
// - getStaticPaths + getStaticProps = 2 functions + 2 exports
// - Data, render, and static path definitions are all separated
💻 🅱️ App Router — generateStaticParams + async page
// ✅ App Router — generateStaticParams + async Server Component

// 📁 app/posts/[slug]/page.tsx
import { notFound } from 'next/navigation';

interface Post { slug: string; title: string; content: string; }

// 1. Generate static paths at build time
export async function generateStaticParams(): Promise<{ slug: string }[]> {
  const posts: Post[] = await fetch('https://api/posts').then(r => r.json());
  return posts.map((p) => ({ slug: p.slug }));
}

// 2. Page component — async + automatic params inference
export default async function PostPage({
  params,
}: {
  params: { slug: string };
}) {
  const post: Post | null = await fetch(
    `https://api/posts/${params.slug}`,
    { next: { revalidate: 60 } },
  ).then(r => r.json());

  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

// 📁 app/docs/[...slug]/page.tsx — catch-all
export default async function Docs({
  params,
}: {
  params: { slug: string[] }; // Always an array
}) {
  const path = params.slug.join('/');
  const doc = await fetch(`https://api/docs/${path}`).then(r => r.json());
  return <article>{doc.content}</article>;
}

// 📁 app/study/[category]/[slug]/page.tsx — Double dynamic (codemaster40 pattern)
export async function generateStaticParams() {
  const all: { category: string; slug: string }[] = [];
  const categories = ['javascript', 'typescript', 'nextjs', 'react'];
  for (const cat of categories) {
    const lessons = await getLessons(cat);
    lessons.forEach((l) => all.push({ category: cat, slug: l.slug }));
  }
  return all;
}

export default async function LessonPage({
  params,
}: {
  params: { category: string; slug: string }; // Both inferred automatically
}) {
  // ...
}

💡 💡 Dynamic Routing in Practice — 5 Tips

1. params are always strings — convert to number yourself when needed

tsx
const id = Number(params.id);
if (Number.isNaN(id)) notFound();

2. [...slug] is an array; [slug] is a string

tsx
[slug]      → params.slug: string
[...slug]   → params.slug: string[]  (1 or more)
[[...slug]] → params.slug?: string[] (0 or more, optional)

3. generateStaticParams must return an array of objects

tsx
return [{ slug: 'a' }, { slug: 'b' }];
// each object is built as one static page

4. Set dynamicParams = false to allow only pre-generated paths

tsx
export const dynamicParams = false;
// any slug not returned by generateStaticParams will result in a 404

Defaults to true (runtime SSR + caching).

5. Place loading.tsx · error.tsx · not-found.tsx in the same folder
Loading, error, and not-found UIs for dynamic pages are inherited automatically. Covers both SEO and UX.

⚡ Try It Yourself — Route Pattern Matching

Simulate how different dynamic patterns match various URLs.
✏️ JS 코드
📟 Console output
▶ Press the Run button
⚠️ Runs in a browser sandbox — only console.log() is supported; alert/fetch are not.

Check Your Understanding

What is the key difference between `app/docs/[...slug]/page.tsx` and `app/docs/[[...slug]]/page.tsx`?
💡 `[...slug]` (catch-all) **requires at least one segment** to match — a standalone `/docs` request will not match. `[[...slug]]` (optional catch-all) **also matches zero segments (i.e., /docs alone)**. When either matches, `params.slug` is of type `string[]`, but with the optional variant it can be `undefined` on a zero-segment match, making the type `string[] | undefined`.
Dynamic Routing — [slug] · [...slug] · generateStaticParams - Next.js