기본적인 폼 유효성 검사 구현
필수값·비밀번호 규칙·필드 일치 검사를 구현하고 입력·포커스 이탈·제출 시점에 맞춰 오류 메시지를 표시합니다.
유효성 검사는 입력 오류를 빠르게 알려 사용자 경험을 높이고, 불필요한 요청을 줄이는 데 유용합니다. 브라우저 검사는 우회할 수 있으므로 보안과 데이터 무결성의 최종 책임은 서버에 있습니다.
이 장에서는 클라이언트 측 유효성 검사의 중요성과, 이를 구현하는 기본 방법을 함께 정리합니다.
유효성 검사의 중요성
유효성 검사는 단순히 데이터의 형식을 확인하는 것을 넘어, 다음과 같은 여러 중요한 역할을 합니다.
- 사용자 경험 개선 (UX): 사용자가 폼을 제출하기 전에 오류를 미리 알려줌으로써, 불필요한 서버 통신과 페이지 새로고침 없이 즉각적인 피드백을 제공합니다. 이는 사용자 frustation을 줄이고 더 나은 인터랙션을 가능하게 합니다.
- 데이터 품질 안내: 저장 전에 형식 오류를 발견하도록 돕습니다. 실제 데이터 무결성은 서버 검증과 데이터베이스 제약 조건으로 보장해야 합니다.
- 보안 경계 구분: 클라이언트 검사는 보안 경계가 아닙니다. 서버 검증과 매개변수화된 쿼리, 출력 문맥에 맞는 인코딩처럼 공격 유형에 맞는 방어를 별도로 적용해야 합니다.
- 서버 부하 감소: 잘못된 요청이 서버에 도달하기 전에 클라이언트에서 걸러내어 서버의 처리 부담을 줄입니다.
- 클라이언트 측 유효성 검사: 사용자 경험을 위해 브라우저에서 즉시 이루어집니다. 빠른 피드백을 제공하지만, 사용자가 쉽게 우회할 수 있으므로 보안 목적으로는 충분하지 않습니다.
- 서버 측 유효성 검사: 데이터가 서버에 도달했을 때 최종적으로 이루어지는 검사입니다. 보안 및 데이터 무결성 측면에서 필수적이며, 클라이언트 측 검사와 병행되어야 합니다.
이번 장에서는 주로 클라이언트 측 유효성 검사에 초점을 맞추겠습니다.
기본적인 유효성 검사 구현 패턴
가장 기본적인 유효성 검사는 useState를 사용하여 폼 필드와 에러 메시지를 관리하는 것입니다.
필수 필드 검사
가장 흔한 유효성 검사 중 하나는 필드가 비어 있는지 확인하는 것입니다.
import React, { useState } from 'react';
function SimpleValidationForm() {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [usernameError, setUsernameError] = useState('');
const [emailError, setEmailError] = useState('');
const [formValid, setFormValid] = useState(false); // 폼 전체 유효성 상태
// 입력 변경 핸들러
const handleUsernameChange = (e) => {
setUsername(e.target.value);
// 실시간 유효성 검사 (선택 사항)
if (e.target.value.trim() === '') {
setUsernameError('사용자 이름은 필수입니다.');
} else {
setUsernameError('');
}
};
const handleEmailChange = (e) => {
setEmail(e.target.value);
// 실시간 유효성 검사 (선택 사항)
if (!e.target.value.includes('@')) {
setEmailError('유효한 이메일 주소를 입력해주세요.');
} else {
setEmailError('');
}
};
// 폼 제출 핸들러
const handleSubmit = (e) => {
e.preventDefault();
let valid = true;
// 제출 시 최종 유효성 검사
if (username.trim() === '') {
setUsernameError('사용자 이름은 필수입니다.');
valid = false;
} else {
setUsernameError('');
}
if (!email.includes('@')) {
setEmailError('유효한 이메일 주소를 입력해주세요.');
valid = false;
} else {
setEmailError('');
}
if (valid) {
console.log('폼 데이터 유효:', { username, email });
alert(`제출 완료!\n사용자 이름: ${username}\n이메일: ${email}`);
// 폼 초기화
setUsername('');
setEmail('');
} else {
console.log('폼 데이터 유효하지 않음.');
alert('폼 입력값을 확인해주세요.');
}
setFormValid(valid); // 폼 전체 유효성 상태 업데이트
};
return (
<div style={{ maxWidth: '500px', margin: '30px auto', padding: '25px', border: '1px solid #ddd', borderRadius: '8px', boxShadow: '0 2px 10px rgba(0,0,0,0.05)', backgroundColor: '#fff' }}>
<h2 style={{ textAlign: 'center', color: '#2c3e50', marginBottom: '30px' }}>기본적인 유효성 검사</h2>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '15px' }}>
<label htmlFor="username" style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>사용자 이름:</label>
<input
type="text"
id="username"
value={username}
onChange={handleUsernameChange}
style={{ width: '100%', padding: '10px', border: usernameError ? '1px solid red' : '1px solid #ccc', borderRadius: '4px', boxSizing: 'border-box' }}
/>
{usernameError && <p style={{ color: 'red', fontSize: '0.8em', marginTop: '5px' }}>{usernameError}</p>}
</div>
<div style={{ marginBottom: '15px' }}>
<label htmlFor="email" style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>이메일:</label>
<input
type="email"
id="email"
value={email}
onChange={handleEmailChange}
style={{ width: '100%', padding: '10px', border: emailError ? '1px solid red' : '1px solid #ccc', borderRadius: '4px', boxSizing: 'border-box' }}
/>
{emailError && <p style={{ color: 'red', fontSize: '0.8em', marginTop: '5px' }}>{emailError}</p>}
</div>
<button type="submit" className="button" style={{ width: '100%', padding: '12px', fontSize: '1.1em' }}>
제출
</button>
</form>
</div>
);
}
export default SimpleValidationForm;이 예시에서는 각 입력 필드에 대한 상태(username, email)와 에러 메시지 상태(usernameError, emailError)를 별도로 관리합니다.
onChange 이벤트에서 실시간으로 유효성을 검사하고, handleSubmit에서 최종적으로 모든 필드를 다시 검사하여 제출 여부를 결정합니다.
복잡한 규칙 검사: 비밀번호 유효성
비밀번호는 길이 제한, 특수 문자 포함 등 여러 복잡한 규칙을 가질 수 있습니다.
// ... 기존 코드 ...
const [password, setPassword] = useState('');
const [passwordError, setPasswordError] = useState('');
const validatePassword = (value) => {
let error = '';
if (value.length < 8) {
error = '비밀번호는 최소 8자 이상이어야 합니다.';
} else if (!/[A-Z]/.test(value)) {
error = '비밀번호는 최소 하나의 대문자를 포함해야 합니다.';
} else if (!/[a-z]/.test(value)) {
error = '비밀번호는 최소 하나의 소문자를 포함해야 합니다.';
} else if (!/[0-9]/.test(value)) {
error = '비밀번호는 최소 하나의 숫자를 포함해야 합니다.';
} else if (!/[!@#$%^&*()]/.test(value)) { // 예시: 특정 특수 문자 허용
error = '비밀번호는 최소 하나의 특수 문자(!@#$%^&*())를 포함해야 합니다.';
}
return error;
};
const handlePasswordChange = (e) => {
const value = e.target.value;
setPassword(value);
setPasswordError(validatePassword(value));
};
// ... handleSubmit 함수 내에서 password 유효성 검사 추가 ...
let valid = true;
// ... 기존 유효성 검사 ...
const passwordValidationError = validatePassword(password);
setPasswordError(passwordValidationError);
if (passwordValidationError) {
valid = false;
}
// ...이처럼 여러 규칙을 적용할 때는 검증 함수를 하나로 분리해 onChange와 handleSubmit에서 재사용할 수 있습니다. 제출 시에는 현재 password로 지역 검증값을 다시 계산하고, 그 값을 에러 상태 저장과 제출 여부 판정에 함께 사용합니다.
필드 간 상호 의존성 검사: 비밀번호 확인
비밀번호 확인 필드는 비밀번호 필드와 값이 일치하는지 확인해야 하는 상호 의존성을 가집니다.
// ... 기존 코드 ...
const [confirmPassword, setConfirmPassword] = useState('');
const [confirmPasswordError, setConfirmPasswordError] = useState('');
const handleConfirmPasswordChange = (e) => {
const value = e.target.value;
setConfirmPassword(value);
// 실시간 검사: 비밀번호와 일치하는지
if (password !== value) {
setConfirmPasswordError('비밀번호가 일치하지 않습니다.');
} else {
setConfirmPasswordError('');
}
};
// ... handleSubmit 함수 내에서 confirmPassword 유효성 검사 추가 ...
let valid = true;
// ... 기존 유효성 검사 ...
if (password !== confirmPassword) {
setConfirmPasswordError('비밀번호가 일치하지 않습니다.');
valid = false;
} else {
setConfirmPasswordError('');
}
// ...이 경우, confirmPassword의 onChange 핸들러나 handleSubmit 함수에서 password 상태의 값을 참조하여 비교 검사를 수행합니다.
React · dependent fields · current values
필드 규칙과 필드 간 관계를 분리한다
validatePassword는 입력 변경과 제출에 같은 규칙 순서를 씁니다. 확인은 현재 두 값을 비교하고, 제출은 저장 오류 대신 현재 값으로 두 결과를 다시 계산합니다.
비밀번호 규칙 검사
validatePassword(value)가 규칙을 정해진 순서로 확인하고 첫 오류 메시지를 반환합니다.
규칙 위반 표시
입력 변경에서 계산한 첫 오류를 상태에 둡니다.
현재 두 값 비교
password === confirmPassword
불일치 표시
현재 두 값이 다르면 확인 필드 오류 메시지를 설정합니다.
제출 시 재검사
현재 비밀번호 규칙과 확인값 일치를 다시 계산합니다.
상태 설정은 다음 렌더를 예약합니다. 제출 여부는 방금 저장한 오류 상태가 아니라 현재 입력에서 계산한 지역 결과로 정합니다.
유효성 검사 메시지 표시
유효성 검사 결과를 사용자에게 명확하고 즉각적으로 피드백하는 것이 중요합니다.
- 인라인 에러 메시지: 각 입력 필드 아래에 에러 메시지를 직접 표시하는 것이 가장 일반적입니다.
- 스타일 변경: 에러가 있는 필드의 테두리 색상을 빨간색으로 변경하는 등 시각적인 힌트를 제공합니다.
- 폼 제출 버튼 비활성화: 폼에 유효성 에러가 하나라도 있다면 제출 버튼을 비활성화하여 사용자가 잘못된 데이터를 제출하는 것을 막을 수 있습니다.
// 컴포넌트를 렌더링할 때 현재 입력값에서 파생합니다.
const formValid =
username.trim() !== '' &&
email.includes('@') &&
validatePassword(password) === '' &&
password === confirmPassword;
return (
// ...
<button type="submit" className="button" disabled={!formValid}>
제출
</button>
// ...
);formValid는 다른 상태에서 완전히 계산할 수 있는 값이므로 별도 상태와 useEffect로 동기화하지 않습니다. Hook은 컴포넌트 최상위에서만 호출해야 하며 이벤트 핸들러 안에 둘 수 없습니다. 버튼을 비활성화하더라도 handleSubmit은 현재 값으로 같은 규칙을 다시 검사해야 합니다. 클라이언트 상태와 무관하게 서버도 요청을 독립적으로 검증합니다.
사용자 경험을 고려한 유효성 검사 시점
React · validation timing · trust boundary
빠른 피드백과 최종 판정의 경계를 나눈다
change·blur는 피드백, submit은 현재 값을 검사합니다. 브라우저 검증은 우회돼 서버가 보안·무결성을 판정합니다.
폼 준비
초기 값을 보여 주고 첫 입력을 기다립니다.
입력 중
현재 값을 갱신하고 필드 규칙을 확인합니다.
필드 피드백
오류를 가까이 보여 주고 변경 때 다시 확인합니다.
제출 검사
현재 규칙을 다시 계산해 실패하면 요청을 막습니다.
서버 최종 판정
클라이언트 통과 여부와 별개로 보안과 데이터 무결성을 다시 확인합니다.
change·blur·submit은 검사 시점을 정합니다. 제출 핸들러는 현재 값을 다시 검사해 상태 갱신 순서와 무관하게 요청 여부를 정합니다.
유효성 검사를 언제 수행할 것인가도 중요한 UX 고려 사항입니다.
onChange(입력 중): 사용자가 타이핑하는 동안 실시간으로 오류를 표시합니다. 즉각적인 피드백을 제공하지만, 너무 민감하면 사용자를 방해할 수 있습니다 (예: 한 글자 입력하자마자 에러).onBlur(필드 포커스 잃을 때): 사용자가 필드 입력을 마치고 다른 곳을 클릭했을 때 검사합니다.onChange보다 덜 방해적이지만,onBlur전에 이미 제출을 시도할 수도 있습니다.onSubmit(폼 제출 시): 폼 제출 버튼을 클릭했을 때 현재 값으로 모든 필드를 다시 검사하는 브라우저 측 최종 관문입니다. 통과한 요청도 서버에서 독립적으로 검증해야 합니다.
onBlur 시 검사: 사용자가 필드를 벗어났을 때 해당 필드의 유효성 검사를 수행하고 에러 메시지를 표시합니다.
onSubmit 시 모든 필드 검사: 폼 제출 시 모든 필드를 검사하고, 유효하지 않은 필드에는 에러 메시지를 표시하여 제출을 막습니다.
이 패턴은 실시간 피드백과 최종 제출 유효성 검사의 균형을 맞춥니다.
useReducer와 커스텀 훅으로 유효성 검사
앞서 2장에서 다루었던 useReducer 또는 커스텀 훅(useForm)은 유효성 검사 로직을 통합하여 관리하는 데 매우 효과적입니다.
useReducer의 경우: 리듀서 함수 내에서 각 필드의 상태 변화에 따라 유효성 검사를 수행하고, 에러 메시지 상태도 함께 업데이트하도록 로직을 추가할 수 있습니다.- 커스텀 훅 (
useForm): 유효성 검사 로직 자체를 훅의 인자로 받아들이고,handleSubmit내부에서 이 유효성 검사 함수를 호출하여 에러를 처리하는 방식이 가장 일반적입니다. (2장의useForm예시 참고)
이러한 접근 방식은 코드 중복을 줄이고, 폼 로직의 응집성을 높여 유지보수성을 향상시킵니다.
9장 3절 기본적인 폼 유효성 검사 구현은 여기까지입니다.
이 장에서는 폼 유효성 검사의 중요성을 이해하고, 클라이언트 측 유효성 검사의 기본적인 구현 패턴을 살펴보았습니다.
필수 필드, 복잡한 규칙, 필드 간 상호 의존성을 검사하는 방법을 배우고, 사용자 경험을 고려하여 에러 메시지를 표시하는 방법과 유효성 검사 시점을 결정하는 원칙에 대해서도 알아보았습니다.