새로운 훅에 대한 아이디어가 구체화되면서, 팀은 가장 먼저 폼의 ‘제출 중(pending)’ 상태를 다루는 훅의 개발에 착수했다. 이 훅의 이름은 그 역할을 명확히 드러내는 useFormStatus로 정해졌다.
useFormStatus의 설계 원칙은 단순했다.
- 이 훅은 반드시
<form>컴포넌트의 자손 컴포넌트에서만 사용되어야 한다. - 이 훅은 가장 가까운 부모
<form>의 상태에 자동으로 연결된다. - 이 훅은 폼의 상태에 대한 읽기 전용(read-only) 정보를 제공한다.
조쉬 스토리는 이 훅의 실제 사용 사례를 보여주기 위해, 재사용 가능한 SubmitButton 컴포넌트를 만들었다.
// SubmitButton.js
'use client';
import { useFormStatus } from 'react-dom'; // react-dom에서 임포트
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '저장 중...' : '저장하기'}
</button>
);
}
코드를 본 팀원들은 그 간결함과 명확성에 감탄했다. SubmitButton 컴포넌트는 자신이 어떤 폼 안에 들어갈지, 어떤 서버 액션을 실행할지 전혀 알지 못했다. 그저 useFormStatus 훅을 호출하여 pending이라는 boolean 값 하나를 얻어올 뿐이었다.
- 부모 폼이 제출을 시작하면,
pending값은true가 된다. - 부모 폼의 액션이 끝나면,
pending값은false가 된다.
이 SubmitButton을 실제 폼 안에 넣어보자, 마법이 일어났다.
// PostForm.js
import { createPost } from './actions';
import { SubmitButton } from './SubmitButton';
function PostForm() {
return (
<form action={createPost}>
<input name="title" />
<textarea name="content" />
<SubmitButton />
</form>
);
}
PostForm 컴포넌트에는 상태 관리 로직이 단 한 줄도 없었다. 하지만 사용자가 폼을 제출하자, SubmitButton은 스스로를 비활성화하고 텍스트를 ‘저장 중…’으로 변경했다. 서버 액션이 끝나자, 다시 원래의 활성화 상태로 돌아왔다.
“이것 봐….” 한 엔지니어가 놀라움을 감추지 못했다. “SubmitButton에 어떤 props도 넘겨주지 않았는데, 어떻게 부모 폼의 상태를 알 수 있는 거죠?”
“컨텍스트(Context)입니다.” 앤드류가 설명했다. “<form action={...}>은 내부적으로 자신만의 상태 컨텍스트를 생성합니다. 그리고 useFormStatus 훅은 그 컨텍스트를 통해 상태를 읽어오는 거죠. 개발자가 직접 컨텍스트를 설정할 필요 없이, React가 모든 것을 알아서 처리해주는 겁니다.”
useFormStatus는 단순한 훅이 아니었다. 그것은 컴포넌트의 관심사를 완벽하게 분리하는 강력한 도구였다.
PostForm은: 어떤 데이터를 제출할 것인지, 어떤 액션을 실행할 것인지에만 관심을 가진다.SubmitButton은: 오직 폼이 제출 중인 ‘상태’에만 관심을 가진다.
더 이상 부모 컴포넌트가 자식의 UI 상태(disabled, loading text 등)를 제어하기 위해 isSubmitting 같은 상태를 props로 지저분하게 내려줄 필요가 없었다. 자식은 이제 스스로 부모의 목소리를 듣고, 자신의 모습을 결정할 수 있게 되었다.
useFormStatus는 폼에게 목소리를 부여했다. 그리고 그 목소리는 폼 안의 모든 자식 컴포넌트에게 전달되어, 조화롭고 일관된 사용자 경험을 만들어내는 교향곡의 시작을 알리고 있었다.


