Next.js 14와 Server Actions 도입 후기: 실제 프로덕션 경험 공유
2025년 초, 실제 프로덕션 환경에 Next.js 14를 도입하면서 얻은 경험을 공유합니다. 이번 마이그레이션의 핵심은 Server Actions와 App Router 전환이었으며, 결과적으로 성능과 개발 생산성 모두에서 긍정적인 변화를 확인했습니다.
왜 Next.js 14로 업그레이드했나?
기존 Next.js 12 기반 프로젝트의 문제점:
- 클라이언트 번들 사이즈가 점점 비대해짐 (1.2MB+)
- API Routes 관리가 복잡해짐 (50개 이상의 엔드포인트)
- 초기 로딩 속도 저하 (LCP 3.5초)
- 코드 중복: 프론트엔드/백엔드 유효성 검사 로직
Server Actions의 핵심 장점
1. API Routes 불필요
기존에는 간단한 폼 처리도 API 엔드포인트가 필요했습니다:
// 기존 방식 (Pages Router + API Route)
// pages/api/contact.ts
export default async function handler(req, res) {
const { name, email, message } = req.body;
await db.contact.create({ name, email, message });
res.json({ success: true });
}
// pages/contact.tsx
const handleSubmit = async (e) => {
e.preventDefault();
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(formData)
});
};// Server Actions 방식 (App Router)
// app/contact/page.tsx
async function submitContact(formData: FormData) {
'use server';
const name = formData.get('name');
const email = formData.get('email');
const message = formData.get('message');
await db.contact.create({ name, email, message });
revalidatePath('/contact');
}
export default function ContactPage() {
return (
<form action={submitContact}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">전송</button>
</form>
);
}결과: API Routes 50개 → 0개로 감소. 코드가 직관적이고 유지보수가 쉬워졌습니다.
2. End-to-End 타입 안정성
// actions/user.ts
'use server';
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(0).max(150)
});
export async function createUser(formData: FormData) {
const validated = UserSchema.parse({
name: formData.get('name'),
email: formData.get('email'),
age: Number(formData.get('age'))
});
// validated의 타입이 자동으로 추론됨
return await db.user.create(validated);
}TypeScript와의 통합이 뛰어나, 런타임 에러를 줄이고 리팩토링 시에도 안전합니다.
3. Progressive Enhancement
Server Actions는 JavaScript 없이도 동작합니다:
<form action={submitContact}>
{/* JS가 로드되지 않아도 폼 제출 가능 */}
{/* JS가 로드되면 자동으로 AJAX로 전환 */}
</form>Server Components 활용
데이터 페칭 간소화
// 기존: getServerSideProps
export async function getServerSideProps() {
const products = await fetchProducts();
return { props: { products } };
}
// App Router: 컴포넌트에서 직접 fetch
async function ProductList() {
const products = await fetchProducts(); // 서버에서 실행
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}스트리밍과 Suspense
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
<h1>대시보드</h1>
{/* 빠르게 로드되는 부분 */}
<UserInfo />
{/* 느린 데이터는 스트리밍 */}
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
);
}페이지 전체를 기다리지 않고 점진적으로 콘텐츠를 표시할 수 있습니다.
마이그레이션 과정
단계적 전환 전략
- 신규 페이지: App Router로 개발
- 공존 기간: pages/와 app/ 병행 운영
- 점진적 이전: 트래픽 낮은 페이지부터 전환
- 완전 전환: pages/ 폴더 제거
// next.config.js - 병행 운영 설정
module.exports = {
experimental: {
// Pages Router와 App Router 동시 사용 허용
}
};성과 측정
| 지표 | 이전 (Next 12) | 이후 (Next 14) | 개선 |
|---|---|---|---|
| 번들 사이즈 | 1.2 MB | 720 KB | -40% |
| LCP | 3.5초 | 1.8초 | -49% |
| TTFB | 800ms | 250ms | -69% |
| API 엔드포인트 | 50개 | 0개 | -100% |
| 빌드 시간 | 180초 | 95초 | -47% |
주의사항과 팁
- 클라이언트 컴포넌트 최소화: 'use client'는 정말 필요한 곳에만
- 캐싱 전략 이해: fetch의 기본 캐싱 동작 숙지 필요
- 에러 바운더리: error.tsx로 에러 처리 구조화
- 로딩 상태: loading.tsx로 일관된 UX 제공
Next.js 14는 단순한 업그레이드가 아니라 웹 개발 방식의 패러다임 전환입니다. Server Actions와 Server Components를 적극 활용하면 더 빠르고, 더 단순하고, 더 안전한 애플리케이션을 만들 수 있습니다.