Next.js 14와 Server Actions 도입 후기: 실제 프로덕션 경험 공유

2025년 초, 실제 프로덕션 환경에 Next.js 14를 도입하면서 얻은 경험을 공유합니다. 이번 마이그레이션의 핵심은 Server ActionsApp Router 전환이었으며, 결과적으로 성능과 개발 생산성 모두에서 긍정적인 변화를 확인했습니다.

왜 Next.js 14로 업그레이드했나?

기존 Next.js 12 기반 프로젝트의 문제점:

  • 클라이언트 번들 사이즈가 점점 비대해짐 (1.2MB+)
  • API Routes 관리가 복잡해짐 (50개 이상의 엔드포인트)
  • 초기 로딩 속도 저하 (LCP 3.5초)
  • 코드 중복: 프론트엔드/백엔드 유효성 검사 로직

Server Actions의 핵심 장점

1. API Routes 불필요

기존에는 간단한 폼 처리도 API 엔드포인트가 필요했습니다:

TYPESCRIPT
// 기존 방식 (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)
  });
};
TYPESCRIPT
// 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 타입 안정성

TYPESCRIPT
// 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 없이도 동작합니다:

TSX
<form action={submitContact}>
  {/* JS가 로드되지 않아도 폼 제출 가능 */}
  {/* JS가 로드되면 자동으로 AJAX로 전환 */}
</form>

Server Components 활용

데이터 페칭 간소화

TYPESCRIPT
// 기존: 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

TSX
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <h1>대시보드</h1>

      {/* 빠르게 로드되는 부분 */}
      <UserInfo />

      {/* 느린 데이터는 스트리밍 */}
      <Suspense fallback={<ChartSkeleton />}>
        <AnalyticsChart />
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  );
}

페이지 전체를 기다리지 않고 점진적으로 콘텐츠를 표시할 수 있습니다.

마이그레이션 과정

단계적 전환 전략

  1. 신규 페이지: App Router로 개발
  2. 공존 기간: pages/와 app/ 병행 운영
  3. 점진적 이전: 트래픽 낮은 페이지부터 전환
  4. 완전 전환: pages/ 폴더 제거
JS
// next.config.js - 병행 운영 설정
module.exports = {
  experimental: {
    // Pages Router와 App Router 동시 사용 허용
  }
};

성과 측정

지표이전 (Next 12)이후 (Next 14)개선
번들 사이즈1.2 MB720 KB-40%
LCP3.5초1.8초-49%
TTFB800ms250ms-69%
API 엔드포인트50개0개-100%
빌드 시간180초95초-47%

주의사항과 팁

  • 클라이언트 컴포넌트 최소화: 'use client'는 정말 필요한 곳에만
  • 캐싱 전략 이해: fetch의 기본 캐싱 동작 숙지 필요
  • 에러 바운더리: error.tsx로 에러 처리 구조화
  • 로딩 상태: loading.tsx로 일관된 UX 제공

Next.js 14는 단순한 업그레이드가 아니라 웹 개발 방식의 패러다임 전환입니다. Server Actions와 Server Components를 적극 활용하면 더 빠르고, 더 단순하고, 더 안전한 애플리케이션을 만들 수 있습니다.