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@ 매개변수를 반드시 추가해주어야 한다.
- 그렇지 않을 경우 오류가 발생한다. ⚠️
- 이때, 폼 제출 이벤트 처리 함수의 첫 번째 인자로 @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
'Programming > React' 카테고리의 다른 글
| [React.js] useLayoutEffect 훅 (0) | 2025.05.10 |
|---|---|
| [React.js] React Hook Form 라이브러리 (0) | 2024.11.23 |
| [React.js] 객체 표기법(Object Notation) 방식과 빌더 콜백 표기법(Builder Callback Notation) 방식 (0) | 2024.11.13 |
| [React.js] .js 파일에서 Uncaught SyntaxError: Unexpected token '<' 오류 발생할 때 해결 방법 (Vite) (0) | 2024.11.13 |
| [React.js] <Link> 컴포넌트와 <NavLink> 컴포넌트 비교 (React Router) (0) | 2024.11.13 |
| [React.js] 리덕스(Redux), 리덕스 툴킷(Redux Toolkit) (0) | 2024.11.11 |
| [React.js] 모든 웹 브라우저에서 공통된 HTML 요소 스타일이 보여지도록 설정하는 방법 (normalize.css) (0) | 2024.11.07 |
| [React.js] Recharts 라이브러리 (1) | 2024.11.06 |