서버 액션의 초기 프로토타입은 폼 제출 로직을 획기적으로 단순화했지만, 그것만으로는 완벽하지 않았다. 개발자들은 곧 폼이 제출되는 동안의 UI 상태(예: 제출 버튼 비활성화)를 관리해야 한다는 현실적인 문제에 부딪혔다. 그리고 그 문제를 해결하기 위해, 다시 useState와 커스텀 onSubmit 핸들러를 도입해야 했다.
// 초기 액션 모델의 한계
function PostForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
async function handleSubmit(formData) {
setIsSubmitting(true);
await createPost(formData);
setIsSubmitting(false);
}
return (
<form action={handleSubmit}>
{/* ... */}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '...' : 'Submit'}
</button>
</form>
);
}
“이건 여전히 불편합니다.”
이 코드를 리뷰하던 조쉬 스토리가 말했다. 그의 표정에는 불만이 가득했다.
“우리는 <form action={serverAction}>이라는 우아한 패턴을 만들었는데, 고작 제출 버튼 하나 때문에 이 모든 보일러플레이트를 다시 가져와야 한다니요. 이건 우리가 원했던 경험이 아닙니다.”
그의 지적에 팀원들은 모두 공감했다. 폼 제출 상태는 폼이라는 컨텍스트에 종속적인 정보여야 했다. 그것을 컴포넌트의 로컬 상태로 관리하는 것은 관심사의 분리 원칙에도 어긋났다.
“문제는 정보의 흐름입니다.” 조쉬가 말을 이었다. “isSubmitting이라는 정보는 PostForm이 아니라, PostForm 안에 있는 button에게 필요한 정보입니다. 그런데 지금은 button이 그 정보를 알 방법이 없으니, 부모인 PostForm이 상태를 만들어서 props로 내려줘야 하는 구조죠.”
“만약….”
그의 눈빛이 바뀌었다.
“만약 자식이 부모의 상태를 props 없이도 ‘감지’할 수 있다면 어떨까요? 마치 와이파이 신호를 잡는 것처럼요.”
그의 아이디어는 Context API의 개념과 맞닿아 있었다. 하지만 개발자가 직접 Provider와 Consumer를 설정하는 번거로운 방식이 아니어야 했다. <form> 태그 자체가 암묵적인 Provider 역할을 하고, 그 상태를 읽고 싶어 하는 자식 컴포넌트가 간단한 훅(Hook) 하나로 그 상태를 ‘구독’할 수 있어야 했다.
이것이 바로 useFormStatus의 발견으로 이어진 결정적인 순간이었다.
useFormStatus는 ‘자식이 부모 폼의 상태를 어떻게 알 수 있는가?’라는 질문에 대한 React의 대답이었다.
<form>이 서버 액션을 실행하면, React는 내부적으로 해당 폼의 상태를 ‘pending’으로 설정하고, 이 상태를 암묵적인 컨텍스트를 통해 하위 트리로 전파한다.<form>의 자손인SubmitButton컴포넌트가useFormStatus()훅을 호출한다.- 이 훅은 컨텍스트를 거슬러 올라가 가장 가까운 부모
<form>을 찾고, 그 폼의 현재 상태(pending: true)를 읽어와 반환한다. - 액션이 끝나면,
<form>의 상태는 ‘idle’로 바뀌고,useFormStatus가 반환하는pending값도false로 변경되어SubmitButton이 리렌더링된다.
이 메커니즘을 통해, 제출 버튼은 더 이상 부모로부터 disabled 상태를 props로 전달받을 필요가 없어졌다. 스스로 부모의 상태를 감지하고, 자신의 모습을 결정할 수 있는 자율성을 얻게 된 것이다.
useFormStatus의 발견은 단순한 훅 하나를 추가한 것이 아니었다. 그것은 컴포넌트 간의 데이터 흐름에 대한 새로운 관점을 제시한 것이었다. 부모가 자식에게 일방적으로 데이터를 내려주는 하향식 흐름뿐만 아니라, 자식이 부모의 컨텍스트를 능동적으로 구독하는 상향식 ‘감지’의 가능성을 열어준 것이다. 이 작은 훅 하나로, React의 폼 처리는 비로소 진정한 우아함을 갖추게 되었다.


