React 19 Server Actions와 useTransition 실전 적용기
배경
사내 관리자 대시보드를 Next.js 15 기반으로 마이그레이션하면서 React 19의 Server Actions를 본격적으로 도입했다. 기존에는 폼 제출 시 API 라우트를 호출하고 클라이언트에서 로딩 상태를 관리했는데, Server Actions로 전환하면서 코드량이 눈에 띄게 줄었다.
기존 방식의 문제점
// 기존 코드
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setIsLoading(true);
try {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(formData)
});
// 에러 처리, 성공 처리...
} finally {
setIsLoading(false);
}
};
매번 로딩 상태를 직접 관리하고, API 라우트 파일을 별도로 만들어야 했다.
Server Actions 적용
// actions.ts
'use server';
export async function createUser(formData: FormData) {
const name = formData.get('name');
await db.user.create({ data: { name } });
revalidatePath('/users');
}
// component.tsx
'use client';
import { useTransition } from 'react';
import { createUser } from './actions';
export function UserForm() {
const [isPending, startTransition] = useTransition();
const handleSubmit = (formData: FormData) => {
startTransition(() => createUser(formData));
};
return (
<form action={handleSubmit}>
<input name="name" disabled={isPending} />
<button disabled={isPending}>
{isPending ? '생성 중...' : '생성'}
</button>
</form>
);
}
얻은 이점
- 코드 간소화: API 라우트 없이 서버 로직을 직접 작성할 수 있다
- 타입 안정성: 클라이언트-서버 간 타입이 자연스럽게 공유된다
- 자동 로딩 상태: useTransition의 isPending으로 로딩 처리가 간결해진다
- Revalidation 통합: revalidatePath로 캐시 갱신이 명확하다
주의할 점
- 민감한 로직은 서버에서 권한 검증 필수
- FormData 기반이라 복잡한 객체 전달 시 직렬화 고려
- 에러 처리는 try-catch와 useActionState 조합 권장
아직 러닝커브가 있지만, 폼 위주 화면에서는 확실히 생산성이 올라간다.