React 19 Server Actions를 실무에 도입하면서 겪은 것들

배경

React 19가 12월에 정식 출시되면서 Server Actions가 안정화됐다. Next.js 14에서 실험적으로 사용해본 적은 있었지만, 프로덕션 환경에 도입하기엔 이르다고 판단했었다. 하지만 정식 릴리즈된 만큼 새로 시작하는 어드민 프로젝트에 적용해보기로 했다.

기존 방식과의 비교

기존에는 form 제출을 위해 API 라우트를 만들고, 클라이언트에서 fetch를 호출하는 방식이었다.

// app/api/products/route.ts
export async function POST(request: Request) {
  const data = await request.json();
  const product = await db.product.create({ data });
  return Response.json(product);
}

// components/ProductForm.tsx
const handleSubmit = async (e) => {
  e.preventDefault();
  const res = await fetch('/api/products', {
    method: 'POST',
    body: JSON.stringify(formData)
  });
};

Server Actions를 사용하면 이렇게 바뀐다.

// app/actions/product.ts
'use server'

export async function createProduct(formData: FormData) {
  const data = {
    name: formData.get('name'),
    price: Number(formData.get('price'))
  };
  const product = await db.product.create({ data });
  revalidatePath('/products');
  return { success: true, product };
}

// components/ProductForm.tsx
import { createProduct } from '@/app/actions/product';

export function ProductForm() {
  return <form action={createProduct}>...</form>;
}

실제로 느낀 점

장점:

  • API 라우트 파일이 줄어들면서 코드 구조가 단순해졌다
  • 타입 안정성이 자연스럽게 유지된다 (서버-클라이언트 간 자동 추론)
  • Progressive Enhancement가 기본으로 작동한다

단점:

  • FormData 파싱이 생각보다 번거롭다. Zod 같은 스키마 검증 라이브러리가 필수다
  • 에러 처리가 명확하지 않다. try-catch를 어디서 할지 고민이 필요하다
  • 아직 생태계가 초기라 베스트 프랙티스가 정립되지 않았다

마이그레이션 전략

기존 프로젝트에 적용한다면 점진적으로 가는 게 맞다고 본다. 새로 만드는 form부터 Server Actions를 적용하고, 기존 API는 그대로 두는 방식이 안전했다.

useFormStatus 훅과 함께 사용하면 로딩 상태 관리도 깔끔해진다. 다만 아직은 러닝 커브가 있어서 팀 전체가 익숙해지기까진 시간이 필요할 것 같다.