useActionState로 폼 상태를 다루는 법
검수 김보성약 3분7 / 8
폼 하나에 왜 이렇게 상태가 많았는가
서버 액션이 나온 뒤로 폼 코드는 대체로 이런 모양이었습니다. 제출 중인지 보여주려고 isPending 상태를 직접 관리하고, 에러 메시지를 보여주려고 error 상태를 따로 두고, 액션 함수 안에서 try/catch로 그 상태를 갱신하고. 셋 다 같은 제출 흐름에 붙어 있는데 코드는 세 군데로 흩어져 있었습니다.
// useActionState 이전
"use client";
import { useState, useTransition } from "react";
function SignupForm() {
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
function handleSubmit(formData: FormData) {
startTransition(async () => {
const result = await signup(formData);
if (result?.error) setError(result.error);
});
}
return (
<form action={handleSubmit}>
<input name="email" />
<button disabled={isPending}>가입</button>
{error && <p>{error}</p>}
</form>
);
}useActionState로 한 데 모으기
useActionState는 이 세 가지(제출 함수, 마지막 결과값, 진행 중 여부)를 하나의 훅으로 묶습니다.
"use client";
import { useActionState } from "react";
import { signup } from "@/app/actions";
const initialState = { error: null };
function SignupForm() {
const [state, formAction, isPending] = useActionState(signup, initialState);
return (
<form action={formAction}>
<input name="email" />
<button disabled={isPending}>가입</button>
{state.error && <p>{state.error}</p>}
</form>
);
}signup 서버 액션은 이제 (이전 상태, formData)를 받아 다음 상태를 반환하는 형태여야 합니다. useState + useTransition + 수동 에러 핸들링 조합이 하던 일을 프레임워크가 대신 관리해줍니다. 코드 줄 수가 줄어드는 것도 있지만, 더 중요한 건 "제출 상태"라는 하나의 개념이 코드에서도 하나로 묶여 있다는 점입니다.
안 써도 되는 경우
모든 폼에 useActionState가 필요한 건 아닙니다. 서버 액션과 연결되지 않은, 순수 클라이언트 검증만 하는 폼(예: 클라이언트 상태만 바꾸는 필터 UI)이라면 이 훅을 쓸 이유가 없습니다. useActionState의 가치는 "서버 액션의 제출 상태를 관리한다"는 데 있지, 모든 폼 상태 관리의 정답이라는 데 있지 않습니다. 서버로 아무것도 보내지 않는 폼에 억지로 끼워 맞추면 오히려 useState 하나면 될 걸 복잡하게 만드는 꼴입니다.