Next.js 13에서 App Router가 도입되면서 기존 Pages Router와의 선택이 중요한 이슈가 되었다. 두 방식은 완전히 다른 라우팅 패러다임을 제공한다.
이번 글에서는 App Router와 Pages Router의 차이를 명확히 이해하고, 실무에서 어떤 상황에 어떤 것을 선택해야 하는지 알아보려고 한다.
Pages Router: 전통적인 방식
Next.js의 전통적인 라우팅 방식으로, pages 디렉토리 구조가 URL을 결정한다.
pages/
index.js → /
about.js → /about
blog/
index.js → /blog
[slug].js → /blog/:slug
api/
users.js → /api/users
기본 구조
// pages/index.js
export default function Home() {
return <h1>Home</h1>;
}
// pages/about.js
export default function About() {
return <h1>About</h1>;
}
// pages/blog/[slug].js
export default function BlogPost({ post }) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
return {
props: { post },
revalidate: 60, // ISR
};
}
export async function getStaticPaths() {
const posts = await getAllPosts();
return {
paths: posts.map(post => ({ params: { slug: post.slug } })),
fallback: 'blocking',
};
}App Router: 새로운 방식
Next.js 13+에서 도입된 방식으로, app 디렉토리 구조와 파일 컨벤션을 사용한다.
app/
page.js → /
about/
page.js → /about
blog/
page.js → /blog
[slug]/
page.js → /blog/:slug
api/
users/
route.js → /api/users
기본 구조
// app/page.js (Server Component 기본)
export default async function Home() {
const data = await fetch('https://api.example.com/data');
return <h1>Home</h1>;
}
// app/about/page.js
export default function About() {
return <h1>About</h1>;
}
// app/blog/[slug]/page.js
export default async function BlogPost({ params }) {
const post = await getPost(params.slug);
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
// 동적 라우트 생성
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(post => ({ slug: post.slug }));
}핵심 차이점
1. 데이터 페칭 방식
Pages Router: getStaticProps, getServerSideProps 사용
// Pages Router
export async function getStaticProps() {
const data = await fetchData();
return {
props: { data },
revalidate: 60,
};
}
export default function Page({ data }) {
return <div>{data}</div>;
}App Router: 컴포넌트 내에서 직접 fetch
// App Router
export default async function Page() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 },
});
return <div>{data}</div>;
}2. 레이아웃 시스템
Pages Router: _app.js로 전역 레이아웃
// pages/_app.js
export default function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}App Router: layout.js로 중첩 레이아웃
// app/layout.js (루트 레이아웃)
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
// app/blog/layout.js (블로그 섹션 레이아웃)
export default function BlogLayout({ children }) {
return (
<div>
<BlogNav />
{children}
</div>
);
}3. 로딩 및 에러 처리
Pages Router: 커스텀 에러 페이지만
// pages/_error.js
function Error({ statusCode }) {
return <p>{statusCode} 에러가 발생했습니다</p>;
}App Router: loading.js, error.js 파일로 자동 처리
// app/blog/loading.js
export default function Loading() {
return <div>로딩 중...</div>;
}
// app/blog/error.js
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>에러가 발생했습니다</h2>
<button onClick={reset}>다시 시도</button>
</div>
);
}4. 메타데이터 관리
Pages Router: next/head 또는 커스텀 _document.js
import Head from 'next/head';
export default function Page() {
return (
<>
<Head>
<title>Page Title</title>
<meta name="description" content="..." />
</Head>
<div>Content</div>
</>
);
}App Router: metadata 객체 또는 generateMetadata 함수
// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.description,
openGraph: {
images: [post.image],
},
};
}5. 서버 컴포넌트 지원
Pages Router: 모든 컴포넌트가 클라이언트 컴포넌트
// Pages Router: 항상 클라이언트에서 실행
export default function Page() {
return <div>Content</div>;
}App Router: 기본적으로 Server Component, 필요시 'use client'
// App Router: 기본적으로 Server Component
export default async function Page() {
const data = await fetchData(); // 서버에서 실행
return <div>{data}</div>;
}
// Client Component가 필요한 경우
'use client';
export default function InteractiveComponent() {
const [state, setState] = useState(0);
return <button onClick={()=> setState(state + 1)}>{state}</button>;
}실무 선택 가이드
Pages Router를 선택해야 하는 경우
- 기존 프로젝트: 이미 Pages Router로 구축된 프로젝트
- 안정성 중시: 검증된 방식으로 빠르게 개발
- 간단한 프로젝트: 복잡한 레이아웃이나 중첩 라우팅이 필요 없음
- API Routes 중심: API Routes를 많이 사용하는 경우
// Pages Router가 적합한 예시
pages/
index.js
about.js
contact.js
api/
contact.jsApp Router를 선택해야 하는 경우
- 새 프로젝트: 처음부터 시작하는 프로젝트
- 복잡한 레이아웃: 중첩 레이아웃이 필요한 경우
- 성능 최적화: Server Components로 번들 사이즈 최적화
- 최신 기능: Streaming, Suspense 등 최신 기능 필요
// App Router가 적합한 예시
app/
(marketing)/
page.js
about/
page.js
(dashboard)/
layout.js
page.js
settings/
page.js마이그레이션 전략
점진적 마이그레이션
Next.js는 Pages Router와 App Router를 동시에 사용할 수 있다.
app/
page.js → / (우선순위 높음)
about/
page.js → /about
pages/
contact.js → /contact (app에 없으면 사용)
api/
users.js → /api/users
단계별 마이그레이션
- 1단계: 새 페이지는 App Router로 작성
- 2단계: 점진적으로 기존 페이지 마이그레이션
- 3단계: 완전히 App Router로 전환
실무 패턴 비교
인증이 필요한 페이지
Pages Router:
// pages/dashboard.js
export async function getServerSideProps({ req }) {
const session = await getSession(req);
if (!session) {
return {
redirect: {
destination: '/login',
permanent: false,
},
};
}
return { props: { session } };
}
export default function Dashboard({ session }) {
return <div>Dashboard</div>;
}App Router:
// app/dashboard/page.js
import { redirect } from 'next/navigation';
export default async function Dashboard() {
const session = await getSession();
if (!session) {
redirect('/login');
}
return <div>Dashboard</div>;
}동적 라우트
Pages Router:
// pages/products/[id].js
export async function getStaticPaths() {
const products = await getProducts();
return {
paths: products.map(p => ({ params: { id: p.id } })),
fallback: 'blocking',
};
}
export async function getStaticProps({ params }) {
const product = await getProduct(params.id);
return { props: { product } };
}App Router:
// app/products/[id]/page.js
export async function generateStaticParams() {
const products = await getProducts();
return products.map(p => ({ id: p.id }));
}
export default async function Product({ params }) {
const product = await getProduct(params.id);
return <div>{product.name}</div>;
}주의사항
1. App Router는 아직 일부 기능이 실험적
- 일부 서드파티 라이브러리가 완전히 지원하지 않을 수 있음
- 문서가 계속 업데이트됨
2. Pages Router는 여전히 안정적
- 모든 기능이 검증됨
- 대부분의 라이브러리가 지원
3. 혼용 시 주의사항
- 같은 경로에 두 방식 모두 있으면 App Router가 우선
- API Routes는 Pages Router 방식 유지
마무리
Pages Router와 App Router는 각각 장단점이 있다. 기존 프로젝트라면 Pages Router를 유지하는 것이 안전하지만, 새 프로젝트라면 App Router의 최신 기능과 성능 이점을 활용하는 것이 좋다.
가장 중요한 것은 팀의 상황과 프로젝트 요구사항에 맞는 선택이다. 복잡한 레이아웃과 성능 최적화가 중요하다면 App Router를, 안정성과 검증된 방식이 중요하다면 Pages Router를 선택하는 것이 합리적이다.
