리액트의 모든 UI의 업데이트는 상태변경으로부터 발생해야한다.
그러나 입력폼의 경우 내부적으로 상태변경이 발생하지 않아도 바로 UI가 변경된다.
이를 uncontrolled component
즉 통제되지 않는 컴포넌트라고한다.
따라서 입력폼에 보여지고 있는 data
- 리액트의 상태
가 matching 되도록 만들어 제어 컴포넌트로 폼을 다루는 것이 중요하다.
React 에서 제어 컴포넌트란 React를 통해 제어하게 되는 컴포넌트를 말한다.
import React, { useState } from 'react';
const ContorlledForm = () => {
const [name, setName] = useState('');
const [id, setId] = useState('');
const [password, setPassword] = useState('');
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
return <form></form>;
};
export default ContorlledForm;
import React, { useState } from 'react';
const ContorlledForm = () => {
const [name, setName] = useState('');
const [id, setId] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({
name: {
invalid: false,
message: '이름이 너무 깁니다.',
},
id: {
invalid: false,
message: 'id는 3글자 이상, 20글자 이하여야 합니다.',
},
password: {
invalid: false,
message: '비밀번호는 10자 이하여야 합니다.',
},
phone: {
invalid: false,
message: '전화번호 형식에 맞지 않습니다.',
},
});
const handleName = (e) => {
setName(e.currentTarget.value);
};
const handleId = (e) => {
setId(e.currentTarget.value);
};
const handlePassword = (e) => {
setPassword(e.currentTarget.value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (name.length > 10) {
setErrors((prev) => ({
...prev,
name: {
...prev.name,
invalid: true,
},
}));
}
if (id.length < 3 || id.length > 20) {
setErrors((prev) => ({
...prev,
id: {
...prev.id,
invalid: true,
},
}));
}
if (password.length > 10) {
setErrors((prev) => ({
...prev,
password: {
...prev.password,
invalid: true,
},
}));
}
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor='name'>name: </label>
<input
id='name'
type='text'
value={name}
onChange={handleName}
placeholder='이름을 입력해주세요'
/>
{errors.name.invalid ? <p>{errors.name.message}</p> : null}
<label htmlFor='id'>id: </label>
<input
id='id'
type='text'
value={id}
onChange={handleId}
placeholder='아이디를 입력해주세요'
/>
{errors.id.invalid ? <p>{errors.id.message}</p> : null}
<label htmlFor='pw'>password: </label>
<input
id='pw'
type='text'
value={password}
onChange={handlePassword}
placeholder='비밀번호를 입력해주세요'
/>
{errors.password.invalid ? <p>{errors.password.message}</p> : null}
<button>Submit</button>
</form>
);
};
export default ContorlledForm;
이렇게 제어 컴포넌트로 폼을 다루기 위해서 state를 모두 선언해주고, 해당 state를 다루기 위해 핸들링 함수를 만들고, 에러를 위한 state를 만들어주었다.
지금은 단순한 예제이기때문에 코드가 간소화 된 듯 하지만 실제로는 유효성 검증도 들어가기 때문에 더더욱 폭잡하고 긴 코드가 될 것 같다.
가장 큰 문제점은 모든 값이 state로 연결되어있어 하나의 값이 변할 때마다 해당 컴포넌트와 모든 자식 컴포넌트는 리렌더링이 발생하게 될 것이다.........💩
아주아주 불필요한 렌더링이라고 할 수 있다.
UI만 보면 정말 단순한 이 폼을 다루기 위해 너무 많은 state와 함수가 담겨져 있고
코드를 짤 때마다 이게 맞나 싶은 생각이 가장 많이 드는 부분이었던 것 같다.
그래서 form 라이브러리인 react-hook-form
을 사용해보기로 한다 !
현재 React form 라이브러리로는 react-hook-form
과 formik
이 사용되고 있다. react-hook-form의 지속적인 업데이트와 빠른 마운트 속도 탓인지 다운로드 횟수가 formik보다 50만 회나 높다. 그래서 나도 react-hook-form 을 사용해보기로 결정했다.
react-hook-form 설치
npm i react-hook-form
yarn add react-hook-form
👉 useForm 훅에는 파라미터로 객체를 넘길 수 있다.
export type UseFormProps<TFieldValues extends FieldValues = FieldValues, TContext = any> = Partial<{
mode: Mode;
reValidateMode: Exclude<Mode, 'onTouched' | 'all'>;
defaultValues: DefaultValues<TFieldValues>;
resolver: Resolver<TFieldValues, TContext>;
context: TContext;
shouldFocusError: boolean;
shouldUnregister: boolean;
shouldUseNativeValidation: boolean;
criteriaMode: CriteriaMode;
delayError: number;
}>;
const model = useForm({
mode: "onChange",
defaultValues: {},
});
👉 훅을 통해 리턴받는 값의 타입
export type UseFormReturn<TFieldValues extends FieldValues = FieldValues, TContext = any> = {
watch: UseFormWatch<TFieldValues>;
getValues: UseFormGetValues<TFieldValues>;
getFieldState: UseFormGetFieldState<TFieldValues>;
setError: UseFormSetError<TFieldValues>;
clearErrors: UseFormClearErrors<TFieldValues>;
setValue: UseFormSetValue<TFieldValues>;
trigger: UseFormTrigger<TFieldValues>;
formState: FormState<TFieldValues>;
resetField: UseFormResetField<TFieldValues>;
reset: UseFormReset<TFieldValues>;
handleSubmit: UseFormHandleSubmit<TFieldValues>;
unregister: UseFormUnregister<TFieldValues>;
control: Control<TFieldValues, TContext>;
register: UseFormRegister<TFieldValues>;
setFocus: UseFormSetFocus<TFieldValues>;
};
👉 직접 구현해보기
import React from 'react';
import { useForm } from 'react-hook-form';
const ReactHookForm = () => {
const {
register,
formState: { errors },
watch,
reset,
handleSubmit,
getValues,
setError,
setFocus,
} = useForm({
mode: 'onSubmit',
defaultValues: {
id: '',
name: '',
password: '',
email: '',
},
});
return <div></div>;
};
export default ReactHookForm;
useForm을 통해 컨트롤할 폼 객체를 리턴받아서 구조분해 한다.
이 때 register 함수를 사용하며, 이 함수로 input 요소를 다룰 수 있다.
<input
type='text'
{...register('email', {
pattern: {
value:
/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i,
message: '이메일 형식에 맞지 않습니다!',
},
})}
/>
register (a, b)
register("name", {required: true, minLength: 10});
register("name", {required: "해당 필드는 필수입니다.", minLength: {
value: 5,
message: "5글자 이상 입력해주세요."
}
});
register 함수에 validation을 넣어줬다면, 에러에 대한 정보는 formState 객체에서 찾을 수 있다.
formState는 이렇듯 errors에 대한 정보도 담기지만, 이 외에도 많은 유용한 정보를 담고있다.
참고 https://react-hook-form.com/api/useform/formstate/
비제어 컴포넌트로 폼을 구현할 때 조건에 따라 필드를 다르게 노출해야하는 경우가 생긴다. 이런 경우 watch 함수를 사용할 수 있다.
const {id, name, password, ...watch} = watch();
// 전체 필드를 리턴
const id = watch('id');
❗️주의
관찰하려는 필드에 defaultValue
를 주지 않는다면 초기값이 undefined로 관리된다.
react-hook-form에서 값을 추적할 수 있는 방법은 두 가지다.
1. watch
2. getValues
watch vs getValues
const handleEvent = (e) => {
const value = getValues('name');
setState(value);
}
폼을 통해 create 뿐 아니라 edit기능도 구현하게된다. 유저가 edit 버튼을 클릭해 정보를 수정하려한다면 비동기 데이터를 활용해 기존의 데이터를 폼에 뿌려줘야 한다.
이런 경우 reset이라는 함수를 활용할 수 있다.
useEffect(() => {
const data = fetch(api).then(res => res.json());
reset({
name: data.name,
id: data.id,
...data
})
}, []);
폼에서 데이터를 입력 후 '제출'버튼을 누른다.
이때 submit 이벤트가 발생하는데 서버에 데이터를 넘기기 전에 해당 데이터에 대한 검증을 할 필요가 있다.
이를 위해 form 요소의 onSubmit에 handleSubmit 함수를 넣어주고, 매개변수로 정의한 onSubmit함수를 넣어준다.
onSubmit 함수를 정의할 때 매개변수로 data라는 값을 받을 수 있는데
이 값은 사용자가 제출버튼을 클릭한 후 내려오는 최종으로 제출하는 데이터이다.
const { handleSubmit } = useForm();
const onSubmit = (data) => {
// data: 최종데이터
console.log(data);
}
...
return <form onSubmit={handleSubmit(onSubmit)} />
onSubmit
setError('name', {type: 'minLength', message: '3글자 이상 입력해주세요'});
setFocus('name');
이 외 더 유용하고 다양한 기능들이 많이 있다 👀👍
해당 글을 참고하여 작성되었습니다.
https://tech.osci.kr/2023/01/02/introduce-react-hook-form/