라우팅 — 파일 시스템 기반 라우팅과 레이아웃
라우팅 — 파일 시스템 기반 라우팅과 레이아웃
React에서 라우팅을 설정하려면 react-router를 설치하고, Route 컴포넌트를 조합하고, 경로를 문자열로 관리해야 합니다. Next.js에서는 폴더를 만드는 것만으로 라우팅이 완성되는데, 이건 어떻게 동작하는 걸까요?
파일 시스템 기반 라우팅은 단순히 편의 기능이 아닙니다. 레이아웃 공유, 로딩 UI, 에러 핸들링 까지 파일 하나로 선언할 수 있어서 라우팅 관련 보일러플레이트를 크게 줄여줍니다.
기본 원리
app/ 디렉토리 안의 폴더 구조가 곧 URL 구조 입니다.
그리고 각 폴더 안에 특수한 파일명을 넣으면 Next.js가 자동으로 인식합니다.
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/hello-world, /blog/nextjs-intro 등
**규칙은 단순합니다 **: page.tsx가 있는 폴더만 라우트가 됩니다.
폴더만 있고 page.tsx가 없으면 URL로 접근할 수 없습니다 — 레이아웃이나 컴포넌트를 정리하는 용도로 쓸 수 있습니다.
특수 파일들
Next.js App Router는 파일명으로 역할을 구분합니다.
| 파일명 | 역할 |
|---|---|
page.tsx | 해당 경로의 페이지 UI |
layout.tsx | 하위 경로에 공유되는 레이아웃 |
loading.tsx | 페이지 로딩 중 표시할 UI (Suspense 경계) |
error.tsx | 에러 발생 시 표시할 UI (Error Boundary) |
not-found.tsx | 404 페이지 |
template.tsx | layout과 비슷하지만 매번 새로 마운트 |
layout.tsx — 레이아웃 공유
// app/blog/layout.tsx
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return (
<div>
<nav>블로그 네비게이션</nav>
<main>{children}</main>
</div>
);
}
이 레이아웃은 /blog와 /blog/[slug] 모두에 적용됩니다.
핵심은 ** 페이지를 이동해도 레이아웃은 리렌더링되지 않는다 **는 것입니다. 상태도 유지됩니다.
loading.tsx — 자동 Suspense
// app/blog/loading.tsx
export default function Loading() {
return <div>로딩 중...</div>;
}
이 파일을 넣어두면 Next.js가 자동으로 page.tsx를 <Suspense>로 감싸고, 로딩 중에 이 컴포넌트를 표시합니다.
직접 Suspense를 작성할 필요가 없습니다.
error.tsx — 자동 Error Boundary
'use client'; // Error Boundary는 반드시 Client Component
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<p>문제가 발생했습니다: {error.message}</p>
<button onClick={() => reset()}>다시 시도</button>
</div>
);
}
에러가 발생하면 전체 앱이 깨지는 대신, ** 해당 라우트 세그먼트만** 에러 UI로 대체됩니다.
reset() 함수를 호출하면 해당 세그먼트를 다시 렌더링합니다.
동적 라우트
URL의 일부를 변수로 받으려면 ** 대괄호([])로 폴더명을 감싸면** 됩니다.
app/blog/[slug]/page.tsx → /blog/hello-world
app/user/[id]/page.tsx → /user/123
// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}
Next.js 15부터 params는 Promise 입니다. await으로 풀어야 합니다.
Catch-all 라우트
app/docs/[...slug]/page.tsx → /docs/a, /docs/a/b, /docs/a/b/c
[...slug]는 이후의 모든 세그먼트를 배열로 받습니다.
/docs/a/b/c라면 slug는 ['a', 'b', 'c']가 됩니다.
라우트 그룹 — URL에 영향 없이 폴더 정리
폴더명을 소괄호(())로 감싸면 URL에 포함되지 않습니다.
app/
├── (marketing)/
│ ├── layout.tsx → 마케팅 전용 레이아웃
│ ├── about/
│ │ └── page.tsx → /about
│ └── pricing/
│ └── page.tsx → /pricing
├── (dashboard)/
│ ├── layout.tsx → 대시보드 전용 레이아웃
│ └── settings/
│ └── page.tsx → /settings
URL에는 (marketing)이 나타나지 않습니다. /about으로 접근됩니다.
** 같은 레벨의 라우트에 서로 다른 레이아웃을 적용 **하고 싶을 때 유용합니다.
주의할 점
layout.tsx와 template.tsx의 차이를 모르면 상태 버그가 생긴다
layout.tsx는 경로가 바뀌어도 ** 리마운트되지 않습니다 **. 내부 상태가 유지됩니다.
template.tsx는 경로가 바뀔 때마다 ** 새로 마운트 **됩니다.
대부분의 경우 layout.tsx가 맞습니다. 하지만 페이지 전환 애니메이션이나, 경로마다 초기화가 필요한 상태가 있다면 template.tsx를 써야 합니다.
동적 라우트와 정적 라우트가 충돌하면
app/blog/new/page.tsx → /blog/new (정적)
app/blog/[slug]/page.tsx → /blog/[slug] (동적)
/blog/new로 접근하면 ** 정적 라우트가 우선 **합니다. Next.js는 구체적인 경로를 먼저 매칭합니다.
하지만 이런 구조가 많아지면 혼란스러워지므로, 가능하면 명확하게 분리하는 것이 좋습니다.
정리
| 핵심 | 내용 |
|---|---|
| 라우팅 방식 | 폴더 구조 = URL 구조 |
| 페이지 생성 | page.tsx가 있으면 접근 가능한 경로 |
| 레이아웃 | layout.tsx로 공유, 리렌더링 없음 |
| 로딩/에러 | loading.tsx, error.tsx로 자동 Suspense/Error Boundary |
| 동적 라우트 | [param], [...catchAll] |
| 라우트 그룹 | (group) — URL에 미반영, 레이아웃 분리용 |
기억할 한 줄: "파일을 만들면 라우트가 되고, 파일명이 역할을 결정한다."