728x90
728x90

폼(Form) 처리 방법 (React 19)

들어가며

  • 예제 코드와 함께 React 19에서 업데이트 된 폼(Form) 처리 방법에 대하여 간단하게 정리해본다.

 

사용 방법

  • 아래의 코드는 React 19에서 새로 추가된 Form 관련 기능들을 적용한 코드이다.

 

Signup.jsx
import { useActionState } from 'react';
import {
  isEmail,
  isNotEmpty,
  isEqualToOtherValue,
  hasMinLength,
} from '../util/validation';

export default function Signup() {
  // React 19 이상에서는 폼 제출 시, formData 객체가 생성되고 함수의 인자로 가져와 특정 필드의 입력값을 가져올 수 있다.
  // 또한, 브라우저의 기본 동작을 자동으로 방지하기 때문에 event.preventDefault() 코드를 따로 작성해주지 않아도 된다.
  // 그리고 폼 제출 시, 자동으로 필드의 값들이 초기화된다.
  function singupAction(prevFormState, formData) {
    const email = formData.get('email');
    const password = formData.get('password');
    const confirmPassword = formData.get('confirm-password');
    const firstName = formData.get('first-name');
    const lastName = formData.get('last-name');
    const role = formData.get('role');
    const terms = formData.get('terms');
    const acquisitionChannel = formData.getAll('acquisition'); // 동일한 name이 여러개 있을 경우

    // 유효성 검증
    let errors = [];

    if (!isEmail(email)) {
      errors.push('Invalid email address');
    }

    if (!isNotEmpty(password) || !hasMinLength(password, 6)) {
      errors.push('You must provide a password with at least six characters.');
    }

    if (!isEqualToOtherValue(password, confirmPassword)) {
      errors.push('Passwords do not match.');
    }

    if (!isNotEmpty(firstName) || !isNotEmpty(lastName)) {
      errors.push('Please provide both your first and last name.');
    }

    if (!isNotEmpty(role)) {
      errors.push('Please select a role.');
    }

    if (!terms) {
      errors.push('You must agree to the terms and conditions.');
    }

    if (acquisitionChannel.length === 0) {
      errors.push('Please select at least one acquisition channel.');
    }

    // 에러 표시
    if (errors.length > 0) {
      return {
        // 에러 문구가 담긴 배열
        errors,
        // 입력값
        enteredValues: {
          email,
          password,
          confirmPassword,
          firstName,
          lastName,
          role,
          acquisitionChannel,
          terms,
        },
      };
    }

    return { errors: null };
  }

  // useActionState 훅 사용
  const [formState, formAction] = useActionState(singupAction, {
    errors: null,
  });

  return (
    <form action={formAction}>
      {/* React 19 이상에서는 onSubmit이 아닌 action을 통해 폼 제출 이벤트 처리 함수를 전달한다. */}
      <h2>Welcome on board!</h2>
      <p>We just need a little bit of data from you to get you started 🚀</p>
      <div className='control'>
        <label htmlFor='email'>Email</label>
        <input
          id='email'
          type='email'
          name='email'
          defaultValue={formState.enteredValues?.email}
        />
      </div>
      <div className='control-row'>
        <div className='control'>
          <label htmlFor='password'>Password</label>
          <input
            id='password'
            type='password'
            name='password'
            defaultValue={formState.enteredValues?.password}
          />
        </div>

        <div className='control'>
          <label htmlFor='confirm-password'>Confirm Password</label>
          <input
            id='confirm-password'
            type='password'
            name='confirm-password'
            defaultValue={formState.enteredValues?.confirmPassword}
          />
        </div>
      </div>
      <hr />
      <div className='control-row'>
        <div className='control'>
          <label htmlFor='first-name'>First Name</label>
          <input
            type='text'
            id='first-name'
            name='first-name'
            defaultValue={formState.enteredValues?.firstName}
          />
        </div>

        <div className='control'>
          <label htmlFor='last-name'>Last Name</label>
          <input
            type='text'
            id='last-name'
            name='last-name'
            defaultValue={formState.enteredValues?.lastName}
          />
        </div>
      </div>
      <div className='control'>
        <label htmlFor='phone'>What best describes your role?</label>
        <select
          id='role'
          name='role'
          defaultValue={formState.enteredValues?.role}
        >
          <option value='student'>Student</option>
          <option value='teacher'>Teacher</option>
          <option value='employee'>Employee</option>
          <option value='founder'>Founder</option>
          <option value='other'>Other</option>
        </select>
      </div>
      <fieldset>
        <legend>How did you find us?</legend>
        <div className='control'>
          <input
            type='checkbox'
            id='google'
            name='acquisition'
            value='google'
            defaultChecked={formState.enteredValues?.acquisitionChannel.includes(
              'google'
            )}
          />
          <label htmlFor='google'>Google</label>
        </div>

        <div className='control'>
          <input
            type='checkbox'
            id='friend'
            name='acquisition'
            value='friend'
            defaultChecked={formState.enteredValues?.acquisitionChannel.includes(
              'friend'
            )}
          />
          <label htmlFor='friend'>Referred by friend</label>
        </div>

        <div className='control'>
          <input
            type='checkbox'
            id='other'
            name='acquisition'
            value='other'
            defaultChecked={formState.enteredValues?.acquisitionChannel.includes(
              'other'
            )}
          />
          <label htmlFor='other'>Other</label>
        </div>
      </fieldset>
      <div className='control'>
        <label htmlFor='terms-and-conditions'>
          <input
            type='checkbox'
            id='terms-and-conditions'
            name='terms'
            defaultChecked={formState.enteredValues?.terms}
          />
          I agree to the terms and conditions
        </label>
      </div>

      {/* 오류 메시지 표시 */}
      {formState.errors && (
        <ul className='errors'>
          {formState.errors.map((error) => (
            <li key={error}>{error}</li>
          ))}
        </ul>
      )}

      <p className='form-actions'>
        <button type='reset' className='button button-flat'>
          Reset
        </button>
        <button className='button'>Sign up</button>
      </p>
    </form>
  );
}

 

React 19에서의 폼 동작 

  • React 19에서는 폼 제출 시, @formData@ 객체가 생성되고 폼 제출 이벤트 처리 함수의 인자로 가져와 특정 필드의 입력값을 가져올 수 있다.
  • 또한, 브라우저의 기본 동작을 자동으로 방지하기 때문에 @event.preventDefault()@ 코드를 폼 제출 이벤트 처리 함수 안에 따로 작성해주지 않아도 된다.
  • 그리고 폼 제출시 필드의 입력값이 자동으로 사라진다.
function singupAction(prevFormState, formData) {
    const email = formData.get('email');
    const password = formData.get('password');
    const confirmPassword = formData.get('confirm-password');
    const firstName = formData.get('first-name');
    const lastName = formData.get('last-name');
    const role = formData.get('role');
    const terms = formData.get('terms');
    const acquisitionChannel = formData.getAll('acquisition'); // 동일한 name이 여러개 있을 경우

    // 유효성 검증
    let errors = [];

    if (!isEmail(email)) {
      errors.push('Invalid email address');
    }

    if (!isNotEmpty(password) || !hasMinLength(password, 6)) {
      errors.push('You must provide a password with at least six characters.');
    }

    if (!isEqualToOtherValue(password, confirmPassword)) {
      errors.push('Passwords do not match.');
    }

    if (!isNotEmpty(firstName) || !isNotEmpty(lastName)) {
      errors.push('Please provide both your first and last name.');
    }

    if (!isNotEmpty(role)) {
      errors.push('Please select a role.');
    }

    if (!terms) {
      errors.push('You must agree to the terms and conditions.');
    }

    if (acquisitionChannel.length === 0) {
      errors.push('Please select at least one acquisition channel.');
    }

    // 에러 표시
    if (errors.length > 0) {
      return {
        // 에러 문구가 담긴 배열
        errors,
        // 입력값
        enteredValues: {
          email,
          password,
          confirmPassword,
          firstName,
          lastName,
          role,
          acquisitionChannel,
          terms,
        },
      };
    }

    return { errors: null };
  }

 

  • 폼 제출 이벤트 처리 함수(@signupAction@)@formData@ 인자를 통해 폼 요소의 필드 안의 값들에 접근하여 가져올 수 있다.
  • @<input>@ 요소에 @defaultValue@ 또는 @defaultChecked@ 속성을 추가하여 유효성 검증(Validation Check) 실패 시, 입력했던 값들이 사라지지 않도록 할 수 있다.
    • 이때 폼 제출 이벤트 처리 함수 안에 해당 기본값들을 반환할 수 있도록 해주어야 한다.
    • 유효성 검증에 통과하여 성공적으로 폼 제출 이벤트 처리 시, 폼 요소의 필드 값들의 내용은 모두 사라진다.
  • @useActionState@ 훅을 이용하여 폼의 필드에 입력된 값들의 상태 관리(@formState@)과 폼 제출 시 처리할 액션(@formAction@)을 관리할 수 있다.
    • 이때, 폼 제출 이벤트 처리 함수의 첫 번째 인자로 @prevFormState@ 매개변수를 반드시 추가해주어야 한다.
      • 그렇지 않을 경우 오류가 발생한다. ⚠️
import { useActionState } from 'react';

// ...

function singupAction(prevFormState, formData) {    // prevFormState 매개변수 추가 (첫 번째)
  // ..
}

// ...

const [formState, formAction] = useActionState(singupAction, {
  errors: null,
});

 

@use@ 훅과 @useFormStatus@ 훅과 함께 사용하기

NewOpinion.jsx
  • React 19에서 새로 추가된 @use@ 훅과 함께 폼 요소 이벤트를 처리할 수 있다. 
    • @use@ 훅은 @Suspense@와 함께 작동하는 @Promise@ 객체를 받아 비동기적으로 데이터를 처리해준다.
import { useActionState, use } from 'react';

import { OpinionsContext } from '../store/opinions-context';

import Submit from './Submit';

export function NewOpinion() {
  const { addOpinion } = use(OpinionsContext);

  // 폼 액션 처리 함수
  async function shareOpinionAction(prevState, formData) {
    const title = formData.get('title');
    const body = formData.get('body');
    const userName = formData.get('userName');

    // 유효성 검증
    let errors = [];

    if (title.trim().length < 5) {
      errors.push('Title must be at least five characters long.');
    }

    if (body.trim().length < 10 || body.trim().length > 300) {
      errors.push('Opinion must be between 10 and 300 characters long.');
    }

    if (!userName.trim()) {
      errors.push('Please provide your name.');
    }

    if (errors.length > 0) {
      return {
        errors,
        enteredValues: {
          title,
          body,
          userName,
        },
      };
    }

    // 백엔드로 전송
    await addOpinion({ title, body, userName }); // 추가

    return { errors: null };
  }

  const [formState, formAction] = useActionState(shareOpinionAction, {
    errors: null,
  });

  return (
    <div id='new-opinion'>
      <h2>Share your opinion!</h2>
      <form action={formAction}>
        <div className='control-row'>
          <p className='control'>
            <label htmlFor='userName'>Your Name</label>
            <input
              type='text'
              id='userName'
              name='userName'
              defaultValue={formState.enteredValues?.userName}
            />
          </p>

          <p className='control'>
            <label htmlFor='title'>Title</label>
            <input
              type='text'
              id='title'
              name='title'
              defaultValue={formState.enteredValues?.title}
            />
          </p>
        </div>
        <p className='control'>
          <label htmlFor='body'>Your Opinion</label>
          <textarea
            id='body'
            name='body'
            rows={5}
            defaultValue={formState.enteredValues?.body}
          ></textarea>
        </p>

        {/* 에러 표시 */}
        {formState.errors && (
          <ul className='errors'>
            {formState.errors.map((error) => (
              <li key={error}>{error}</li>
            ))}
          </ul>
        )}

        {/* 제출 버튼 */}
        <Submit />
      </form>
    </div>
  );
}

 

Submit.jsx
  • @useFormStatus@은 @react-dom@에서 제공되는 훅으로, 폼 상태를 관리할 때 사용되는 훅이다.
  • 이 훅은 폼이 포함된 컴포넌트에서 직접 사용할 수 없으며, 반드시 폼 내부에 중첩된 컴포넌트에서 호출해야 한다.
import { useFormStatus } from 'react-dom';

export default function Submit() {
  const { pending } = useFormStatus();

  return (
    <p className='actions'>
      <button type='submit' disabled={pending}>
        {pending ? 'Submitting...' : 'Submit'}
      </button>
    </p>
  );
}

 

마치며

  • React 19에서 새롭게 추가된 폼 관련 기능들을 적용한 코드를 통해 빠르게 정리해 보았다.
  • 보다 더 자세하고 심화적인 내용(@useOptimistic@ 훅을 이용한 낙관적 업데이트 등)은 차후에 다시 정리해보려고 한다.

 

참고 사이트

 

useActionState – React

The library for web and native user interfaces

ko.react.dev

 

useFormStatus – React

The library for web and native user interfaces

ko.react.dev

 

React v19 – React

The library for web and native user interfaces

ko.react.dev

 

728x90
728x90