1. 핵심 개념
App Router의 가장 큰 장점은 "개발자가 따로 신경 쓰지 않아도, 성능 최적화 기본값이 구조적으로 적용된다" 는 점이다.
Pages Router에서도 일부는 가능했지만, App Router는 라우팅 + 렌더링 + 데이터 패칭 구조 자체가 최적화를 전제로 설계되어 있다.
비유하자면, Pages Router는 "요리 재료를 잘 쓰면 맛있는 음식이 될 수 있는 주방"이고, App Router는 "기본 레시피 자체가 이미 최적화된 주방"이다.
2. 왜 이렇게 설계하는가 ⭐
Pages Router는 개발자가 직접 dynamic import, prefetch 옵션, 레이아웃 유지 로직을 구현해야 했다.
반복 구현이 많아질수록 실수가 생기고 팀마다 구조가 달라지는 문제가 있었다.
App Router는 이 최적화들을 구조 자체에 내장해서, 개발자가 신경 쓰지 않아도 기본값으로 적용되도록 설계됐다.
"잘 쓰면 최적화되는 구조"가 아니라 "기본값이 이미 최적화인 구조"가 핵심이다.
"App Router를 왜 쓰나요?" 질문엔 단순히 "최신이라서"가 아니라,
"최적화가 구조적 기본값으로 내장되어 있어 개발자 실수를 줄이고
일관된 성능을 보장하기 때문"이라고 답하면 된다.
3. 사용 원리 (코드 실행 흐름)
자동 Code Splitting
라우트 단위로 JS 번들이 자동 분리된다. 해당 라우트에 접근하기 전까지는 관련 JS가 브라우저에 내려오지 않는다.
// app/dashboard/user/page.tsx
// [입력] /dashboard/user URL 요청
// [처리] Next.js가 해당 라우트 파일만 번들에서 분리
// [출력] UserPage, UserTable 관련 JS는 이 라우트 접근 전까지 로드 안 됨
export default function UserPage() {
return <UserTable />;
}Link 기반 자동 Prefetch
// [입력] <Link />가 뷰포트에 들어오는 순간
// [처리] Next.js가 백그라운드에서 JS + RSC Payload + 데이터까지 사전 준비
// [출력] 클릭 시 즉시 화면 전환 (네트워크 요청 없음)
import Link from 'next/link';
<Link href="/dashboard/user">User</Link>React Server Component (RSC)
// [입력] 서버에서 페이지 요청
// [처리] 기본적으로 Server Component로 실행 → 클라이언트 JS 번들에 미포함
// [출력] HTML + 필요한 데이터만 클라이언트로 전달
export default async function Page() {
const data = await fetchData(); // 서버에서 직접 DB/API 접근 가능
return <div>{data}</div>;
}
// 클라이언트 상호작용이 필요할 때만 최소 단위로 분리
'use client';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}Streaming + Suspense
// [입력] 페이지 요청
// [처리] 빠른 UI 먼저 전송 → 느린 데이터는 Suspense boundary에서 대기
// [출력] 사용자는 로딩 중에도 빈 화면이 아닌 기본 UI를 볼 수 있음
<Suspense fallback={<Loading />}>
<SlowComponent />
</Suspense>4. 데이터 흐름
Code Splitting + Prefetch 흐름
- 사용자가
/페이지 진입 →app/page.tsx만 로드 - 뷰포트에
<Link href="/dashboard">등장 →/dashboard관련 JS + RSC Payload 백그라운드 준비 - 사용자가 링크 클릭 → 이미 준비된 데이터로 즉시 전환
/dashboard/user는 아직 요청 전 → JS 로드 안 됨
RSC + Streaming 흐름
/dashboard요청 →RootLayout(Server Component) 실행DashboardLayout실행 → 공통 사이드바, 헤더 렌더링page.tsx실행 → Suspense boundary 안의 느린 컴포넌트는 fallback UI 먼저 전송- 데이터 준비 완료 → 스트리밍으로 실제 컴포넌트 교체
5. 주요 기능 (실무 기준)
| 기능 | 역할 | Pages Router 대비 |
|---|---|---|
| Code Splitting | 라우트 단위 자동 분리 | Pages Router는 페이지 단위만 |
| Prefetch | JS + RSC Payload + 데이터까지 사전 준비 | Pages Router는 JS만 |
| Layout 유지 | 공통 UI 재렌더링 없이 page만 교체 | 직접 구현 필요 |
| RSC | 기본값이 Server Component | 지원 안 함 |
| Streaming | 라우트 단위 스트리밍 | 제한적 |
| 상태 파일 | loading, error, not-found 파일 기반 | 코드로 직접 처리 |
| 데이터 캐싱 | fetch 옵션으로 ISR/Revalidate 통합 | 별도 설정 필요 |
6. 실무 사용 패턴
패턴 1: 인증 영역을 Segment Layout으로 분리
// app/(auth)/layout.tsx
// 인증이 필요한 모든 페이지에서 세션 체크를 구조적으로 강제
export default async function AuthLayout({ children }) {
const session = await getSession();
if (!session) redirect('/login');
return <>{children}</>;
}인증이 필요한 구역에 Segment Layout을 두고 여기서 세션 체크를 한다.
각 페이지마다 인증 로직을 반복하지 않아도 되고, 실수로 빠뜨리는 경우도 없어진다.
패턴 2: 느린 데이터는 Suspense로 분리
// app/dashboard/page.tsx
// 빠른 UI와 느린 데이터를 Suspense로 분리 → 초기 화면 응답 속도 개선
export default function DashboardPage() {
return (
<div>
<DashboardHeader /> {/* 빠름 → 먼저 렌더 */}
<Suspense fallback={<Skeleton />}>
<SlowDataComponent /> {/* 느림 → 스트리밍으로 나중에 */}
</Suspense>
</div>
);
}패턴 3: 매 이동마다 초기화가 필요한 UI는 template 사용
// app/dashboard/template.tsx (layout.tsx가 아님)
// 페이지뷰 트래킹, 애니메이션 초기화 등 매 이동 시 리셋이 필요한 경우
export default function DashboardTemplate({ children }) {
useEffect(() => {
trackPageView(); // 매 이동마다 실행됨
}, []);
return <>{children}</>;
}7. 자주 하는 실수 / 주의사항
❌ 잘못된 방법: Server Component에서 useState, useEffect 사용
// app/dashboard/page.tsx
export default function Page() {
const [count, setCount] = useState(0); // ❌ 에러
return <div>{count}</div>;
}✅ 올바른 방법: 'use client' 선언 후 최소 단위로 분리
'use client';
export default function Counter() {
const [count, setCount] = useState(0); // ✅
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}🔎 이유: App Router에서 모든 컴포넌트는 기본 Server Component다. 브라우저 API와 React 훅은 클라이언트 환경에서만 실행되기 때문에 에러가 발생한다. 'use client'는 필요한 컴포넌트에만 최소 단위로 적용해야 번들 사이즈를 줄일 수 있다.
❌ 잘못된 방법: 매 이동마다 리셋이 필요한 UI를 layout.tsx에 배치
✅ 올바른 방법: template.tsx 사용
🔎 이유: layout.tsx는 페이지 이동 시 재마운트되지 않고 상태를 유지하는 구조다. 애니메이션 초기화, 페이지뷰 트래킹처럼 매 이동마다 리셋이 필요한 UI는 template.tsx를 써야 한다.
❌ 잘못된 방법: 모든 컴포넌트에 'use client' 붙이기
✅ 올바른 방법: 상호작용이 필요한 최말단 컴포넌트에만 적용
🔎 이유: 'use client'를 최상위 컴포넌트에 붙이면 하위 컴포넌트 전체가 클라이언트 번들에 포함된다. RSC의 번들 감소 이점이 사라진다.
8. 한 줄 요약
App Router의 핵심은 "최적화를 잘하는 것"이 아니라,
"Code Splitting, Prefetch, RSC, Streaming이 구조적 기본값으로 내장되어 개발자가 별도로 신경 쓰지 않아도 성능이 보장되는 구조"를 제공한다는 점이다.