Next.js 13부터 등장한 App Router, 단순히 폴더 이름이 pages에서 app으로 바뀐 게 아닙니다. 우리가 리액트를 사용하는 방식 자체가 더 효율적으로 변한 것입니다! 어떤 차이가 있는지 살펴봅시다.
Pages Router와 App Router는 사용할 수 있는 컴포넌트와 기본(default) 컴포넌트에 차이가 있습니다.
Pages Router에서는 클라이언트 컴포넌트만 사용할 수 있고, App Router는 기본적으로 서버 컴포넌트이지만, 파일 최상단에 'use client’를 붙여 클라이언트 컴포넌트로 전환할 수 있습니다.
클라이언트 컴포넌트만 존재하는 Pages Router는 서버에서 렌더링(SSR)을 하더라도 클라이언트에서 실행되어야 하기 때문에 거대한 JS 번들을 내려받아야 합니다. 하지만, App Router에는 JS 번들에 포함되지 않는 ‘서버 컴포넌트’가 존재하므로 클라이언트로 보내는 JS 양이 획기적으로 줄어들게 되고, 초기 로딩 속도가 비약적으로 향상됩니다.
💡 클라이언트 컴포넌트 vs. 서버 컴포넌트 가장 근본적인 차이는 "컴포넌트가 어디서 실행되는가"입니다. 클라이언트 컴포넌트는 서버에서 HTML을 먼저 만든 후 브라우저에서 다시 실행(Hydration)되지만, 서버 컴포넌트는 서버에서만 실행됩니다.
| 구분 | Pages Router | App Router |
| 기본 컴포넌트 성격 | 클라이언트 컴포넌트 (SSR 지원) | 서버 컴포넌트 (RSC) |
| 서버 컴포넌트 유무 | 없음 (모든 파일이 JS 번들에 포함됨) | 있음 (기본값, JS 번들에 포함 안 됨) |
| 클라이언트 컴포넌트 | 별도 선언 없이 모든 컴포넌트 | 파일 최상단에 'use client' 선언 필요 |
| 서버 전용 로직 | getServerSideProps 등 특수 함수 내부 | 컴포넌트 본문 전체 (async/await 사용) |
| 브라우저 API 사용 | useEffect 안에서만 가능 | 클라이언트 컴포넌트에서만 가능 |
| 상태 관리 (useState) | 모든 컴포넌트에서 가능 | 클라이언트 컴포넌트에서만 가능 |
| DB / 보안 통신 | API Route나 특수 함수에서만 가능 | 서버 컴포넌트 내부에서 직접 가능 |
💡 JS 번들 웹 서비스를 실행하기 위해 필요한 모든 자바스크립트 파일을 하나로 묶은 압축 팩
Pages Router는 getServerSideProps나 getStaticProps 같은 특수 함수를 페이지 단위로만 사용해야 했습니다. 데이터가 필요한 깊숙한 자식 컴포넌트까지 Props Drilling이 발생하기 쉬운 구조였습니다.
// `/pages` 폴더 안의 페이지에서만 가능!
export async function getServerSideProps() {
const res = await fetch(`https://...`);
const todos = await res.json();
return { props: { todos } };
}
export default function TodoListPage({ todos }: { todos: TodoType[] }) {
return (
<ul>
{todos.map((todo) => (
<Todo key={todo.id} todo={todo} />
))}
</ul>
);
}App Router는 서버 컴포넌트, 클라이언트 컴포넌트, 레이아웃 fetch가 필요한 컴포넌트 어디서든 async/await를 사용하여 데이터를 가져올 수 있습니다.
// `Profile` component에서도 서버에서 데이터를 가져오는 것이 가능!
export default async function Profile() {
const res = await fetch(`https://...`)
const user = await res.json()
return (
<div>
<div>{user.name}</div>
<div>{user.age}</div>
</div>
);
}📎 게다가, App Router에서의 fetch 함수는 표준 Web API에서 더 확장된 기능을 제공합니다.
- 중복 요청 제거 (Request Memoization): 한 번의 렌더링 안에서 똑같은 요청은 딱 한 번만 보내는 것
- 서버 데이터 캐시 (Data Cache): 서버(백엔드 등)로 부터 가져온 데이터를 Next.js 서버(ex. vercel 컴퓨터) 메모리에 저장했다가 다른 유저가 같은 요청을 했을 때, 캐싱된 데이터를 보여주는 것
- 재검증 (Revalidate): 캐싱된 값이 유효한 시간
Pages Router는 _app.tsx가 전체 애플리케이션을 감싸는 구조라 특정 부분만 공통 레이아웃을 적용하기가 까다로웠습니다. 페이지 이동 시 모든 요소가 다시 렌더링되는 경우도 많았습니다.
App Router는 중첩 레이아웃(Nested Layouts)을 지원합니다. 특정 폴더(경로)마다 layout.tsx를 둘 수 있어, 대시보드나 설정 페이지처럼 복잡한 UI를 효율적으로 관리할 수 있습니다. 특히 페이지가 바뀌어도 상태(State)가 유지되고 공통 UI는 리렌더링되지 않아 앱 같은 부드러운 UX를 제공합니다.
/app
layout.tsx // 전체 레이아웃
not-found.tsx // 전체 NotFoundPage
loading.tsx // 전체 로딩
error.tsx // 전체 에러
/dashboard
layout.tsx // dashboard 전용 레이아웃
not-found.tsx // dashboard 전용 NotFoundPage
loading.tsx // dashboard 전용 로딩
error.tsx // dashboard 전용 에러📎 참고 layout 뿐만 아니라, 로딩/에러 등의 상태를 not-found.tsx, loading.tsx, error.tsx 과 같이 파일로 처리할 수도 있고, 파일 위치에 따라 전용 상태 페이지로 사용할 수도 있습니다
차이점을 비교해보고, 사용해보니 왜 Next.js에서 App Router 사용을 권장하는지 알 수 있었습니다! App Router 쵝오 😘