Next.js 13부터 도입된 App Router는 app/ 디렉토리 기반의 라우팅 시스템입니다. 이전의 Pages Router(pages/ 디렉토리)를 대체하며, React Server Components, 중첩 레이아웃, Streaming 등 최신 기능을 지원합니다.
/about에 접속하면 소개 페이지를, /blog에 접속하면 블로그 목록을 보여주는 규칙이 바로 라우팅입니다.
| 비교 | Pages Router | App Router |
|---|---|---|
| 디렉토리 | pages/ | app/ |
| 기본 컴포넌트 | 클라이언트 컴포넌트 | 서버 컴포넌트 |
| 레이아웃 | 수동 구성 | 자동 중첩 |
| 데이터 페칭 | getServerSideProps 등 | async 컴포넌트에서 직접 |
새 프로젝트라면 App Router를 사용하세요. Pages Router는 레거시 호환용으로만 유지됩니다.
App Router에서는 특정 이름을 가진 파일들이 특별한 역할을 합니다:
page.tsx - 페이지 UI해당 경로에서 보여줄 UI를 정의합니다. 이 파일이 있어야 해당 경로에 접근할 수 있습니다.
// app/about/page.tsx → /about 경로
export default function About() {
return <h1>소개 페이지</h1>;
}
page.tsx의 default export를 해당 경로의 UI로 사용합니다. named export(export function About)로 작성하면 페이지를 인식하지 못합니다. layout.tsx, loading.tsx 등 다른 특수 파일도 동일한 규칙입니다.
layout.tsx - 공유 레이아웃하위 페이지들이 공유하는 UI를 정의합니다. 내비게이션, 사이드바 등에 사용합니다.
// app/layout.tsx → 모든 페이지가 공유
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>{children}</body>
</html>
);
}
loading.tsx - 로딩 UIReact Suspense를 기반으로 자동 로딩 UI를 제공합니다.
// app/dashboard/loading.tsx
export default function Loading() {
return <div>대시보드 로딩 중...</div>;
}
error.tsx - 에러 UIReact Error Boundary를 기반으로 에러 발생 시 보여줄 UI를 정의합니다. Error Boundary는 반드시 클라이언트 컴포넌트여야 합니다.
// app/dashboard/error.tsx
'use client' // Error Boundary는 반드시 클라이언트 컴포넌트여야 합니다
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// 에러 리포팅 서비스에 에러를 기록합니다
console.error(error)
}, [error])
return (
<div>
<h2>문제가 발생했습니다</h2>
<button onClick={() => reset()}>다시 시도</button>
</div>
)
}
not-found.tsx - 404 페이지
// app/not-found.tsx
export default function NotFound() {
return <h1>페이지를 찾을 수 없습니다</h1>;
}
App Router에서 폴더 이름이 곧 URL 세그먼트가 됩니다:
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ └── page.tsx → /blog
└── contact/
└── page.tsx → /contact
새 페이지를 추가하려면 폴더를 만들고 page.tsx를 넣기만 하면 됩니다:
// app/about/page.tsx
export default function About() {
return (
<main>
<h1>About Us</h1>
<p>우리 팀을 소개합니다.</p>
</main>
);
}
// app/blog/page.tsx
export default function Blog() {
return (
<main>
<h1>블로그</h1>
<p>최신 글 목록</p>
</main>
);
}
URL의 일부를 변수로 사용해야 할 때 대괄호로 폴더명을 감쌉니다:
[slug] - 단일 동적 세그먼트
app/blog/[slug]/page.tsx → /blog/hello-world, /blog/my-first-post 등
// app/blog/[slug]/page.tsx
type Props = {
params: Promise<{ slug: string }>;
};
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
return <h1>블로그 글: {slug}</h1>;
}
params 객체의 키가 됩니다. [slug]이면 params.slug, [id]이면 params.id로 접근합니다. 프로젝트 맥락에 맞는 이름을 자유롭게 사용하세요 - 예를 들어, 상품 상세 페이지라면 [productId]가 더 직관적입니다.
다음 세션에서는 이 라우팅 시스템 위에 구축되는 레이아웃 시스템과 중첩 라우팅을 학습합니다.