useFormState - 서버의 응답

452025년 09월 29일4

useFormStatus는 폼이 제출되는 ‘과정’의 상태를 우아하게 처리했다. 이제 남은 과제는 그 과정이 끝난 ‘후’, 즉 서버 액션의 ‘결과’를 어떻게 클라이언트에 전달하고 반영할 것인가 하는 문제였다. 예를 들어, 사용자가 제출한 폼 데이터에 유효성 검사 오류가 있거나, 비밀번호가 틀렸다는 서버의 피드백을 어떻게 보여줄 것인가?

이 문제를 해결하기 위해, 팀은 useFormStatus의 형제 격인 새로운 훅, useFormState를 설계했다.

useFormStateuseFormStatus보다 한 단계 더 나아간 기능을 제공했다. 이 훅은 서버 액션과 함께 호출되며, 액션의 실행 결과에 따라 업데이트되는 상태를 반환한다.

로렌 탄은 회원가입 폼을 예시로 useFormState의 동작 원리를 설명했다.

먼저, 서버 액션을 수정해야 했다. 기존의 서버 액션은 단순히 작업을 수행할 뿐, 어떤 값도 반환하지 않았다. 하지만 useFormState와 함께 사용될 서버 액션은 두 개의 인자, 즉 ‘이전 상태’와 ‘폼 데이터’를 받고, 새로운 ‘상태’를 반환하도록 약속되어야 했다.

// actions.js
'use server';

export async function signup(previousState, formData) {
  const email = formData.get('email');
  if (!email.includes('@')) {
    return { message: '유효한 이메일 주소를 입력해주세요.' }; // 새로운 상태 반환
  }
  
  // ... 유저 생성 로직 ...
  
  return { message: '회원가입 성공!' }; // 새로운 상태 반환
}

이제 클라이언트 컴포넌트에서 useFormState를 사용해 이 액션과 상태를 연결할 수 있었다.

// SignupForm.js
'use client';

import { useFormState } from 'react-dom';
import { signup } from './actions';

const initialState = { message: null };

function SignupForm() {
  const [state, formAction] = useFormState(signup, initialState);

  return (
    <form action={formAction}>
      {/* ... inputs ... */}
      <SubmitButton />
      {state?.message && <p>{state.message}</p>}
    </form>
  );
}

코드는 처음 보면 조금 생소했지만, 그 안에는 놀라운 효율성이 숨어 있었다.

  1. useFormState는 두 개의 인자를 받는다: 실행할 서버 액션(signup)과 초기 상태(initialState).
  2. 그리고 두 개의 값을 반환한다: 현재의 폼 상태(state)와, <form>action 속성에 전달할 새로운 래핑된 액션(formAction).
  3. 사용자가 폼을 제출하면, React는 formAction을 실행한다. 이때 내부적으로 실제 signup 액션을 호출하면서, 현재 상태(state)와 폼 데이터를 인자로 넘겨준다.
  4. 서버에서 signup 액션이 실행되고, 로직에 따라 새로운 상태 객체(성공 또는 에러 메시지가 담긴)를 반환한다.
  5. React는 서버로부터 반환된 이 새로운 상태를 받아, state 변수를 업데이트하고 컴포넌트를 리렌더링한다.

결과적으로, state.message는 서버 액션의 실행 결과에 따라 동적으로 바뀌었고, 화면에는 ‘유효한 이메일 주소를 입력해주세요.’ 또는 ‘회원가입 성공!’ 같은 메시지가 나타났다.

이것은 서버와 클라이언트 간의 상태 동기화를 위한 완벽한 폐쇄 루프(Closed Loop)였다.

개발자는 더 이상 fetch 요청을 보내고, 그 응답을 받아 useState로 상태를 업데이트하는 수동적인 과정을 거칠 필요가 없었다. useFormState가 그 모든 비동기적인 피드백 흐름을 하나의 훅 안에 완벽하게 캡슐화해주었다.

서버는 자신의 처리 결과를 ‘상태’라는 형태로 이야기하고, 클라이언트는 그 상태를 받아 화면에 그리기만 하면 됐다. useFormState는 마치 서버로부터 온 편지를 뜯어 그 내용을 상태로 바꿔주는 충실한 우편배달부와도 같았다. 이로써 React Actions는 단순한 원격 함수 호출을 넘어, 서버와 클라이언트가 상태를 통해 대화하는 양방향 통신 채널로 진화하게 되었다.