액션 처리 중 상태 관리

432025년 09월 27일3

서버 액션과 <form>의 결합은 폼 제출 로직을 극적으로 단순화시켰다. 하지만 곧 새로운 과제가 수면 위로 떠올랐다. 바로 폼이 제출되는 ‘동안’의 상태를 어떻게 관리할 것인가 하는 문제였다.

사용자가 ‘글쓰기’ 버튼을 누르면, 서버 액션이 실행되고 서버로부터 응답이 오기까지는 약간의 시간이 걸린다. 그 시간 동안 사용자에게 어떤 피드백을 주어야 할까?

“가장 기본적인 것은 제출 버튼을 비활성화해서 중복 제출을 막는 것입니다.”

로렌 탄이 말했다. “그리고 사용자에게 지금 무언가 처리 중이라는 것을 알려주기 위해, 버튼 텍스트를 ‘저장 중…’으로 바꾸거나 로딩 스피너를 보여줘야 합니다.”

이것은 모든 웹 애플리케이션에서 반드시 필요한 UI 패턴이었다. 하지만 서버 액션 모델에서는 이 간단한 작업을 구현하기가 까다로웠다.

// PostForm.client.js
function PostForm() {
  const [isSubmitting, setIsSubmitting] = useState(false); // <--- 이런 상태가 필요하다!

  const handleSubmit = async (formData) => {
    setIsSubmitting(true);
    await createPost(formData);
    setIsSubmitting(false);
  };

  return (
    <form action={handleSubmit}>
      {/* ... inputs ... */}
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '저장 중...' : '글쓰기'}
      </button>
    </form>
  );
}

이 코드는 동작은 하지만, <form action={createPost}>의 간결함을 포기하고 다시 onSubmit 핸들러와 useState를 도입해야만 했다. 이것은 명백한 퇴보였다.

“폼의 제출 상태(pending)는 폼 자기 자신과 그 자식들만 알면 되는 정보입니다. 이걸 컴포넌트의 최상단 상태로 끌어올리는 것은 좋은 설계가 아니에요.” 앤드류가 지적했다.

팀은 이 문제를 해결하기 위한 더 ‘React스러운’ 방법을 찾기 시작했다. 해답은 훅(Hook)에 있었다. 만약 폼의 자식 컴포넌트가 부모인 <form>의 현재 상태를 ‘읽을’ 수만 있다면?

이 아이디어를 바탕으로 새로운 훅들이 탄생하기 시작했다.

첫 번째 훅은 오직 폼의 ‘제출 중’ 상태, 즉 ‘pending’ 상태를 알려주는 데 특화된 훅이었다. 이 훅은 <form> 태그의 자손 컴포넌트 어디에서든 호출할 수 있으며, 가장 가까운 부모 <form>이 현재 서버 액션을 처리 중인지 여부를 boolean 값으로 반환한다.

두 번째 훅은 한 단계 더 나아갔다. 서버 액션이 끝난 후, 그 ‘결과’(예: 성공 메시지, 또는 유효성 검사 실패로 인한 에러 메시지)를 받아와 상태를 업데이트하는 역할까지 책임지는 훅이었다.

이 새로운 훅들이 있다면, 개발자는 더 이상 폼 제출 상태를 관리하기 위해 직접 useState를 선언하고 복잡한 핸들러 함수를 작성할 필요가 없었다.

  • 제출 버튼 컴포넌트는: 첫 번째 훅을 사용해 자신이 비활성화되어야 하는지, 로딩 스피너를 보여줘야 하는지를 스스로 판단할 수 있다.
  • 폼 컴포넌트는: 두 번째 훅을 사용해 서버로부터 받은 에러 메시지를 화면에 보여줄 수 있다.

이 모든 상태 관리가 <form>이라는 컨텍스트(Context) 안에서 자연스럽게 이루어지는 것이다.

useFormStatususeFormState.

이 두 개의 훅은 서버 액션이라는 강력한 무기에, 사용자 경험을 위한 섬세한 제어 능력을 부여해 줄 마지막 퍼즐 조각이었다. React 19는 이제 데이터 제출의 시작부터 끝, 그리고 그 과정의 모든 순간까지 책임질 준비를 하고 있었다. 개발자들은 곧 이 우아한 상태 관리의 세계를 만나게 될 터였다.