폼 다루기 - 폼 밸리데이션(validation)
폼 밸리데이션(form validation)이란 사용자가 입력한 값이 유효한지 확인하고, 올바르지 않은 경우 경고를 표시하거나 폼 제출을 막는 작업을 말합니다.
폼 밸리데이션이란 form 태그 내부에서 작성한 값이 유효한지 체크하는 로직을 말한다.
기본 밸리데이션
리액트에서 HTML5의 기본 폼 검증 기능을 사용할 수 있으며, 이 기능을 사용하면 별도의 로직 작성 없이 간단한 유효성 검사가 가능하다. 기본 폼 검증 속성과 입력 필드의 타입을 서로 조합해서 사용한다.
HTML5 기본 폼 검증 속성 required : 필수 입력 minlength : 입력 최소 길이 maxlength : 입력 최대 길이 min : 입력 최솟값 max : 입력 최댓값 step : 숫자 입력 간격 type : 입력 필드의 타입 pattern : 정규 표현식 패턴
입력 필드의 타입 종류 text email number date tel url color range
기본적인 예시 화면과 코드는 다음과 같다.
form.checkValidity()는 폼 안의 모든 요소가 HTML 속성의 조건을 만족하는지 검사하는 메서드이며, 조건을 만족하지 못하면 필드에 경고 메시지가 뜬다.

import { useState } from "react";
export default function App() {
const [ formData, setFormData ] = useState({
username: '', email: '', age: '', birthdate: '', phone: '',
website: '', color: '#000000', rating: '5',
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
})
}
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
if (form.checkValidity()) {
alert('제출 성공');
}
}
return(
<form onSubmit={handleSubmit}>
<div>
<label>
이름
<input name='username' value={formData.username} onChange={handleChange}
required minLength={3} maxLength={20} pattern="[A-Za-z0-9]+"
title='3-20자 사이의 영문자와 숫자만 가능'></input>
</label>
</div>
<div>
<label>
이메일
<input name='email' type='email' value={formData.email} onChange={handleChange}
required></input>
</label>
</div>
<div>
<label>
나이
<input name='age' type='number' value={formData.age} onChange={handleChange}
required min={18} max={120}></input>
</label>
</div>
<div>
<label>
생일
<input name='birthdate' type='date' value={formData.birthdate} onChange={handleChange}
required min='1900-01-01' max={new Date().toISOString().split('T')[0]}></input>
</label>
</div>
<div>
<label>
핸드폰
<input name='phone' type='tel' value={formData.phone} onChange={handleChange}
pattern="[0-9]{3}-[0-9]{4}-[0-9]{4}" placeholder='010-1234-5678'></input>
</label>
</div>
<div>
<label>
웹사이트
<input name='website' type='url' value={formData.website} onChange={handleChange}
placeholder='https://example.com'></input>
</label>
</div>
<div>
<label>
선호 색상
<input name='color' type='color' value={formData.color} onChange={handleChange}></input>
</label>
</div>
<div>
<label>
평점
<input name='rating' type='range' value={formData.rating} onChange={handleChange}
min='0' max='10' step='1'></input>
<span>{formData.rating}/10</span>
</label>
</div>
<div>
<button type='submit'>제출</button>
</div>
</form>
);
}
커스텀 밸리데이션
…실제 프로젝트에서는 사용자가 입력한 값이 특정 조건을 충족하는지 더 정밀하게 검사해야 하는 경우가 많습니다. 이때는 개발자가 직접 유효성 검사 로직을 작성해 폼을 검증하게 되는데, 이를 커스텀 밸리데이션(custom validation)이라고 합니다.
유효성 검사 함수를 직접 작성하여 제출 전에 form의 유효성을 체크하는 방식을 커스텀 밸리데이션이라고 한다. 유효성 검사 함수는 원하는 곳에서 실행하면 되지만, 보통 submit을 할 때 체크하는 것이 일반적이다.
import { useState } from "react";
export default function App() {
const [name, setName] = useState('');
const [err, setErr] = useState('');
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const validationError = validateName(name);
setErr(validationError);
if (!validationError) {
alert('제출 성공');
}
}
const validateName = (name: string) => {
if (!name) {
return '이름을 입력하세요.';
}
if (name.length < 3 || name.length > 20) {
return '사용자 이름은 3~20자 사이여야 합니다.';
}
if (!/^[A-Za-z0-9]+$/.test(name)) {
return '사용자 이름은 영문자와 숫자만 포함해야 합니다.'
}
return ''
}
return(
<form onSubmit={handleSubmit}>
<div>
<label>
이름
<input value={name} onChange={(e) => {setName(e.target.value)}}></input>
<span style={{color: 'red'}}>{ err && <div>{err}</div> }</span>
</label>
</div>
<div>
<button type='submit'>제출</button>
</div>
</form>
);
}
자습서의 예제가 길어서 핵심만 파악할 수 있게 줄였는데, handleSubmit 코드를 작성하던 중 오류가 생겼다.
기존에 내가 썼던 코드대로 동작시키면, 이름 칸이 비어있는데도 버튼을 눌렀을 때 ‘제출 성공’ 팝업이 떴다.
// 기존 코드
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErr(validateName(name));
if (!err) alert('제출 성공');
}
이는 최초 실행시 err가 ''로 저장되어있어, 비동기로 진행되는 리액트에서는 err 값이 바로 반영되지않기 때문인 것 같았다.
그래서 이전에 배웠던대로 콜백 함수를 적용해보았다.
// 수정 코드
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErr(() => (validateName(name))); // 콜백 함수 형태로 변경
if (!err) alert('제출 성공');
}
하지만 그래도 동작하지 않았다. 이유를 찾아보니 함수형 업데이트는 다음 렌더링 시 이전 값을 참조해서 값을 바꿔달라고 예약을 하는 것이라, 렌더링 전의 값에는 영향을 주지 못한다고 한다. 결국 마지막으로 수정한 형태는 로컬 변수를 써서 적용한 형태가 되었다.
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const validationError = validateName(name);
setErr(validationError);
if (!validationError) {
alert('제출 성공');
}
}
라이브러리 - Formik(포믹)
유효성 검사를 위해 폼 검증 전용 라이브러리(ex. Formik, React Hook Form)를 활용하는 방법도 있다. 자습서에서는 이 중 Formik 예시를 다루고 있었다.
import { ErrorMessage, Field, Form, Formik } from "formik";
interface ErrorValues{
email?: string;
password?: string;
}
export default function App() {
return(
<Formik
initialValues={{email: '', password: ''}}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(true);
setTimeout(() => {
console.log(values);
alert('제출 완료');
setSubmitting(false);
}, 1000)
}}
validateOnChange={false}
validateOnBlur={true}
validate={(values) => {
const errors: ErrorValues = {};
if (!values.email) {
errors.email = '이메일을 입력하세요.'
}
else if (!values.email.includes('@')) {
errors.email = '올바르지 않은 이메일 형식입니다.'
}
if (!values.password) {
errors.password = '비밀번호를 입력하세요.'
}
else if (values.password.length < 4){
errors.password = '비밀번호는 4글자 이상이여야 합니다.'
}
return Object.keys(errors).length > 0 ? errors : {};
}}>
{({ isSubmitting }) => (
<Form>
<Field name="email" type="email" component='input'></Field>
<ErrorMessage name='email' component='div'></ErrorMessage>
<Field name="password" type="password" component='input'></Field>
<ErrorMessage name='password' component='div'></ErrorMessage>
<button type='submit' disabled={isSubmitting}>로그인</button>
</Form>
)}
</Formik>
);
}
큰 형태를 보자면 <Formik> 컴포넌트로 <form>태그를 감싸고, <form>태그 대신 <Form>태그를, <input>태그 대신 <Field>태그를 사용한 형태다.
<Formik><Form><Field /><ErrorMessage /></Form></Formik>
<Formik>
Formik 라이브러리 사용 시 가장 먼저 작성해야하는 컴포넌트이다.
<Formik>사용 구조<Formik initialValues={} onSubmit={} validateOnChange={} validateOnBlur={} validate={}></Formik>initialValues : 각 입력 요소에 대응하는 초깃값 객체 전달 onSubmit : 폼이 제출될 때 실행할 콜백 함수 작성 validateOnChange : 값이 false이면 사용자가 입력하는 도중에는 유효성 검사 하지 않음 validateOnBlur : 값이 true이면 포커스 사라질 때 유효성 검사 실행 validate : 유효성 검사 로직 수행
onSubmit
onSubmit 콜백 함수는 2개의 인자를 받는다. 첫번째 인자는 사용자가 입력한 값이 담긴 객체(values), 두번째 인자를 Formik이 제공하는 다양한 도우미 함수가 들어있는 객체(formikHelpers)이다.
도우미 함수 중 setSubmitting() 함수를 이용하면 폼 중복 제출을 방지할 수 있다. setSubmitting() 함수는 현재 폼이 제출 중인 상태인지 아닌지를 설정한다. 매개변수에 true를 넣어 호출하면 폼이 제출 중인 상태, false를 넣어 호출하면 제출 완료 상태로 변경이 가능하다.
setSubmitting() 함수로 저장한 값은 isSubmitting으로 가지고 올 수 있는데, 가지고 올 때는 <Formik>의 자식으로 함수를 배치하여 가지고 온다.
렌더 프롭스 패턴을 이용한 것인데, 함수 안에 있는 태그에 컴포넌트의 상태(isSubmitting)를 전달할 수 있는 패턴이다.
현재는 렌더 프롭스가 아닌 훅으로 로직을 많이 대체하고 있지만, Formik에서는 아직 사용이 되고 있다고 한다.
위 코드에서도 isSubmitting을 이용해 이미 제출 중일 때는 버튼이 disable되도록 로직을 짜두었다.
validate
validate 함수는 사용자가 입력한 값이 담긴 객체(values)를 인자로 받는다. 유효성 검사 후 오류가 없으면 빈 객체, 오류가 있으면 오류 메시지를 속성으로 포함한 객체를 반환해줘야한다. 여기서 객체의 키는 initialValues에 설정한 키와 같아야 한다.
위 예제에서 반환하는 객체를 정의할 때 interface를 썼는데, interface 없이 빈 객체에 값을 추가할 수 없는지 찾아봤다. 결론적으로 타입스크립트는 자바스크립트와 달리 바로 점(.)을 찍어서 키를 추가할 수가 없었다. 반드시 정해진 키나 타입으로 추가가 가능함을 알아두자.
// 객체 키 추가가 가능한 형태
// 1. 정해진 키로 추가가 가능함
interface ErrorValues{
email?: string;
password?: string;
}
// 2. 정해진 타입으로 추가가 가능함
const errors: Record<string, string> = {};
<Form>
기존의 <form> 태그를 대신하는 컴포넌트이다.
Form 컴포넌트를 사용하면 Formik이 내부적으로 관리하는 상태나 메서드와 자동으로 연동되기 때문에 Formik을 사용할 때는 Form 컴포넌트를 함께 사용하는 것이 좋다.
<Field>
기존의 <input> 태그를 대신하는 컴포넌트이다.
<Field name='email' type='email' component='input' />name : 입력 값의 이름, initialValues 객체의 키와 일치해야 해당 값과 연결됨 type :<input>에서 사용하는 타입과 동일 component : 어떤 HTML 요소를 렌더링할지 지정하는 속성(ex. input, textarea / 기본 input)
<ErrorMessage>
에러메시지를 표시해주는 컴포넌트이다.
<ErrorMessage name='email' component='div' />name : 오류 메시지를 표시할 대상 필드의 이름, initialValues 객체의 키와 일치해야함 component : 오류 메시지를 감쌀 HTML 태그 이름
