useFormStatus - 폼의 목소리

442025년 09월 28일3

새로운 훅에 대한 아이디어가 구체화되면서, 팀은 가장 먼저 폼의 ‘제출 중(pending)’ 상태를 다루는 훅의 개발에 착수했다. 이 훅의 이름은 그 역할을 명확히 드러내는 useFormStatus로 정해졌다.

useFormStatus의 설계 원칙은 단순했다.

  1. 이 훅은 반드시 <form> 컴포넌트의 자손 컴포넌트에서만 사용되어야 한다.
  2. 이 훅은 가장 가까운 부모 <form>의 상태에 자동으로 연결된다.
  3. 이 훅은 폼의 상태에 대한 읽기 전용(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는 폼에게 목소리를 부여했다. 그리고 그 목소리는 폼 안의 모든 자식 컴포넌트에게 전달되어, 조화롭고 일관된 사용자 경험을 만들어내는 교향곡의 시작을 알리고 있었다.