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
  • useFormStatusreact-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

폼(Form) 처리 방법 (React 19)들어가며사용 방법React 19에서의 폼 동작 use 훅과 useFormStatus 훅과 함께 사용하기마치며참고 사이트