React 19 Server Actions를 프로덕션에 도입하며 배운 것들
배경
팀 프로젝트에서 Next.js 15와 React 19를 도입하면서 Server Actions를 본격적으로 사용하기 시작했다. 기존 API Routes + client-side fetch 패턴에서 벗어나 form 중심의 서버 액션으로 전환했는데, 생각보다 고려할 점이 많았다.
useActionState로 폼 상태 관리
기존에는 useState + onSubmit 핸들러로 처리하던 것을 useActionState로 변경했다.
// actions.ts
'use server'
export async function updateProfile(prevState: any, formData: FormData) {
const name = formData.get('name') as string;
if (!name || name.length < 2) {
return { error: '이름은 2자 이상이어야 합니다' };
}
await db.user.update({ name });
return { success: true };
}
// ProfileForm.tsx
import { useActionState } from 'react';
function ProfileForm() {
const [state, formAction, isPending] = useActionState(updateProfile, null);
return (
<form action={formAction}>
<input name="name" disabled={isPending} />
{state?.error && <span>{state.error}</span>}
<button type="submit" disabled={isPending}>저장</button>
</form>
);
}
에러 핸들링 전략
Server Actions는 에러가 클라이언트로 직렬화되지 않는다. 처음에는 try-catch로 에러를 던졌다가 클라이언트에서 제대로 받지 못하는 문제가 있었다.
결국 모든 에러를 상태 객체로 반환하는 방식으로 통일했다. 예외 상황은 Sentry 같은 모니터링 도구로 따로 기록하고, 사용자에게는 명확한 메시지만 전달한다.
Progressive Enhancement
JS가 로드되기 전에도 폼이 동작해야 한다는 점이 신선했다. formAction prop만으로도 기본 submit이 작동하고, hydration 후 useActionState가 인터셉트하는 구조다.
실제로 느린 네트워크 환경 테스트에서 JS 로드 전에도 폼 제출이 가능했고, 이후 클라이언트 상태 업데이트가 자연스럽게 이어졌다.
마이그레이션 팁
- API Routes는 당장 제거하지 않았다. 외부 webhook이나 third-party 연동은 그대로 두고 내부 form만 먼저 전환
- 복잡한 비즈니스 로직은 별도 서비스 레이어로 분리. Server Actions는 얇은 어댑터 역할만
- 타입 안정성을 위해 Zod로 FormData 검증 레이어 추가
소감
초기 러닝 커브가 있지만, 폼 처리가 확실히 간결해졌다. 특히 낙관적 업데이트와 조합하면 UX가 크게 개선된다. 다만 아직 생태계가 완전히 정착되지 않아서 베스트 프랙티스를 찾아가는 중이다.