
Next.js 15 App Router란 무엇인가?
2026년 현재, Next.js 15는 React 생태계의 표준 풀스택 프레임워크로 자리잡았습니다. App Router는 기존 Pages Router를 대체하는 새로운 라우팅 시스템으로, React Server Components(RSC)를 기반으로 서버와 클라이언트를 자연스럽게 통합합니다. 이 가이드에서는 App Router의 핵심 개념부터 Server Actions, Partial Prerendering까지 실전 중심으로 완전히 정복합니다.

1. App Router 핵심 구조 이해하기
Next.js 15 App Router는 app/ 디렉토리를 기반으로 작동합니다. 기존 pages/ 디렉토리와 공존할 수 있지만, 새 프로젝트라면 처음부터 App Router를 사용하는 것을 강력히 권장합니다.

기본 파일 구조
App Router에서는 다음 특수 파일들이 각각의 역할을 담당합니다:
- page.tsx — 해당 라우트의 UI 컴포넌트 (공개적으로 접근 가능)
- layout.tsx — 여러 페이지에 공유되는 레이아웃 (중첩 가능)
- loading.tsx — Suspense 기반 로딩 UI
- error.tsx — 에러 바운더리 (Client Component 필수)
- not-found.tsx — 404 페이지 커스터마이즈
- route.ts — API 엔드포인트 (App Router 방식의 API Route)
app/
├── layout.tsx ← 루트 레이아웃 (필수)
├── page.tsx ← 홈 페이지 (/)
├── about/
│ └── page.tsx ← /about
├── blog/
│ ├── page.tsx ← /blog (목록)
│ ├── [slug]/
│ │ └── page.tsx ← /blog/:slug (상세)
│ └── loading.tsx ← /blog 로딩 UI
└── api/
└── users/
└── route.ts ← /api/users

2. React Server Components (RSC) 완전 정복
App Router의 가장 강력한 기능은 React Server Components입니다. RSC는 서버에서만 실행되므로 클라이언트에 JavaScript 번들을 전송하지 않으며, 데이터베이스·파일시스템에 직접 접근할 수 있습니다.
Server Component vs Client Component
App Router에서 모든 컴포넌트는 기본적으로 Server Component입니다. "use client" 디렉티브를 파일 최상단에 추가하면 Client Component가 됩니다.
// ServerComponent.tsx (기본 - 서버에서 실행)
async function ProductList() {
// 데이터베이스 직접 쿼리 가능!
const products = await db.query('SELECT * FROM products');
return (
<ul>
{products.map(p => (
<li key={p.id}>{p.name} - {p.price}원</li>
))}
</ul>
);
}
// ClientComponent.tsx (인터랙션 필요 시)
"use client";
import { useState } from 'react';
function AddToCartButton({ productId }: { productId: string }) {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
// 클라이언트 사이드 로직
await addToCart(productId);
setLoading(false);
};
return (
<button onClick={handleClick} disabled={loading}>
{loading ? '추가 중...' : '장바구니 담기'}
</button>
);
}
Server Component의 장점
- ✅ 클라이언트 번들 크기 대폭 감소 (라이브러리 코드가 서버에만 존재)
- ✅ 데이터베이스·파일시스템 직접 접근 (API Route 불필요)
- ✅ 민감한 정보(API 키, 비밀번호)를 클라이언트에 노출하지 않음
- ✅ 자동 코드 분할 및 스트리밍 지원
3. Server Actions — 폼 처리의 혁명
Server Actions는 클라이언트에서 서버 함수를 직접 호출할 수 있는 기능입니다. 기존에는 폼 제출 시 API Route를 만들고 fetch로 호출해야 했지만, Server Actions로 이 과정이 대폭 단순화됩니다.
// app/contact/actions.ts
"use server";
import { revalidatePath } from 'next/cache';
export async function submitContact(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
const message = formData.get('message') as string;
// 유효성 검사
if (!name || !email || !message) {
return { error: '모든 필드를 입력해주세요.' };
}
// 데이터베이스 저장
await db.insert('contacts', { name, email, message, createdAt: new Date() });
// 이메일 발송 (서버에서만 실행 → 안전)
await sendEmail({ to: 'admin@example.com', subject: `새 문의: ${name}` });
// 캐시 무효화
revalidatePath('/admin/contacts');
return { success: true };
}
// app/contact/page.tsx (Server Component에서 직접 사용)
import { submitContact } from './actions';
export default function ContactPage() {
return (
<form action={submitContact}>
<input name="name" placeholder="이름" required />
<input name="email" type="email" placeholder="이메일" required />
<textarea name="message" placeholder="메시지" required />
<button type="submit">전송</button>
</form>
);
}
useActionState로 서버 응답 처리
"use client";
import { useActionState } from 'react';
import { submitContact } from './actions';
export function ContactForm() {
const [state, formAction, isPending] = useActionState(submitContact, null);
return (
<form action={formAction}>
{state?.error && <p className="error">{state.error}</p>}
{state?.success && <p className="success">전송 완료!</p>}
<input name="name" placeholder="이름" required />
<input name="email" type="email" placeholder="이메일" required />
<textarea name="message" placeholder="메시지" required />
<button type="submit" disabled={isPending}>
{isPending ? '전송 중...' : '전송'}
</button>
</form>
);
}
4. Partial Prerendering (PPR) — 하이브리드 렌더링
Next.js 15의 가장 혁신적인 기능 중 하나는 Partial Prerendering(PPR)입니다. PPR은 하나의 페이지에서 정적 부분과 동적 부분을 동시에 제공하는 하이브리드 렌더링 방식입니다.
PPR 작동 원리
- 정적 쉘(Static Shell): 헤더, 네비게이션, 레이아웃 등 → 즉시 제공 (CDN 캐시)
- 동적 홀(Dynamic Holes): 개인화된 콘텐츠, 실시간 데이터 → Suspense로 스트리밍
// next.config.ts - PPR 활성화
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
ppr: 'incremental', // 점진적 도입 가능
},
};
export default nextConfig;
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { StaticHeader } from '@/components/StaticHeader';
import { DynamicFeed } from '@/components/DynamicFeed';
import { UserStats } from '@/components/UserStats';
// PPR 활성화 (이 페이지만)
export const experimental_ppr = true;
export default function Dashboard() {
return (
<main>
{/* 정적으로 즉시 제공 */}
<StaticHeader />
{/* 동적 데이터는 Suspense로 스트리밍 */}
<Suspense fallback={<StatsLoading />}>
<UserStats />
</Suspense>
<Suspense fallback={<FeedLoading />}>
<DynamicFeed />
</Suspense>
</main>
);
}
5. 데이터 페칭 패턴 모범 사례
Next.js 15에서는 데이터 페칭이 크게 단순화되었습니다. Server Component에서 직접 async/await를 사용하면 됩니다.
병렬 데이터 페칭 (성능 최적화)
// ❌ 순차 페칭 (느림)
async function Page({ params }: { params: { id: string } }) {
const user = await getUser(params.id); // 100ms
const posts = await getUserPosts(params.id); // 100ms (user 이후)
// 총 200ms
}
// ✅ 병렬 페칭 (빠름)
async function Page({ params }: { params: { id: string } }) {
const [user, posts] = await Promise.all([
getUser(params.id), // 동시에 시작
getUserPosts(params.id), // 동시에 시작
]);
// 총 100ms
}
캐싱 전략
// 정적 캐싱 (기본값 - 빌드 시 한번만 페칭)
const data = await fetch('https://api.example.com/data');
// 시간 기반 재검증 (60초마다 갱신)
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
});
// 태그 기반 재검증 (필요할 때만 갱신)
const data = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
});
// Server Action에서 태그 무효화
import { revalidateTag } from 'next/cache';
await revalidateTag('posts'); // 'posts' 태그 캐시 즉시 무효화
// 캐싱 비활성화 (매 요청마다 새로 페칭)
const data = await fetch('https://api.example.com/realtime', {
cache: 'no-store'
});
6. 미들웨어로 인증 처리
Next.js 15에서 미들웨어는 Edge Runtime에서 실행되어 모든 요청을 인터셉트할 수 있습니다. 인증, A/B 테스팅, 지역화 등에 활용합니다.
// middleware.ts (프로젝트 루트)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value;
// 보호된 경로 체크
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
// 로그인 페이지에서 이미 로그인 시 대시보드로 리다이렉트
if (request.nextUrl.pathname === '/login' && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
};
7. Image & Font 최적화
Next.js 15는 이미지와 폰트 최적화를 내장하고 있어 별도 설정 없이도 뛰어난 성능을 제공합니다.
import Image from 'next/image';
import { Inter, Noto_Sans_KR } from 'next/font/google';
// 폰트 자동 최적화 (FOUT 방지, 개인정보 보호)
const inter = Inter({ subsets: ['latin'] });
const notoSansKR = Noto_Sans_KR({
subsets: ['latin'],
weight: ['400', '700'],
display: 'swap',
});
// 이미지 최적화
function HeroSection() {
return (
<section>
<Image
src="/hero.jpg"
alt="히어로 이미지"
width={1200}
height={600}
priority // LCP 이미지는 priority 추가
placeholder="blur" // 로딩 중 블러 처리
/>
</section>
);
}
Next.js 15 vs 14: 주요 변경 사항
| 기능 | Next.js 14 | Next.js 15 |
|---|---|---|
| React 버전 | React 18 | React 19 지원 |
| 캐싱 기본값 | 공격적 캐싱 | opt-in 캐싱 (더 예측 가능) |
| params | 동기 접근 | 비동기 접근 (Promise) |
| Turbopack | 실험적 | dev 기본값으로 안정화 |
| PPR | 미지원 | 점진적 도입 가능 |
실전 프로젝트 시작하기
# Next.js 15 프로젝트 생성
npx create-next-app@latest my-app \
--typescript \
--tailwind \
--eslint \
--app \ # App Router 선택
--src-dir \ # src/ 디렉토리 사용
--import-alias "@/*"
cd my-app
npm run dev
Next.js 15 App Router는 단순한 버전 업그레이드가 아닙니다. 서버-클라이언트 경계를 새롭게 정의하고, 풀스택 개발을 하나의 코드베이스에서 가능하게 합니다. Server Components로 번들 크기를 줄이고, Server Actions로 API 보일러플레이트를 제거하며, PPR로 성능과 동적 콘텐츠를 동시에 잡는 — 이것이 2026년 웹 개발의 표준입니다.