포스트

[이제와서 시작하는 Next.js 마스터하기 #2] App Router와 라우팅 시스템 완전 정복

[이제와서 시작하는 Next.js 마스터하기 #2] App Router와 라우팅 시스템 완전 정복

“URL이 자동으로 만들어진다고?” - 네! Next.js의 파일 기반 라우팅은 폴더 구조가 곧 웹사이트 주소입니다. 복잡한 설정 없이 파일만 만들면 끝!

🎯 이 글에서 배울 내용

  • 파일 기반 라우팅의 마법 같은 편리함
  • 동적 라우팅 ([id], [slug], [...all])
  • 레이아웃으로 공통 UI 재사용하기
  • Loading UI로 부드러운 사용자 경험 만들기
  • Error Boundary로 에러 우아하게 처리하기
  • Route Groups로 프로젝트 구조 깔끔하게 정리하기

예상 소요 시간: 40분 사전 지식: Next.js #1 - 처음 시작하는 Next.js


🗺️ 라우팅이란? (쉽게 설명)

라우팅은 “주소(URL)에 따라 다른 페이지를 보여주는 것”입니다.

🏢 빌딩으로 이해하는 라우팅

웹사이트를 큰 빌딩이라고 생각해보세요:

1
2
3
4
yoursite.com/          →  1층 로비 (홈페이지)
yoursite.com/about     →  2층 회사 소개
yoursite.com/blog      →  3층 블로그
yoursite.com/contact   →  4층 연락처

전통적인 방식 (React Router 등):

  • 엘리베이터 버튼(라우터)을 일일이 프로그래밍해야 함
  • 각 층(페이지)과 버튼을 연결하는 코드 작성

Next.js 방식:

  • 각 층에 방(폴더)을 만들면 엘리베이터가 자동으로 버튼 생성!
  • 설정 파일? 필요 없음!

📁 파일 기반 라우팅 기초

1. 기본 페이지 만들기

1
2
3
4
5
6
7
8
app/
├── page.js              →  yoursite.com/
├── about/
│   └── page.js          →  yoursite.com/about
├── blog/
│   └── page.js          →  yoursite.com/blog
└── contact/
    └── page.js          →  yoursite.com/contact

핵심 규칙:

  • 📁 폴더 이름 = URL 경로
  • 📄 page.js = 해당 URL에서 보여줄 페이지

2. 중첩 라우팅 (Nested Routes)

1
2
3
4
5
6
7
8
9
app/
└── blog/
    ├── page.js                  →  /blog
    ├── authors/
    │   └── page.js              →  /blog/authors
    └── categories/
        ├── page.js              →  /blog/categories
        └── tech/
            └── page.js          →  /blog/categories/tech

폴더를 중첩하면 URL도 중첩됩니다!

🎯 실습: 블로그 구조 만들기

1단계: 폴더 생성

1
mkdir -p app/blog/posts

2단계: 페이지 파일 작성

1
2
3
4
5
6
7
8
9
// app/blog/page.js
export default function BlogPage() {
  return (
    <div className="p-8">
      <h1 className="text-4xl font-bold mb-4">블로그</h1>
      <p className="text-gray-600">블로그 글 목록이 여기 표시됩니다.</p>
    </div>
  );
}
1
2
3
4
5
6
7
8
9
// app/blog/posts/page.js
export default function PostsPage() {
  return (
    <div className="p-8">
      <h1 className="text-4xl font-bold mb-4">모든 포스트</h1>
      <p className="text-gray-600">전체 포스트 목록</p>
    </div>
  );
}

3단계: 브라우저에서 확인

  • http://localhost:3000/blog → 블로그 메인
  • http://localhost:3000/blog/posts → 포스트 목록

🎉 설정 파일 없이 2개의 페이지가 자동으로 생성되었습니다!


🎭 동적 라우팅 (Dynamic Routes)

🤔 왜 필요한가요?

블로그 글이 100개라면 100개의 폴더를 만들 건가요? 당연히 아니죠!

문제 상황:

1
2
3
4
app/blog/post-1/page.js  →  /blog/post-1
app/blog/post-2/page.js  →  /blog/post-2
app/blog/post-3/page.js  →  /blog/post-3
... (100개?! 😱)

해결책: 동적 라우팅

1
app/blog/[id]/page.js  →  /blog/아무거나

1. 기본 동적 라우팅: [id]

대괄호 []가 핵심입니다!

1
2
3
4
5
6
7
8
9
10
11
12
// app/blog/[id]/page.js
export default async function PostPage({ params }) {
  // Next.js 16부터 params는 Promise입니다!
  const { id } = await params;

  return (
    <div className="p-8">
      <h1 className="text-4xl font-bold mb-4">블로그 포스트</h1>
      <p className="text-xl">포스트 ID: {id}</p>
    </div>
  );
}

동작 원리:

1
2
3
4
/blog/1      →  id = "1"
/blog/2      →  id = "2"
/blog/hello  →  id = "hello"
/blog/안녕   →  id = "안녕"

2. 실전 예제: 블로그 포스트 상세 페이지

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// app/blog/[slug]/page.js
const posts = {
  'nextjs-intro': {
    title: 'Next.js 시작하기',
    content: 'Next.js는 React 기반의...',
    author: '홍길동',
    date: '2025-11-23'
  },
  'react-basics': {
    title: 'React 기초',
    content: 'React는 사용자 인터페이스를...',
    author: '김철수',
    date: '2025-11-22'
  }
};

export default async function PostDetailPage({ params }) {
  const { slug } = await params;
  const post = posts[slug];

  // 포스트가 없으면?
  if (!post) {
    return (
      <div className="p-8">
        <h1 className="text-2xl font-bold text-red-600">
          포스트를 찾을 수 없습니다 😢
        </h1>
      </div>
    );
  }

  return (
    <article className="max-w-2xl mx-auto p-8">
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>

      <div className="flex gap-4 text-gray-600 mb-8">
        <span>작성자: {post.author}</span>
        <span>날짜: {post.date}</span>
      </div>

      <div className="prose">
        {post.content}
      </div>
    </article>
  );
}

테스트:

  • /blog/nextjs-intro → Next.js 시작하기 글
  • /blog/react-basics → React 기초 글
  • /blog/unknown → 포스트를 찾을 수 없습니다

3. Catch-all Routes: [...slug]

여러 단계의 경로를 한 번에 처리

1
2
3
4
5
6
7
8
9
10
11
12
// app/docs/[...slug]/page.js
export default async function DocsPage({ params }) {
  const { slug } = await params;
  // slug는 배열입니다!

  return (
    <div className="p-8">
      <h1 className="text-3xl font-bold mb-4">문서 페이지</h1>
      <p>경로: {slug.join('')}</p>
    </div>
  );
}

동작 예시:

1
2
3
4
5
6
7
8
/docs/getting-started
→ slug = ["getting-started"]

/docs/api/authentication
→ slug = ["api", "authentication"]

/docs/guides/deployment/vercel
→ slug = ["guides", "deployment", "vercel"]

4. Optional Catch-all: [[...slug]]

이중 대괄호 [[]]는 선택적입니다

1
2
3
4
5
6
7
8
9
10
11
12
13
// app/shop/[[...categories]]/page.js
export default async function ShopPage({ params }) {
  const { categories } = await params;

  if (!categories) {
    // /shop → 전체 상품
    return <div>전체 상품 목록</div>;
  }

  // /shop/electronics → 전자제품
  // /shop/electronics/phones → 휴대폰
  return <div>카테고리: {categories.join(' > ')}</div>;
}

🖼️ 레이아웃 (Layouts) - 공통 UI 재사용

🤔 레이아웃이란?

여러 페이지에서 공통으로 사용하는 “틀”입니다.

비유: 액자로 이해하기

  • 레이아웃 = 액자 (테두리, 고정된 부분)
  • 페이지 콘텐츠 = 그림 (바뀌는 부분)

1. 루트 레이아웃 (Root Layout)

모든 페이지에 적용되는 최상위 레이아웃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// app/layout.js
export default function RootLayout({ children }) {
  return (
    <html lang="ko">
      <body>
        {/* 모든 페이지에 표시되는 헤더 */}
        <header className="bg-blue-600 text-white p-4">
          <nav className="max-w-6xl mx-auto flex gap-4">
            <a href="/"></a>
            <a href="/blog">블로그</a>
            <a href="/about">소개</a>
          </nav>
        </header>

        {/* 각 페이지의 내용이 여기 들어감 */}
        <main className="min-h-screen">
          {children}
        </main>

        {/* 모든 페이지에 표시되는 푸터 */}
        <footer className="bg-gray-800 text-white p-8 text-center">
          © 2025 My Website
        </footer>
      </body>
    </html>
  );
}

{children}이 핵심입니다!

  • 홈 페이지를 보면 → children에 홈 페이지 내용
  • 블로그를 보면 → children에 블로그 내용

2. 중첩 레이아웃 (Nested Layouts)

특정 섹션에만 적용되는 레이아웃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// app/blog/layout.js
export default function BlogLayout({ children }) {
  return (
    <div className="max-w-6xl mx-auto p-8">
      {/* 블로그 전용 사이드바 */}
      <div className="grid grid-cols-12 gap-8">
        <aside className="col-span-3">
          <h2 className="font-bold mb-4">카테고리</h2>
          <ul className="space-y-2">
            <li><a href="/blog/tech">기술</a></li>
            <li><a href="/blog/life">일상</a></li>
            <li><a href="/blog/review">리뷰</a></li>
          </ul>
        </aside>

        {/* 블로그 페이지 내용 */}
        <main className="col-span-9">
          {children}
        </main>
      </div>
    </div>
  );
}

레이아웃 중첩 구조:

1
2
3
4
5
RootLayout (모든 페이지)
  ├─ Header, Footer
  └─ BlogLayout (블로그 섹션만)
      ├─ 사이드바
      └─ 페이지 내용

3. 레이아웃 vs 템플릿

특징 Layout Template
파일명 layout.js template.js
페이지 이동 시 유지됨 (리렌더 안 됨) 새로 렌더링됨
상태(state) 유지됨 초기화됨
사용 케이스 공통 UI (헤더, 사이드바) 페이지마다 초기화 필요한 경우

Template 예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app/blog/template.js
'use client';

import { useEffect } from 'react';

export default function BlogTemplate({ children }) {
  useEffect(() => {
    // 페이지 이동할 때마다 실행
    console.log('새 페이지 로드!');
  }, []);

  return (
    <div className="animate-fadeIn">
      {children}
    </div>
  );
}

⏳ Loading UI - 로딩 상태 처리하기

🤔 왜 필요한가요?

데이터를 가져오는 동안 사용자에게 “로딩 중”을 보여줘야 합니다.

Next.js 이전:

1
2
3
4
5
6
7
8
9
10
11
12
// 복잡한 로딩 상태 관리
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);

useEffect(() => {
  fetchData().then(data => {
    setData(data);
    setLoading(false);
  });
}, []);

if (loading) return <Spinner />;

Next.js 방식:

1
// loading.js 파일만 만들면 끝!

1. Loading UI 만들기

1
2
3
4
5
6
7
8
9
10
11
12
// app/blog/loading.js
export default function LoadingBlog() {
  return (
    <div className="p-8">
      <div className="animate-pulse">
        <div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
        <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
        <div className="h-4 bg-gray-200 rounded w-2/3"></div>
      </div>
    </div>
  );
}

자동으로 동작합니다!

  • 페이지 로딩 시작 → loading.js 표시
  • 페이지 로딩 완료 → 실제 페이지 표시

2. Suspense와 Streaming

부분적인 로딩도 가능합니다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// app/dashboard/page.js
import { Suspense } from 'react';

async function SlowComponent() {
  // 느린 데이터 로딩 (3초)
  await new Promise(resolve => setTimeout(resolve, 3000));
  return <div>느린 컴포넌트 로드 완료!</div>;
}

async function FastComponent() {
  // 빠른 데이터 로딩 (0.5초)
  await new Promise(resolve => setTimeout(resolve, 500));
  return <div>빠른 컴포넌트 로드 완료!</div>;
}

export default function DashboardPage() {
  return (
    <div className="p-8">
      <h1 className="text-3xl font-bold mb-8">대시보드</h1>

      {/* 빠른 컴포넌트는 먼저 표시 */}
      <Suspense fallback={<div>빠른 데이터 로딩 중...</div>}>
        <FastComponent />
      </Suspense>

      {/* 느린 컴포넌트는 나중에 표시 */}
      <Suspense fallback={<div>느린 데이터 로딩 중...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}

결과:

  1. 페이지 즉시 표시
  2. 0.5초 후 → 빠른 컴포넌트 나타남
  3. 3초 후 → 느린 컴포넌트 나타남

❌ Error Handling - 에러 우아하게 처리하기

🤔 에러가 발생하면?

Next.js 이전: 앱 전체가 멈춤 (흰 화면 😱)

Next.js 방식: 에러 부분만 처리 (나머지는 정상 작동 😊)

1. Error Boundary 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// app/blog/error.js
'use client'; // Error 컴포넌트는 Client Component여야 함!

import { useEffect } from 'react';

export default function BlogError({ error, reset }) {
  useEffect(() => {
    // 에러 로깅 (Sentry 등)
    console.error('블로그 에러:', error);
  }, [error]);

  return (
    <div className="p-8 text-center">
      <h2 className="text-2xl font-bold text-red-600 mb-4">
        앗! 문제가 발생했습니다 😢
      </h2>

      <p className="text-gray-600 mb-4">
        {error.message}
      </p>

      <button
        onClick={reset}
        className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
      >
        다시 시도
      </button>
    </div>
  );
}

2. 에러 테스트하기

1
2
3
4
5
6
7
// app/blog/test-error/page.js
export default function ErrorTestPage() {
  // 의도적으로 에러 발생
  throw new Error('테스트 에러입니다!');

  return <div>이 부분은 실행되지 않습니다</div>;
}

/blog/test-error 접속 시:

  • ✅ 에러 메시지 표시
  • ✅ “다시 시도” 버튼 제공
  • ✅ 나머지 사이트는 정상 작동

3. Global Error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// app/global-error.js
'use client';

export default function GlobalError({ error, reset }) {
  return (
    <html>
      <body>
        <div className="min-h-screen flex items-center justify-center">
          <div className="text-center">
            <h2 className="text-3xl font-bold mb-4">
              전체 에러 발생
            </h2>
            <button onClick={reset}>다시 시작</button>
          </div>
        </div>
      </body>
    </html>
  );
}

🚫 Not Found 페이지

1. 자동 404 페이지

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// app/not-found.js
export default function NotFound() {
  return (
    <div className="min-h-screen flex items-center justify-center">
      <div className="text-center">
        <h1 className="text-6xl font-bold text-gray-800 mb-4">404</h1>
        <p className="text-xl text-gray-600 mb-8">
          페이지를 찾을 수 없습니다 😢
        </p>
        <a
          href="/"
          className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
        >
          홈으로 돌아가기
        </a>
      </div>
    </div>
  );
}

2. 프로그래밍 방식으로 404 표시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/blog/[slug]/page.js
import { notFound } from 'next/navigation';

export default async function PostPage({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);

  // 포스트가 없으면 404 페이지 표시
  if (!post) {
    notFound();
  }

  return <div>{post.title}</div>;
}

📂 Route Groups - 구조 정리하기

🤔 왜 필요한가요?

문제: 폴더 이름이 URL에 나타납니다

1
app/admin/dashboard/page.js  →  /admin/dashboard

하지만 때로는:

  • 폴더로 구조는 정리하고 싶지만
  • URL에는 나타나지 않았으면 좋겠을 때!

Route Groups: (folder)

괄호 ()로 감싸면 URL에 나타나지 않습니다!

1
2
3
4
5
6
7
8
9
10
11
12
app/
├── (marketing)/           # URL에 안 나타남!
│   ├── about/
│   │   └── page.js       →  /about (not /marketing/about)
│   └── contact/
│       └── page.js       →  /contact
│
└── (shop)/                # URL에 안 나타남!
    ├── products/
    │   └── page.js       →  /products
    └── cart/
        └── page.js       →  /cart

실전 예제: 여러 레이아웃 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
app/
├── (main)/
│   ├── layout.js         # 메인 사이트 레이아웃
│   ├── page.js           →  /
│   └── about/
│       └── page.js       →  /about
│
└── (auth)/
    ├── layout.js         # 로그인 페이지 전용 레이아웃
    ├── login/
    │   └── page.js       →  /login
    └── signup/
        └── page.js       →  /signup
1
2
3
4
5
6
7
8
9
10
// app/(main)/layout.js
export default function MainLayout({ children }) {
  return (
    <div>
      <header>메인 헤더</header>
      {children}
      <footer>메인 푸터</footer>
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
// app/(auth)/layout.js
export default function AuthLayout({ children }) {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100">
      <div className="max-w-md w-full bg-white p-8 rounded-lg shadow">
        {children}
      </div>
    </div>
  );
}

결과:

  • /, /about → 헤더 + 푸터 있음
  • /login, /signup → 중앙 카드 레이아웃

🎯 실전 프로젝트: 블로그 라우팅 구조

완성된 폴더 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
app/
├── layout.js                      # 루트 레이아웃
├── page.js                        # 홈
├── not-found.js                   # 404
│
├── (marketing)/                   # 마케팅 페이지
│   ├── about/
│   │   └── page.js               →  /about
│   └── contact/
│       └── page.js               →  /contact
│
└── blog/
    ├── layout.js                  # 블로그 레이아웃
    ├── loading.js                 # 블로그 로딩
    ├── error.js                   # 블로그 에러
    ├── page.js                    →  /blog
    ├── [slug]/
    │   ├── page.js                →  /blog/[slug]
    │   └── loading.js             # 포스트 로딩
    └── category/
        └── [...slug]/
            └── page.js            →  /blog/category/[...slug]

단계별 구현

1단계: 루트 레이아웃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// app/layout.js
import Link from 'next/link';

export default function RootLayout({ children }) {
  return (
    <html lang="ko">
      <body>
        <header className="bg-white border-b">
          <nav className="max-w-6xl mx-auto p-4 flex gap-6">
            <Link href="/" className="font-bold"></Link>
            <Link href="/blog">블로그</Link>
            <Link href="/about">소개</Link>
            <Link href="/contact">연락처</Link>
          </nav>
        </header>

        <main className="min-h-screen">
          {children}
        </main>

        <footer className="bg-gray-50 border-t p-8 text-center text-gray-600">
          © 2025 My Blog
        </footer>
      </body>
    </html>
  );
}

2단계: 블로그 레이아웃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// app/blog/layout.js
export default function BlogLayout({ children }) {
  return (
    <div className="max-w-6xl mx-auto p-8">
      <div className="grid grid-cols-12 gap-8">
        <aside className="col-span-3">
          <h2 className="font-bold text-lg mb-4">카테고리</h2>
          <ul className="space-y-2">
            <li>
              <a href="/blog/category/tech" className="text-blue-600 hover:underline">
                기술
              </a>
            </li>
            <li>
              <a href="/blog/category/life" className="text-blue-600 hover:underline">
                일상
              </a>
            </li>
          </ul>
        </aside>

        <div className="col-span-9">
          {children}
        </div>
      </div>
    </div>
  );
}

3단계: 블로그 목록 페이지

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// app/blog/page.js
import Link from 'next/link';

const posts = [
  { slug: 'nextjs-intro', title: 'Next.js 시작하기', excerpt: 'Next.js를 배워봅시다' },
  { slug: 'react-hooks', title: 'React Hooks', excerpt: 'Hooks 완벽 가이드' },
];

export default function BlogPage() {
  return (
    <div>
      <h1 className="text-4xl font-bold mb-8">블로그</h1>

      <div className="space-y-6">
        {posts.map(post => (
          <article key={post.slug} className="border-b pb-6">
            <Link href={`/blog/${post.slug}`}>
              <h2 className="text-2xl font-semibold mb-2 hover:text-blue-600">
                {post.title}
              </h2>
            </Link>
            <p className="text-gray-600">{post.excerpt}</p>
          </article>
        ))}
      </div>
    </div>
  );
}

4단계: 포스트 상세 페이지

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// app/blog/[slug]/page.js
import { notFound } from 'next/navigation';

const posts = {
  'nextjs-intro': {
    title: 'Next.js 시작하기',
    content: 'Next.js는 React 기반의 강력한 프레임워크입니다...',
    date: '2025-11-23'
  },
  'react-hooks': {
    title: 'React Hooks 완벽 가이드',
    content: 'React Hooks는 함수형 컴포넌트에서...',
    date: '2025-11-22'
  }
};

export default async function PostPage({ params }) {
  const { slug } = await params;
  const post = posts[slug];

  if (!post) {
    notFound();
  }

  return (
    <article className="prose max-w-none">
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
      <p className="text-gray-600 mb-8">{post.date}</p>
      <div className="leading-relaxed">
        {post.content}
      </div>
    </article>
  );
}

🔍 자주 묻는 질문 (FAQ)

Q1: params를 await 해야 하는 이유는?

: Next.js 16부터 params가 비동기(Promise)가 되었습니다!

이유: 성능 최적화를 위해 params를 나중에 읽을 수 있게 변경

사용법:

1
2
3
4
5
6
7
8
9
10
// ✅ Next.js 16 (올바름)
export default async function Page({ params }) {
  const { id } = await params;
  return <div>{id}</div>;
}

// ❌ Next.js 15 방식 (16에서 오류)
export default function Page({ params }) {
  return <div>{params.id}</div>;
}
Q2: layout.js와 page.js의 차이는?

:

  • layout.js: 여러 페이지에서 공유하는 “틀” (헤더, 사이드바 등)
  • page.js: 실제 페이지 내용

비유:

  • layout.js = 액자
  • page.js = 그림

중요: layout.js는 페이지 이동 시 리렌더링되지 않습니다! (성능 향상)

Q3: [slug]와 [...slug]의 차이는?

:

[slug] (단일 세그먼트):

1
2
/blog/hello       ✅  slug = "hello"
/blog/hello/world ❌  매치 안 됨

[...slug] (여러 세그먼트):

1
2
3
/blog/hello       ✅  slug = ["hello"]
/blog/hello/world ✅  slug = ["hello", "world"]
/blog/a/b/c       ✅  slug = ["a", "b", "c"]

[[...slug]] (선택적):

1
2
3
/blog             ✅  slug = undefined
/blog/hello       ✅  slug = ["hello"]
/blog/hello/world ✅  slug = ["hello", "world"]
Q4: loading.js가 표시되지 않아요

: 데이터 로딩이 너무 빠르면 loading.js가 보이지 않을 수 있습니다!

해결책:

1
2
3
4
5
6
// 개발 중 테스트를 위해 인위적인 지연 추가
export default async function Page() {
  await new Promise(resolve => setTimeout(resolve, 2000)); // 2초 대기

  return <div>페이지 내용</div>;
}

프로덕션에서는: 실제 API 호출이 있으면 자연스럽게 표시됩니다.

Q5: Route Groups는 언제 사용하나요?

: 이런 경우에 유용합니다!

1. 여러 레이아웃 적용

1
2
3
(main) → 일반 페이지 레이아웃
(auth) → 로그인 페이지 레이아웃
(admin) → 관리자 페이지 레이아웃

2. 코드 구조 정리

1
2
3
(marketing) → 마케팅 관련 페이지
(shop) → 쇼핑 관련 페이지
(blog) → 블로그 관련 페이지

3. 팀별 작업 분리

1
2
(team-frontend) → 프론트엔드 팀
(team-backend) → 백엔드 팀

핵심: URL에는 영향 없이 폴더 구조만 정리!


💡 초보자를 위한 팁

1. 폴더 구조는 미리 계획하세요

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
✅ 좋은 구조 (명확함):
app/
├── (marketing)/
│   ├── about/
│   └── contact/
└── blog/
    ├── [slug]/
    └── category/

❌ 나쁜 구조 (혼란스러움):
app/
├── page1/
├── page2/
├── temp/
└── test/

2. loading.js는 작은 단위로

1
2
3
4
5
6
// ✅ 좋음: 각 세그먼트마다 loading
app/blog/loading.js
app/blog/[slug]/loading.js

// ⚠️ 피하기: 루트에만 loading
app/loading.js

3. error.js는 적절한 위치에

1
2
3
4
5
6
7
8
// 전체 앱 에러
app/error.js

// 블로그 섹션 에러
app/blog/error.js

// 특정 페이지 에러
app/blog/[slug]/error.js

4. 개발자 도구 활용

Next.js는 유용한 에러 메시지를 제공합니다!

1
2
3
Error: Page "/blog/[slug]/page.js" is missing generateStaticParams.

→ 이 메시지를 읽고 문서를 찾아보세요!

🎯 오늘 배운 내용 정리

✅ 핵심 개념

  1. 파일 기반 라우팅
    • 폴더 = URL 경로
    • page.js = 페이지 표시
  2. 동적 라우팅
    • [id] = 단일 세그먼트
    • [...slug] = 여러 세그먼트
    • [[...slug]] = 선택적
  3. 레이아웃
    • 공통 UI 재사용
    • 중첩 가능
    • 페이지 이동 시 유지
  4. 특수 파일
    • loading.js = 로딩 UI
    • error.js = 에러 처리
    • not-found.js = 404 페이지
  5. Route Groups
    • (folder) = URL에 안 나타남
    • 구조 정리용

🚀 다음 단계

다음 포스트에서는:

  • Server Components 깊이 이해하기
  • 데이터 페칭 전략
  • 캐싱 최적화

를 배워보겠습니다!


📚 시리즈 네비게이션

이전 글

다음 글


🔗 참고 자료


“복잡해 보이지만 하나씩 따라하면 금방 익숙해집니다!” - 라우팅은 Next.js의 가장 강력한 기능 중 하나입니다! 🎯

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.