Next.js
React 폼의 onChange 검증을 너무 많이 넣었을 때의 실수
폼의 매 입력마다 검증을 하면 사용자 경험이 나빠진다. 올바른 검증 시점을 찾아야 한다.
React 폼을 만들 때 사용자 입력의 검증은 중요한데, onChange에서 매번 검증하면 예상 밖의 문제가 생긴다.
onChange에서 매번 검증할 때의 문제들
// 나쁜 예
function SignupForm() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmail(value);
// 매번 검증
if (!value.includes('@')) {
setError('Invalid email');
} else if (value.length < 5) {
setError('Email too short');
} else {
setError('');
}
};
return (
<div>
<input value={email} onChange={handleChange} />
{error && <span>{error}</span>}
</div>
);
}
이렇게 하면:
- 사용자가 입력하는 순간마다 "Invalid email" 에러가 뜬다
- 아직 안 된 입력에 자꾸만 빨간 에러 메시지가 나타난다
- 사용자 경험이 매우 불쾌하다
올바른 검증 시점들
1. onBlur (필드 떠날 때)
function SignupForm() {
const [email, setEmail] = useState('');
const [touched, setTouched] = useState(false);
const [error, setError] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
};
const handleBlur = () => {
setTouched(true);
validateEmail(email);
};
const validateEmail = (value: string) => {
if (!value.includes('@')) {
setError('Invalid email');
} else if (value.length < 5) {
setError('Email too short');
} else {
setError('');
}
};
return (
<div>
<input
value={email}
onChange={handleChange}
onBlur={handleBlur}
aria-invalid={touched && !!error}
/>
{touched && error && <span>{error}</span>}
</div>
);
}
2. 제출할 때 (submit)
function SignupForm() {
const [formData, setFormData] = useState({ email: '', password: '' });
const [errors, setErrors] = useState<Record<string, string>>({});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.email.includes('@')) {
newErrors.email = 'Invalid email';
}
if (formData.password.length < 8) {
newErrors.password = 'Password too short';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
// 서버로 전송
submitForm(formData);
}
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={formData.email}
onChange={handleChange}
aria-invalid={!!errors.email}
/>
{errors.email && <span>{errors.email}</span>}
<button type="submit">Sign up</button>
</form>
);
}
실시간 피드백이 필요한 경우
아이디 중복 확인처럼 서버와 통신이 필요한 경우는 디바운싱을 써서 과도한 요청을 줄인다.
function UsernameField() {
const [username, setUsername] = useState('');
const [isAvailable, setIsAvailable] = useState<boolean | null>(null);
const debounceTimer = useRef<NodeJS.Timeout>();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setUsername(value);
// 이전 타이머 취소
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
// 500ms 후 서버 확인
debounceTimer.current = setTimeout(async () => {
if (value.length >= 3) {
const response = await fetch(`/api/check-username?q=${value}`);
const { available } = await response.json();
setIsAvailable(available);
}
}, 500);
};
return (
<div>
<input value={username} onChange={handleChange} />
{isAvailable === true && <span>✓ Available</span>}
{isAvailable === false && <span>✗ Already taken</span>}
</div>
);
}
검증 라이브러리 사용
복잡한 폼은 react-hook-form이나 Formik 같은 라이브러리를 쓰면 더 간단하다.
import { useForm } from 'react-hook-form';
function SignupForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
})}
/>
{errors.email && <span>{errors.email.message}</span>}
<button type="submit">Sign up</button>
</form>
);
}
onChange에서 매번 검증하는 건 문제해결이 아니라 사용자에게 불필요한 좌절감을 준다. 검증 시점을 신중하게 선택하자.