본문 바로가기
web

Next.js 15 App Router 완벽 가이드 2026 — Server Components·Server Actions·Partial Prerendering으로 풀스택 개발 완전 정복

by bamsik 2026. 3. 13.
반응형

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년 웹 개발의 표준입니다.


📎 참고 자료

반응형