Next.js App Router 완벽 가이드 | Server Components·Streaming·Parallel Routes
이 글의 핵심
Next.js 13+ App Router의 모든 것을 실전 예제로 완벽 정리합니다. Server Components, Streaming, Server Actions, Parallel Routes, Intercepting Routes까지 실무에 바로 적용할 수 있는 가이드입니다.
실무 경험 공유: Pages Router에서 App Router로 마이그레이션하면서, 초기 로딩 속도를 50% 향상시키고 코드 복잡도를 크게 줄인 경험을 공유합니다.
들어가며: “Pages Router가 복잡해요”
실무 문제 시나리오
시나리오 1: getServerSideProps가 복잡해요
데이터 페칭 로직이 분리되어 있습니다. App Router는 컴포넌트 안에서 직접 fetch합니다.
시나리오 2: 레이아웃 공유가 어려워요
_app.tsx가 복잡합니다. App Router는 중첩 레이아웃을 지원합니다.
시나리오 3: 로딩 상태 관리가 번거로워요
useState로 관리해야 합니다. App Router는 Suspense로 간단합니다.
1. App Router란?
핵심 특징
App Router는 Next.js 13+의 새로운 라우팅 시스템입니다.
주요 장점:
- Server Components: 서버에서 렌더링
- Streaming: 점진적 렌더링
- Server Actions: Form 처리 간소화
- 중첩 레이아웃: 레이아웃 공유
- Parallel Routes: 병렬 렌더링
2. 파일 기반 라우팅
기본 구조
app/
├── layout.tsx # 루트 레이아웃
├── page.tsx # / (홈)
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── layout.tsx # 블로그 레이아웃
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug
└── dashboard/
├── layout.tsx
├── page.tsx # /dashboard
└── settings/
└── page.tsx # /dashboard/settings
3. Server Components
기본 사용
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store', // 항상 최신 데이터
});
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}
캐싱 전략
// 정적 생성 (기본값)
fetch('https://api.example.com/posts', {
cache: 'force-cache',
});
// 매 요청마다 새로 가져오기
fetch('https://api.example.com/posts', {
cache: 'no-store',
});
// 10초마다 재검증
fetch('https://api.example.com/posts', {
next: { revalidate: 10 },
});
4. Client Components
’use client’ 지시어
// app/components/Counter.tsx
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Server + Client 조합
// app/posts/page.tsx (Server Component)
import { Counter } from './Counter';
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>Posts</h1>
<Counter /> {/* Client Component */}
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
5. Loading & Error
loading.tsx
// app/posts/loading.tsx
export default function Loading() {
return (
<div>
<p>Loading posts...</p>
</div>
);
}
error.tsx
// app/posts/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
6. Server Actions
Form 처리
// app/posts/new/page.tsx
import { redirect } from 'next/navigation';
async function createPost(formData: FormData) {
'use server';
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
});
redirect('/posts');
}
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
<button type="submit">Create</button>
</form>
);
}
useFormStatus
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
);
}
7. Parallel Routes
구조
app/
└── dashboard/
├── layout.tsx
├── @analytics/
│ └── page.tsx
├── @notifications/
│ └── page.tsx
└── page.tsx
Layout
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
notifications,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
notifications: React.ReactNode;
}) {
return (
<div>
<div>{children}</div>
<div>{analytics}</div>
<div>{notifications}</div>
</div>
);
}
8. Intercepting Routes
모달 구현
app/
├── photos/
│ ├── page.tsx
│ └── [id]/
│ └── page.tsx
└── @modal/
└── (.)photos/
└── [id]/
└── page.tsx
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/Modal';
export default function PhotoModal({ params }: { params: { id: string } }) {
return (
<Modal>
<img src={`/photos/${params.id}.jpg`} alt="Photo" />
</Modal>
);
}
9. 실전 예제: 블로그
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 60 },
});
if (!res.ok) {
return null;
}
return res.json();
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
if (!post) {
return {};
}
return {
title: post.title,
description: post.excerpt,
};
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
정리 및 체크리스트
핵심 요약
- App Router: Next.js 13+의 새로운 라우팅
- Server Components: 서버에서 렌더링
- Streaming: 점진적 렌더링
- Server Actions: Form 처리 간소화
- Parallel Routes: 병렬 렌더링
- Intercepting Routes: 모달 구현
구현 체크리스트
- App Router 프로젝트 생성
- Server Components 이해
- Loading/Error 처리
- Server Actions 구현
- Parallel Routes 활용
- 메타데이터 최적화
- 배포
같이 보면 좋은 글
- React 18 심화 가이드
- Remix 완벽 가이드
- Prisma 완벽 가이드
이 글에서 다루는 키워드
Next.js, App Router, React, Server Components, Streaming, SSR, Full Stack
자주 묻는 질문 (FAQ)
Q. Pages Router에서 마이그레이션해야 하나요?
A. 새 프로젝트는 App Router를 권장합니다. 기존 프로젝트는 점진적 마이그레이션이 가능합니다.
Q. Server Components는 항상 사용해야 하나요?
A. 기본적으로 Server Components를 사용하고, 상호작용이 필요한 경우만 Client Components를 사용하세요.
Q. 성능이 더 좋아지나요?
A. 네, Server Components로 JavaScript 번들이 줄어들고 초기 로딩이 빨라집니다.
Q. 학습 곡선이 가파른가요?
A. Server/Client Components 구분이 처음엔 헷갈릴 수 있지만, 익숙해지면 더 직관적입니다.