Before & After

462025년 09월 30일4

useFormStatususeFormState가 완성되면서, 서버 액션을 중심으로 한 새로운 폼 처리 패러다임의 모든 조각이 맞춰졌다. React Core Team은 이 변화가 가져온 혁신을 가장 명확하게 보여주기 위해, ‘이전’과 ‘이후’의 코드를 나란히 비교하는 문서를 작성하기 시작했다.

그 결과물은 한 편의 드라마와도 같았다.

[Before] 서버 액션이 없던 시절: 수많은 상태와 핸들러의 미로

화면에는 로그인 폼을 구현한 전형적인 React 18 이전 시대의 코드가 펼쳐졌다.

// LoginForm_Old.js
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export function LoginFormOld() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState(null);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const router = useRouter();

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    setError(null);

    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password }),
    });

    const data = await response.json();

    if (!response.ok) {
      setError(data.message);
      setIsSubmitting(false);
    } else {
      router.push('/dashboard');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '로그인 중...' : '로그인'}
      </button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </form>
  );
}

코드는 길고 복잡했다.

  • 입력값 관리를 위한 email, password 상태.
  • 에러 메시지 표시를 위한 error 상태.
  • 제출 중 UI 처리를 위한 isSubmitting 상태.
  • 총 4개의 useState가 필요했다.
  • 폼의 기본 동작을 막는 e.preventDefault().
  • 상태를 JSON으로 변환하고 fetch를 호출하는 수동적인 API 통신 로직.
  • 응답 결과에 따라 error 상태를 설정하거나 라우터를 통해 페이지를 이동시키는 분기 처리.

이 모든 것이 단지 로그인 폼 하나를 위해 필요한 코드였다.

[After] React 19의 시대: 선언적인 우아함

다음으로, 똑같은 기능을 하는 React 19의 코드가 나타났다.

// actions.js (Server File)
'use server';
// ... (로그인 로직, 성공 시 redirect, 실패 시 메시지 반환)

// LoginForm_New.js (Client Component)
'use client';
import { useFormState } from 'react-dom';
import { loginAction } from './actions';
import { SubmitButton } from './SubmitButton'; // useFormStatus를 사용하는 버튼

const initialState = { message: null };

export function LoginFormNew() {
  const [state, formAction] = useFormState(loginAction, initialState);

  return (
    <form action={formAction}>
      <input type="email" name="email" />
      <input type="password" name="password" />
      <SubmitButton />
      {state?.message && <p style={{ color: 'red' }}>{state.message}</p>}
    </form>
  );
}

회의실은 조용해졌다. 그 침묵은 코드의 압도적인 간결함이 주는 경이로움 때문이었다.

  • 입력값, 에러, 제출 중 상태를 관리하던 4개의 useState가 모두 사라지고, useFormState 하나로 통합되었다.
  • 복잡했던 handleSubmit 함수 전체가 사라졌다.
  • fetch 호출과 JSON 직렬화 코드가 사라졌다.
  • 제출 버튼의 로딩 상태 관리는 SubmitButton 컴포넌트 내부로 완벽하게 캡슐화되었다.

코드는 이제 자신이 ‘무엇을 하는지’만 선언적으로 보여줄 뿐, ‘어떻게 하는지’에 대한 지저분한 구현은 모두 React의 뒤편으로 사라졌다.

이 Before & After 비교 문서는 단순한 코드 조각이 아니었다. 그것은 React 19가 웹 개발의 복잡성을 어떻게 정복했는지를 보여주는 가장 강력한 증거였다. 개발자들은 더 이상 상태 관리의 미로에서 헤맬 필요가 없었다. 그들은 이제 비즈니스 로직이라는 본질에 집중할 수 있는 자유를 얻게 될 것이었다. 이 극명한 대비는 앞으로 수많은 개발자들이 새로운 패러다임을 받아들이게 될 결정적인 계기가 될 것이 분명했다.