Next.js Page Router vs App Router 성능 비교

contability·2025년 8월 22일

Next.js Page Router vs App Router 성능 비교

개요

Next.js 13에서 도입된 App Router는 React 18의 Server Components를 활용해 기존 Page Router 대비 상당한 성능 향상을 제공한다. 가장 큰 차이점은 서버와 클라이언트의 역할 분담이 명확해지면서 번들 크기 최적화렌더링 효율성이 크게 개선된 것이다.

핵심 아키텍처 차이

Page Router의 한계

  • 모든 컴포넌트가 클라이언트에서 실행
  • 서버 로직은 getServerSideProps, getStaticProps로만 처리 가능
  • 서버와 클라이언트에 동일한 코드가 중복 존재

App Router의 혁신

  • Server Components: 서버에서만 실행되는 컴포넌트
  • Client Components: 브라우저에서 실행 ('use client' 필요)
  • 컴포넌트 레벨에서 직접 async/await 사용 가능
  • 자동 중복 요청 제거 및 캐싱 최적화

번들 분할과 실행 환경

빌드 결과 구조

.next/
├── server/           # 서버 번들 (Node.js 환경)
│   ├── app/
│   │   └── page.js   # Server Components 코드
│   └── chunks/
└── static/           # 클라이언트 번들 (브라우저로 전송)
    └── chunks/
        └── client.js # Client Components만 포함

실행 흐름

  1. 브라우저 요청 → Next.js 서버
  2. 서버: Server Components 실행 → RSC Payload 생성
  3. 클라이언트 전송: RSC Payload + 필요한 Client Components만
  4. 브라우저: Client Components 실행 + RSC Payload 렌더링

RSC (React Server Components) 동작 원리

서버에서 클라이언트로의 데이터 전송

기존 SSR과 달리 App Router는 클라이언트로 HTML이 아닌 직렬화된 React Element Tree를 전송한다.

순수 Server Component 예시

// Server Component - 서버에서만 실행
async function ProductList() {
  const products = await db.getProducts();
  return (
    <div>
      {products.map(product => (
        <div key={product.id}>
          <h3>{product.name}</h3>
          <p>{product.price}</p>
        </div>
      ))}
    </div>
  );
}

생성되는 RSC Payload:

M1:{"id":"./src/app/products/page.js","chunks":["app/products/page"],"name":""}
0:["$","div",null,{"children":[
  ["$","div",null,{"children":[
    ["$","h3",null,{"children":"iPhone 15"}],
    ["$","p",null,{"children":"$999"}]
  ],"key":"1"}],
  ["$","div",null,{"children":[
    ["$","h3",null,{"children":"Samsung Galaxy"}],
    ["$","p",null,{"children":"$899"}]
  ],"key":"2"}]
]}]

server component + server component 또한 플레이스홀더 없이 RSC Payload가 생성된다.

Server + Client Component 혼합 예시

// Server Component
async function HomePage() {
  const posts = await fetchPosts();
  return (
    <div>
      <h1>Posts</h1>
      {posts.map(post => <div key={post.id}>{post.title}</div>)}
      <ClientButton count={5} />  {/* Client Component */}
    </div>
  );
}

생성되는 RSC Payload:

M1:{"id":"./src/app/page.js","chunks":["app/page"],"name":""}
M2:{"id":"./src/components/ClientButton.js","chunks":["components/ClientButton"],"name":"ClientButton"}
0:["$","div",null,{"children":[
  ["$","h1",null,{"children":"Posts"}],
  ["$","div",null,{"children":"React 18 Guide"},"key":"1"],
  ["$","div",null,{"children":"Next.js Tutorial"},"key":"2"],
  ["$","$M2",null,{"count":5}]  // ← Client Component 플레이스홀더
]}]

Client Component 처리:

  • RSC Payload에는 플레이스홀더($M2)와 props만 포함
  • 브라우저에서 M2 모듈(ClientButton)을 로드하여 해당 위치에 렌더링
  • ClientButton에만 하이드레이션 적용

성능 최적화 효과

1. 번들 크기 최적화

Page Router의 문제:

// 모든 코드가 클라이언트 번들에 포함
import heavyDataProcessor from 'heavy-lib'; // 📦 클라이언트 번들에 포함
import { dbUtils } from 'database-utils';   // 📦 클라이언트 번들에 포함

export default function Products({ products }) {
  const processed = heavyDataProcessor(products); // 🖥️ 클라이언트에서 실행
  return <div>{/* 렌더링 */}</div>;
}

App Router의 해결:

// Server Component - 무거운 코드는 서버에만
import heavyDataProcessor from 'heavy-lib'; // ❌ 클라이언트 번들에 포함 안됨
import { dbUtils } from 'database-utils';   // ❌ 클라이언트 번들에 포함 안됨

export default async function Products() {
  const products = await fetchProducts();              // 🖥️ 서버에서만 실행
  const processed = heavyDataProcessor(products);      // 🖥️ 서버에서만 실행
  
  return (
    <div>
      {processed.map(item => <Item key={item.id} data={item} />)}
      <ClientButton />  {/* 필요한 인터랙션만 클라이언트에서 */}
    </div>
  );
}

2. 전송 최적화 비교

항목Page RouterApp Router
서버 번들A + B + C + DA + B (서버 코드만)
클라이언트 번들A + B + C + DC + D (클라이언트 코드만)
브라우저 다운로드전체 번들클라이언트 번들 + RSC Payload
코드 중복있음없음

3. 렌더링 성능

Page Router (기존 SSR):
1. 서버에서 HTML 생성
2. 클라이언트로 HTML + 전체 JavaScript 번들 전송
3. 하이드레이션으로 전체 페이지를 다시 연결

App Router (RSC):
1. 서버에서 Server Components 실행
2. 직렬화된 React Element Tree + 필요한 Client Components만 전송
3. Server Component 부분은 하이드레이션 없이 Virtual DOM에 직접 삽입
4. Client Component만 선택적 하이드레이션

추가 성능 이점

데이터 페칭 효율성

  • 컴포넌트 레벨 데이터 페칭: 각 컴포넌트에서 필요한 데이터를 직접 가져옴
  • 자동 중복 제거: 같은 데이터를 여러 컴포넌트에서 요청해도 한 번만 실행
  • 병렬 처리: 독립적인 데이터 요청이 동시에 실행

스트리밍과 Suspense

  • 점진적 로딩: 준비된 부분부터 순차적으로 렌더링
  • 세밀한 로딩 상태 제어: 컴포넌트별로 로딩 상태 관리
  • 사용자 경험 향상: 전체 페이지를 기다릴 필요 없음

결론

아주 간단하게 내용을 정리하면
서버 컴포넌트는 RSC Payload로 결과만 전달하므로, 브라우저가 서버 로직까지 다운로드할 필요가 없다는 점이다.

이처럼 App Router는 단순한 라우팅 방식의 변화가 아니라, 서버와 클라이언트의 역할을 근본적으로 재정의한 아키텍처이다.

핵심은 "같은 기능, 더 적은 클라이언트 번들"이다. 총 코드량은 비슷하지만 브라우저가 다운로드해야 하는 JavaScript의 양을 크게 줄임으로써, 초기 로딩 속도와 전반적인 사용자 경험을 개선한다.

0개의 댓글