Pages Router 개념
Pages Router는 Next.js의 전통적인 라우팅 시스템이다.
/pages 디렉토리 내의 파일 구조를 기반으로 라우트가 자동으로 생성되는 방식으로 동작한다.
(직관적이고 간단하게 라우팅을 구현할 수 있다는 장점)
예를 들어, /pages/about.js 파일을 생성하면 자동으로 /about 경로로 접근할 수 있게 된다.
또한 /pages/blog/[id].js와 같은 동적 라우팅도 지원하여 블로그 포스트와 같은 동적 콘텐츠를 쉽게 구현할 수 있다.
기본적인 Pages Router 구조의 예시 코드는 다음과 같다!
// pages/index.js
export default function Home() {
return (
<div>
<h1>홈페이지</h1>
<p>Next.js Pages Router를 사용한 홈페이지입니다.</p>
</div>
);
}
// pages/about.js
export default function About() {
return (
<div>
<h1>소개 페이지</h1>
<p>이 사이트에 대한 소개 페이지입니다.</p>
</div>
);
}
// pages/blog/[id].js
import { useRouter } from 'next/router';
export default function BlogPost() {
const router = useRouter();
const { id } = router.query;
return (
<div>
<h1>블로그 포스트 {id}</h1>
<p>이 페이지는 동적 라우팅을 통해 생성되었습니다.</p>
</div>
);
}
위 코드에서 pages/blog/[id].js는 동적 라우팅을 구현한 예시이다.
useRouter 훅을 통해 URL 파라미터를 가져올 수 있으며, 이를 통해 블로그 포스트의 ID에 접근할 수 있다.
예를 들어 /blog/1로 접속하면 id 값은 1이 된다
Pages Router에서 데이터 페칭은 주로 getStaticProps, getServerSideProps, getStaticPaths 함수를 통해 이루어진다.
이런 함수들은 페이지가 렌더링되기 전에 서버에서 데이터를 미리 가져오는 역할을 한다.
App Router 개념
App Router는 Next.js 13에서 도입된 새로운 라우팅 시스템으로, React 18의 서버 컴포넌트와 Suspense를 활용한다.
기존 Pages Router와 달리 /app 디렉토리를 사용하며, 더 유연하고 강력한 라우팅 기능을 제공한다.
App Router의 가장 큰 특징은 서버 컴포넌트를 기본적으로 사용한다는 점이다.
서버 컴포넌트는 서버에서 렌더링되며, 클라이언트로 HTML만 전송되므로 JavaScript 번들 크기를 줄이고 초기 로딩 속도를 향상시킬 수 있다.
App Router의 기본 구조 또한 살펴볼까???
// app/page.js
export default function Home() {
return (
<div>
<h1>홈페이지</h1>
<p>Next.js App Router를 사용한 홈페이지입니다.</p>
</div>
);
}
// app/about/page.js
export default function About() {
return (
<div>
<h1>소개 페이지</h1>
<p>이 사이트에 대한 소개 페이지입니다.</p>
</div>
);
}
// app/blog/[id]/page.js
export default function BlogPost({ params }) {
return (
<div>
<h1>블로그 포스트 {params.id}</h1>
<p>이 페이지는 App Router의 동적 라우팅으로 생성되었습니다.</p>
</div>
);
}
위 코드에서 볼 수 있듯이, App Router에서는 폴더 구조가 URL 경로를 결정한다.
app/blog/[id]/page.js는 /blog/1과 같은 URL에 대응된다.
또한 Pages Router와 달리 파라미터를 가져오기 위해 useRouter를 사용할 필요 없이, 컴포넌트의 params prop을 통해 직접 접근할 수 있다.
App Router에서 데이터 페칭은 주로 비동기 컴포넌트를 통해 이루어진다.
아래 예시 코드를 살펴보자.
// app/blog/[id]/page.js
async function getPost(id) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
next: { revalidate: 3600 } // 1시간마다 재검증
});
return res.json();
}
export default async function BlogPost({ params }) {
const post = await getPost(params.id);
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
이 코드에서는 컴포넌트 자체가 비동기 함수로 선언되어 데이터를 직접 가져온다.
또한 fetch 함수 옵션을 통해 데이터 캐싱과 재검증 전략을 설정할 수 있다.
이는 Pages Router의 getStaticProps와 getServerSideProps를 대체하는 더 직관적인 방식이다!!!!
라우팅 방식 비교
개념을 살펴보았는데 아직도 차이를 모르시는 분이 있을 수도 있다.
Pages Router와 App Router의 라우팅 방식에는 몇 가지 중요한 차이점이 있다.
Pages Router는 파일 기반 라우팅을 사용하며 /pages 디렉토리 내의 파일 이름과 구조가 URL 경로를 결정한다.
예를 들어보자.
- /pages/index.js → /
- /pages/about.js → /about
- /pages/blog/[id].js → /blog/:id
반면, App Router는 폴더 기반 라우팅을 사용하며 /app 디렉토리 내의 폴더 구조가 URL 경로를 결정한다.
🚨각 경로에는 page.js 파일이 필요하다!!!!🚨
- /app/page.js → /
- /app/about/page.js → /about
- /app/blog/[id]/page.js → /blog/:id
App Router에서는 특별한 파일 규칙을 통해 추가 기능을 제공한다.
- layout.js: 여러 페이지에 공통 레이아웃 적용
- loading.js: 로딩 상태 표시
- error.js: 에러 처리
- not-found.js: 404 페이지
이런 특별한 파일들을 사용한 구조의 예시는 다음과 같다.
// app/blog/layout.js
export default function BlogLayout({ children }) {
return (
<div className="blog-layout">
<h1>블로그</h1>
<nav>
<a href="/blog/1">포스트 1</a>
<a href="/blog/2">포스트 2</a>
</nav>
<main>{children}</main>
<footer>© 2025 My Blog</footer>
</div>
);
}
// app/blog/loading.js
export default function Loading() {
return <div>포스트를 불러오는 중...</div>;
}
// app/blog/[id]/error.js
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>에러가 발생했습니다</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>다시 시도</button>
</div>
);
}
- 위 코드에서 layout.js는 /blog 경로 아래의 모든 페이지에 공통된 레이아웃을 제공한다.
- loading.js는 페이지가 로딩 중일 때 표시되는 컴포넌트이며,
- error.js는 에러 발생 시 표시되는 컴포넌트이다.
데이터 페칭 방식 비교
데이터 페칭 방식에서도 Pages Router와 App Router 사이에 상당한 차이가 있다.
위에서도 설명했지만 Pages Router에서는 주로 세 가지 함수를 사용하여 데이터를 가져온다.
- getStaticProps: 빌드 시점에 데이터를 가져와서 정적 페이지 생성
- getServerSideProps: 요청 시점에 서버에서 데이터를 가져옴
- getStaticPaths: 동적 라우트에 대해 빌드 시점에 생성할 경로 지정
// pages/posts/[id].js (Pages Router)
export async function getServerSideProps({ params }) {
const res = await fetch(`https://api.example.com/posts/${params.id}`);
const post = await res.json();
return {
props: { post },
};
}
export default function Post({ post }) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
반면, App Router에서는 컴포넌트 내에서 직접 비동기 데이터 페칭이 가능하다.
React 서버 컴포넌트를 활용하여 더 간단하고 직관적인 방식으로 데이터를 가져올 수 있다.
// app/posts/[id]/page.js (App Router)
async function getPost(id) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
cache: 'no-store' // SSR과 동등한 동작
});
return res.json();
}
export default async function Post({ params }) {
const post = await getPost(params.id);
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
App Router에서는 fetch 함수에 옵션을 추가하여 캐싱 전략을 설정할 수 있다!!!
- cache: 'force-cache': 기본값, 결과를 무기한 캐싱 (Pages Router의 getStaticProps와 유사)
- cache: 'no-store': 캐싱 없이 매 요청마다 데이터 가져오기 (Pages Router의 getServerSideProps와 유사)
- next: { revalidate: 60 }: 60초마다 데이터 재검증 (ISR - Incremental Static Regeneration)
이러한 방식은 데이터 페칭 로직을 컴포넌트와 함께 배치할 수 있어 코드 관리가 더 용이하다고 볼 수 있다!!!
렌더링 방식 비교
Pages Router와 App Router는 렌더링 방식에서도 큰 차이가 있다.
Pages Router에서는 모든 컴포넌트가 기본적으로 클라이언트 컴포넌트로 취급된다.
서버 사이드 렌더링(SSR)이나 정적 사이트 생성(SSG)을 통해 초기 HTML을 서버에서 생성하지만, 클라이언트로 전송된 후에는 React가 하이드레이션(hydration) 과정을 거쳐 인터랙티브한 페이지로 만든다.
// pages/index.js (Pages Router)
import { useState } from 'react';
export default function Home() {
const [count, setCount] = useState(0);
return (
<div>
<h1>카운터: {count}</h1>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
위 코드는 클라이언트에서 렌더링되는 일반적인 React 컴포넌트이다.
useState와 같은 React 훅을 사용하고 있으며, 버튼 클릭 시 상태가 업데이트되어 UI가 변경된다.
React의 useState 훅 관련 글은 아래글을 참고해봐도 좋을 것 같다.
[React] useState로 상태 관리하기
필자는 React를 처음 공부하기 시작했을 때, 클래스 컴포넌트의 this.state와 this.setState가 너무 복잡하게 느껴졌다.그러던 중 함수형 컴포넌트와 함께 등장한 useState라는 것을 알았고 기억을 더듬어
bbin-guuuu.tistory.com
반면, App Router에서는 모든 컴포넌트가 기본적으로 서버 컴포넌트로 취급된다.
서버 컴포넌트는 서버에서만 실행되며, 렌더링 결과만 클라이언트로 전송된다.
이는 JavaScript 번들 크기를 줄이고 초기 로딩 성능을 향상시킨다!!!!
// app/page.js (App Router - 서버 컴포넌트)
async function getData() {
const res = await fetch('https://api.example.com/data');
return res.json();
}
export default async function Home() {
const data = await getData();
return (
<div>
<h1>서버 컴포넌트 예시</h1>
<p>데이터: {data.message}</p>
</div>
);
}
위 코드는 서버 컴포넌트 예시로, 비동기 함수를 사용하여 데이터를 가져오고 있다.
이 컴포넌트는 서버에서만 실행되며, 클라이언트로는 렌더링된 HTML만 전송된다.
만약 App Router에서 클라이언트 상태나 이벤트 핸들러가 필요한 경우, 'use client' 지시어를 사용하여 클라이언트 컴포넌트를 선언할 수 있다!
// app/counter.js (App Router - 클라이언트 컴포넌트)
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>카운터: {count}</h2>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
// app/page.js
import Counter from './counter';
export default function Home() {
return (
<div>
<h1>홈페이지</h1>
<Counter />
</div>
);
}
이 코드에서 Counter 컴포넌트는 'use client' 지시어를 통해 클라이언트 컴포넌트로 선언되었다.
이 컴포넌트는 클라이언트에서 실행되며, React 훅과 이벤트 핸들러를 사용할 수 있다.
서버 컴포넌트인 Home에서 이 클라이언트 컴포넌트를 가져와 사용하고 있다.
이러한 서버 컴포넌트와 클라이언트 컴포넌트의 분리는 App Router의 가장 큰 특징 중 하나라고 볼 수 있다!!!!
레이아웃 구성 비교
레이아웃 구성 방식에서도 Pages Router와 App Router 사이에 큰 차이가 있다.
Pages Router에서는 주로 _app.js와 _document.js 파일을 통해 전역 레이아웃을 구성한다.
모든 페이지에 공통적으로 적용되는 레이아웃을 구현할 수 있지만, 중첩된 레이아웃을 구현하기 위해서는 별도의 컴포넌트를 직접 생성하고 관리해야 한다.
// pages/_app.js
import '../styles/globals.css';
import Navbar from '../components/Navbar';
import Footer from '../components/Footer';
function MyApp({ Component, pageProps }) {
return (
<>
<Navbar />
<Component {...pageProps} />
<Footer />
</>
);
}
export default MyApp;
위 코드는 모든 페이지에 적용되는 기본 레이아웃을 구현한 예시이다
_app.js에서 Navbar와 Footer 컴포넌트를 배치하여 모든 페이지에서 공통적으로 사용된다!
반면, App Router에서는 layout.js 파일을 통해 더 유연하고 직관적인 레이아웃 구성이 가능하다.
폴더 구조에 맞게 중첩된 레이아웃을 쉽게 구현할 수 있다.
// app/layout.js (루트 레이아웃)
import './globals.css';
export default function RootLayout({ children }) {
return (
<html lang="ko">
<body>
<header>
<nav>
<a href="/">홈</a>
<a href="/about">소개</a>
<a href="/blog">블로그</a>
</nav>
</header>
<main>{children}</main>
<footer>© 2025 My Website</footer>
</body>
</html>
);
}
// app/blog/layout.js (블로그 섹션 레이아웃)
export default function BlogLayout({ children }) {
return (
<div>
<aside>
<h3>최근 포스트</h3>
<ul>
<li><a href="/blog/1">포스트 1</a></li>
<li><a href="/blog/2">포스트 2</a></li>
</ul>
</aside>
<section>{children}</section>
</div>
);
}
위 코드에서 app/layout.js는 모든 페이지에 적용되는 루트 레이아웃을 정의한다. app/blog/layout.js는 /blog 경로 아래의 모든 페이지에 추가적으로 적용되는 레이아웃이다. 이처럼 App Router에서는 폴더 구조에 따라 레이아웃을 중첩하여 구성할 수 있어 더 직관적이고 유지보수하기 쉽다.
그럼 어떤 것을 선택해야 할까?
Pages Router와 App Router 중 어떤 것을 선택해야 할지는 프로젝트의 요구사항과 개발자의 선호도에 따라 달라질 수 있다.
Pages Router는 다음과 같은 경우에 적합하다.
- 기존 Next.js 프로젝트를 유지보수하는 경우
- 간단한 웹사이트나 애플리케이션을 빠르게 개발해야 하는 경우
- 안정적이고 검증된 기술을 선호하는 경우
- 서드파티 라이브러리와의 호환성이 중요한 경우
App Router는 다음과 같은 경우에 적합하다.
- 새 프로젝트를 시작하는 경우
- React 서버 컴포넌트의 이점을 활용하고 싶은 경우
- 더 세분화된 렌더링 제어와 성능 최적화가 필요한 경우
- 중첩된 레이아웃과 로딩 상태를 더 직관적으로 관리하고 싶은 경우
'Next.js' 카테고리의 다른 글
[Next.js] 'use client'와 React Hooks의 관계 - SSR과 CSR의 경계 이해하기 (0) | 2025.03.16 |
---|---|
[Next.js] ❗️입문자 주목❗️ TypeScript로 구현하는 로그인 시스템 튜토리얼 (0) | 2025.03.14 |
[Next.js] SSR이란 무엇인가? - Next.js의 서버 사이드 렌더링 이해하기 (1) | 2025.03.12 |
[Next.js] styled-components(CSS in JS)를 Next.js에서 사용한다고??! 🚨 (1) | 2025.03.11 |
[Next.js] 파일 시스템 기반 라우팅 개념 정리 (0) | 2025.03.10 |