무엇
React의 <form>은 브라우저의 폼 요소를 확장해, action 프로퍼티로 함수까지 받을 수 있게 한다. 문자열 URL을 주면 평범한 HTML 폼처럼 굴고, 함수를 주면 제출을 자바스크립트로 가로채 그 함수에게 넘긴다.
action에 함수를 넘기면 폼이 제출될 때 React가 그 함수를 부르고, 폼 필드를 담은 FormData 객체를 인자로 준다.
async function search(formData: FormData) {
const query = formData.get('query')
// ...
}
<form action={search}>
<input name="query" />
<button type="submit">검색</button>
</form>
무슨 일이 일어나나
action이 함수면 제출은 트랜지션 안에서 실행된다. 그래서 event.preventDefault()를 직접 부를 필요가 없고, 함수가 비동기여도 되며, 실행 중에는 폼이 pending 상태가 된다.
함수에 들어오는 FormData에는 name 속성이 있는 필드들이 담긴다. 값은 상태가 아니라 DOM에서 읽어오기 때문에, 필드를 굳이 controlled로 만들지 않아도 된다. 제출이 성공으로 끝나면 React가 uncontrolled 필드를 자동으로 초기화한다. controlled 필드(값을 상태로 쥔 입력)는 자동으로 비워지지 않으므로 직접 되돌려야 한다.
함수 action을 쓰면 HTTP 메서드는 method 프로퍼티와 무관하게 항상 POST다. 함수 안에서 던진 에러는 폼이 삼키지 않고 가장 가까운 에러 바운더리로 올라간다.
사용법
제출 상태는 useFormStatus로 읽는다. 이 훅은 자신을 감싼 <form>의 pending 여부를 돌려주므로, 반드시 폼 안쪽 컴포넌트에서 불러야 한다. 폼 자신에서는 읽을 수 없다.
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? '보내는 중…' : '보내기'}
</button>
)
}
export function MessageForm({ action }: { action: (fd: FormData) => Promise<void> }) {
return (
<form action={action}>
<input name="message" />
<SubmitButton />
</form>
)
}
버튼마다 다른 동작이 필요하면 formAction을 쓴다. <button>·<input type="submit">·<input type="image">에 formAction을 걸면, 그 버튼으로 제출할 때 폼의 action 대신 그 함수가 불린다.
<form action={publish}>
<textarea name="content" />
<button type="submit">발행</button>
<button formAction={saveDraft}>임시 저장</button>
</form>
실무 예시
에러 메시지까지 다루려면 action을 useActionState로 감싼다. 이 훅은 (이전상태, formData) => 다음상태 형태의 함수를 받아, 반환값을 다음 렌더의 상태로 넘겨준다. 검증 실패 메시지를 그대로 화면에 뿌리기 좋다.
'use client'
import { useActionState } from 'react'
import { signUp } from './api'
export function SignupForm() {
const [error, formAction] = useActionState(
async (_prev: string | null, formData: FormData) => {
const email = formData.get('email')
try {
await signUp(String(email))
return null
} catch (e) {
return e instanceof Error ? e.message : '가입 실패'
}
},
null,
)
return (
<form action={formAction}>
<input name="email" placeholder="you@example.com" />
<button type="submit">가입</button>
{error && <p role="alert">{error}</p>}
</form>
)
}
왜 중요한가
React 19의 폼 처리는 action을 축으로 다시 짜였다. 제출을 트랜지션으로 돌리고, useFormStatus로 pending을, useActionState로 결과·에러를, useOptimistic으로 낙관적 갱신을 잡는 한 세트가 여기서 맞물린다. onSubmit 핸들러를 직접 쓰던 방식도 여전히 유효하지만, 서버 함수·점진적 향상·pending 추적을 공짜로 얻으려면 함수 action이 기본 선택지가 된다. 폼을 자주 만드는 앱일수록 이 묶음을 손에 익혀 두는 값이 크다.
Reference