React 요약 7 - 폼 다루기
Updated:
제어 컴포넌트 (Controlled Components)
import { useState } from 'react';
function ControlledForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
gender: 'male',
subscribe: false,
interests: []
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
if (type === 'checkbox') {
if (name === 'subscribe') {
// 단일 체크박스
setFormData({
...formData,
[name]: checked
});
} else if (name === 'interests') {
// 체크박스 그룹
const updatedInterests = [...formData.interests];
if (checked) {
updatedInterests.push(value);
} else {
const index = updatedInterests.indexOf(value);
if (index > -1) {
updatedInterests.splice(index, 1);
}
}
setFormData({
...formData,
interests: updatedInterests
});
}
} else {
// 텍스트, 라디오 등 다른 입력 타입
setFormData({
...formData,
[name]: value
});
}
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
// API 호출 등의 로직...
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="message">Message:</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
/>
</div>
<div>
<p>Gender:</p>
<label>
<input
type="radio"
name="gender"
value="male"
checked={formData.gender === 'male'}
onChange={handleChange}
/>
Male
</label>
<label>
<input
type="radio"
name="gender"
value="female"
checked={formData.gender === 'female'}
onChange={handleChange}
/>
Female
</label>
</div>
<div>
<p>Interests:</p>
<label>
<input
type="checkbox"
name="interests"
value="sports"
checked={formData.interests.includes('sports')}
onChange={handleChange}
/>
Sports
</label>
<label>
<input
type="checkbox"
name="interests"
value="music"
checked={formData.interests.includes('music')}
onChange={handleChange}
/>
Music
</label>
<label>
<input
type="checkbox"
name="interests"
value="reading"
checked={formData.interests.includes('reading')}
onChange={handleChange}
/>
Reading
</label>
</div>
<div>
<label>
<input
type="checkbox"
name="subscribe"
checked={formData.subscribe}
onChange={handleChange}
/>
Subscribe to newsletter
</label>
</div>
<button type="submit">Submit</button>
</form>
);
}
코멘트: 제어 컴포넌트는 React 상태를 “단일 진실 공급원(single source of truth)”으로 사용합니다. 모든 폼 요소의 값은 React 상태에 저장되고, 상태 변경 함수를 통해서만 업데이트됩니다. 이 방식은 폼 데이터에 대한 완전한 제어를 제공하지만, 많은 입력 필드가 있는 경우 코드가 복잡해질 수 있습니다.
비제어 컴포넌트 (Uncontrolled Components)
import { useRef } from 'react';
const handleSubmit = (e) => {
e.preventDefault();
// ref를 통해 직접 DOM 요소의 값에 접근
const formData = {
name: nameRef.current.value,
email: emailRef.current.value,
message: messageRef.current.value,
file: fileRef.current.files[0]
};
console.log('Form submitted:', formData);
// API 호출 등의 로직...
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
ref={nameRef}
defaultValue=""
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
ref={emailRef}
defaultValue=""
/>
</div>
<div>
<label htmlFor="message">Message:</label>
<textarea
id="message"
name="message"
ref={messageRef}
defaultValue=""
/>
</div>
<div>
<label htmlFor="file">Upload File:</label>
<input
type="file"
id="file"
name="file"
ref={fileRef}
/>
</div>
<button type="submit">Submit</button>
</form>
);
}
코멘트: 비제어 컴포넌트는 DOM 자체를 데이터의 진실 공급원으로 사용합니다. React의 ref를 사용하여 필요할 때(주로 폼 제출 시) DOM 요소의 값에 접근합니다. 이 방식은 코드가 간결하고 파일 입력과 같은 일부 입력 타입에 더 적합할 수 있지만, 실시간 유효성 검사나 조건부 렌더링과 같은 기능을 구현하기 어렵습니다.
폼 라이브러리: React Hook Form
import { useForm } from 'react-hook-form';
function ReactHookFormExample() {
const {
register,
handleSubmit,
formState: { errors },
watch
} = useForm();
const onSubmit = (data) => {
console.log('Form submitted:', data);
// API 호출 등의 로직...
};
// 실시간으로 password 값 감시
const password = watch('password');
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
{...register('name', {
required: 'Name is required',
minLength: { value: 2, message: 'Name must be at least 2 characters' }
})}
/>
{errors.name && <p className="error">{errors.name.message}</p>}
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
})}
/>
{errors.email && <p className="error">{errors.email.message}</p>}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
type="password"
{...register('password', {
required: 'Password is required',
minLength: { value: 8, message: 'Password must be at least 8 characters' }
})}
/>
{errors.password && <p className="error">{errors.password.message}</p>}
</div>
<div>
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
id="confirmPassword"
type="password"
{...register('confirmPassword', {
required: 'Please confirm your password',
validate: value => value === password || 'Passwords do not match'
})}
/>
{errors.confirmPassword && <p className="error">{errors.confirmPassword.message}</p>}
</div>
<button type="submit">Submit</button>
</form>
);
}
코멘트: React Hook Form은 성능과 개발자 경험을 모두 고려한 인기 있는 폼 라이브러리입니다. 비제어 컴포넌트 방식을 기반으로 하면서도 유효성 검사, 에러 처리, 폼 상태 관리 등의 기능을 제공합니다. 복잡한 폼을 다룰 때 코드량을 줄이고 성능을 향상시킬 수 있습니다.
폼 유효성 검사
import { useState } from 'react';
function FormValidation() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
// 입력 변경 처리
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
// 입력 변경 시 유효성 검사
validateField(name, value);
};
// 필드 포커스 아웃 처리
const handleBlur = (e) => {
const { name } = e.target;
setTouched({
...touched,
[name]: true
});
// 포커스 아웃 시 유효성 검사
validateField(name, formData[name]);
};
// 단일 필드 유효성 검사
const validateField = (name, value) => {
let fieldErrors = { ...errors };
switch (name) {
case 'username':
if (!value) {
fieldErrors.username = 'Username is required';
} else if (value.length < 3) {
fieldErrors.username = 'Username must be at least 3 characters';
} else {
delete fieldErrors.username;
}
break;
case 'email':
if (!value) {
fieldErrors.email = 'Email is required';
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) {
fieldErrors.email = 'Invalid email address';
} else {
delete fieldErrors.email;
}
break;
case 'password':
if (!value) {
fieldErrors.password = 'Password is required';
} else if (value.length < 8) {
fieldErrors.password = 'Password must be at least 8 characters';
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
fieldErrors.password = 'Password must contain at least one uppercase letter, one lowercase letter, and one number';
} else {
delete fieldErrors.password;
}
break;
default:
break;
}
setErrors(fieldErrors);
return Object.keys(fieldErrors).length === 0;
};
// 전체 폼 유효성 검사
const validateForm = () => {
let isValid = true;
// 모든 필드 검사
Object.keys(formData).forEach(name => {
if (!validateField(name, formData[name])) {
isValid = false;
}
});
// 모든 필드를 touched로 표시
const allTouched = Object.keys(formData).reduce((acc, field) => {
acc[field] = true;
return acc;
}, {});
setTouched(allTouched);
return isValid;
};
// 폼 제출 처리
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
console.log('Form is valid, submitting:', formData);
// API 호출 등의 로직...
} else {
console.log('Form has errors, cannot submit');
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
onBlur={handleBlur}
className={touched.username && errors.username ? 'error' : ''}
/>
{touched.username && errors.username && (
<p className="error-message">{errors.username}</p>
)}
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
className={touched.email && errors.email ? 'error' : ''}
/>
{touched.email && errors.email && (
<p className="error-message">{errors.email}</p>
)}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
className={touched.password && errors.password ? 'error' : ''}
/>
{touched.password && errors.password && (
<p className="error-message">{errors.password}</p>
)}
</div>
<button type="submit">Submit</button>
</form>
);
}
코멘트: 이 예제는 수동으로 폼 유효성 검사를 구현하는 방법을 보여줍니다. 실제 프로젝트에서는 Formik, React Hook Form과 같은 라이브러리나 Yup, Zod와 같은 스키마 유효성 검사 라이브러리를 사용하는 것이 더 효율적입니다. 이 패턴의 핵심 요소는 다음과 같습니다:
- 각 필드의 오류 상태 추적
- 사용자가 상호작용한 필드만 유효성 검사 메시지 표시 (touched 상태)
- 실시간 유효성 검사 (입력 변경 시)
- 최종 유효성 검사 (폼 제출 시)
Leave a comment