Next.js 13에 도입된 앱 라우터는 기존 페이지 라우터와 비교하여 라우팅 및 렌더링에 근본적으로 다른 접근 방식을 취합니다. 이제 안정적이고 널리 채택되었으므로 마이그레이션을 계획하는 사람들을 위해 기본 사항을 자세히 살펴보겠습니다.
앱 라우터와 페이지 라우터
| 기능 | 페이지 라우터 | 앱 라우터 |
|---|---|---|
| 라우팅 | pages/ 아래 | app/에서 |
| 구성 요소 | 기본적으로 클라이언트 구성요소 | 서버 구성요소 기본적으로 |
| 레이아웃 | 수동 구현 | 중첩 가능 layout.js |
| 로딩 | 수동 구현 | loading.js을 통해 자동 |
| 데이터 가져오기 | getServerSideProps 등 | 구성 요소 내부의 직접 async |
1. 파일 기반 라우팅
app/ 내부의 폴더 구조는 URL 경로에 직접 매핑됩니다.
app/
├── page.js → /
├── layout.js → Shared layout for all pages
├── about/
│ └── page.js → /about
└── blog/
├── page.js → /blog
└── [slug]/
└── page.js → /blog/hello-world (dynamic route)
// app/blog/[slug]/page.js
export default async function BlogPost({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.title}</article>;
}
2. 레이아웃 시스템
layout.js은 해당 폴더 아래의 모든 페이지에 자동으로 적용됩니다. 레이아웃은 중첩될 수 있습니다.
// app/layout.js (root layout)
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>Site Header</header>
{children}
<footer>Site Footer</footer>
</body>
</html>
);
}
// app/blog/layout.js (blog section layout)
export default function BlogLayout({ children }) {
return (
<div className="blog-wrapper">
<aside>Sidebar</aside>
<main>{children}</main>
</div>
);
}
3. 로딩 및 오류 상태
loading.js 및 error.js을 폴더에 놓으면 로딩 및 오류 UI가 자동으로 표시됩니다.
// app/blog/loading.js
export default function Loading() {
return <div>Loading...</div>;
}
// app/blog/error.js
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
4. 서버 및 클라이언트 구성 요소
앱 라우터에서는 서버 구성 요소가 기본값입니다. 이것이 핵심 변경 사항입니다.
| 서버 구성요소 | 클라이언트 구성요소 | |
|---|---|---|
| 렌더링 대상 | 서버 | 브라우저 |
| useState / useEffect | ❌ 이용 불가 | ✅ 이용 가능 |
| 직접 DB 접속 | ✅ 가능 | ❌ 불가능 |
| 번들 크기 | 작음(JS가 전송되지 않음) | 대형 |
| 상호작용 | ❌ 불가능 | ✅ 가능 |
// Server Component (default) — can access DB directly
export default async function PostList() {
const posts = await db.query('SELECT * FROM posts');
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
경험 법칙: 기본적으로 서버 구성 요소를 작성하고 가장 작은 대화형 부분에만 클라이언트 구성 요소를 사용합니다.
5. 데이터 가져오기 패턴
앱 라우터를 사용하면 구성요소 내에서 직접 fetch()을 호출할 수 있습니다. 캐싱은 기본적으로 켜져 있습니다.
// Cached (default)
const data = await fetch('https://api.example.com/posts');
// No cache (fresh every request)
const data = await fetch('https://api.example.com/posts', {
cache: 'no-store',
});
// Time-based revalidation
const data = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 },
});
6. 경로 그룹
URL에 영향을 주지 않고 파일을 정리하려면 ( )에 폴더 이름을 래핑하세요.
app/
├── (marketing)/
│ ├── page.js → /
│ └── pricing/
│ └── page.js → /pricing
└── (shop)/
├── layout.js → Shop-specific layout
└── products/
└── page.js → /products
URL 접두사가 없는 섹션 전체에서 다양한 레이아웃을 사용하는 데 적합합니다.
요약
App Router는 Server Components를 중심으로 한 새로운 아키텍처로, Pages Router와는 그 철학이 크게 다릅니다. 처음에는 혼란스러울 수도 있지만 “서버 구성 요소를 먼저, 필요한 곳에만 클라이언트 구성 요소"를 고수하면 고성능 응용 프로그램이 탄생할 것입니다. 마이그레이션의 경우 더 작은 페이지부터 시작하여 점진적으로 이동하세요.

