오늘은 내가 실무를 하면서 제일 맘에 들었던 라이브러리 중 하나인 React-hook-form에 대해서 알아보자.
개발을 진행하다보면 input 기능을 무조건 구현해야 할 일이 찾아온다. 그럴경우 쉽게 기능을 구현하기 위해 사용되어 지는 라이브러리이다.
input 기능을 구현하다보면 점점 코드가 더러워지는 것읋 볼수 있다.
validation check, state subscription, data 변형과 같은 구현하기 위함인데 이러한 것들을 굉장히 쉽게 구현 하게 해주고,
무엇보다 input value값을 구독하기 위해서 state를 사용하게 되는데 그렇게 되면 리렌더링이 계속 발생해서 성능적 이슈가 발생 할 수 있다.
이 React-hook-form은 해당 렌더링 이슈를 해결하기에 굉장히 좋은 대안이 될 수 있다.
이전에 'ThemeProvider를 이용해 다크모드 구현하기'에서 만든 프로젝트를 이어나가볼것이다.
React + TypeScript
React-hook-form 설치
npm install react-hook-form
form 컴포넌트 만들기
// src/components/newForm.tsx
function NewForm() {
return (
<NewFormStyle>
<p>newForm</p>
<input type="email" placeholder="email" className="input-field" />
<input
type="password"
placeholder="password"
className="input-field"
/>
</NewFormStyle>
);
}
const NewFormStyle = styled.div`
margin: 0 auto;
margin-top: 2rem;
width: 15rem;
border-radius: 8px;
background-color: lightgray;
text-align: center;
padding: 1rem 1rem;
.input-field {
width: 100%;
height: 2rem;
margin-top: 1rem;
}
`;
export default NewForm;
실행화면
위의 컴포넌트를 사용하여 React-hook-form을 알아보자
import styled from "styled-components";
import { useForm } from "react-hook-form";
type formDataType = {
email: string;
password: string;
}; // input 결과 값 type지정
function NewForm() {
const {
register, // input field 지정
handleSubmit, // onSubmit시에 결과값을 미리 validation을 한 후 안에 함수의 parmeter로 넘겨준다.
watch, // input값 subscription
formState: { errors }, // error 반환
} = useForm<formDataType>(); // 결과 값 type 넘겨주기
const onSubmit = (data: formDataType) => {
console.log(data);
};
console.log(watch("email")); // email 값이 바뀔때마다 log 찍기
return (
<NewFormStyle>
<p>newForm</p>
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="email"
defaultValue=""
placeholder="email"
className="input-field"
{...register("email")} // email field 지정
/>
<input
type="password"
placeholder="password"
className="input-field"
defaultValue=""
{...register("password", { required: true })} // password field 지정, required option
/>
{errors.password && (
<p className="error-text">패스워드는 필수 입력입니다!</p> // 에러가나면 에러메세지 노출
)}
<input type="submit" className="submit-button" />
</form>
</NewFormStyle>
);
}
const NewFormStyle = styled.div`
margin: 0 auto;
margin-top: 2rem;
width: 15rem;
border-radius: 8px;
background-color: lightgray;
text-align: center;
padding: 1rem 1rem;
.input-field {
width: 100%;
height: 2rem;
margin-top: 1rem;
}
.submit-button {
margin-top: 1rem;
padding: 0.5rem 1rem;
}
.error-text {
color: red;
}
`;
export default NewForm;
실행화면
위의 사진과 같이 email값을 구독하고 있어 바뀔때마다 log가 찍히고,
required option을 이용하여 password를 입력하지 않고 제출 시 에러 메세지가 노출되도록 구현하였다.
function NewForm() {
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<formDataType>({
mode: "onChange", // mode를 onChange로 하면 값이 변할때 마다 체크를 진행
});
const onSubmit = (data: formDataType) => {
console.log(data);
};
console.log(watch("email"));
return (
<NewFormStyle>
<p>newForm</p>
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="text"
defaultValue=""
placeholder="email"
className="input-field"
{...register("email", {
pattern: {
value: /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g, // email 정규표현식
message: "이메일 형식을 지켜주세요",
},
})}
/>
{errors.email && (
<p className="error-text">{errors.email.message}</p> // email error 메세지 띄우기
)}
<input
type="text"
placeholder="password"
className="input-field"
defaultValue=""
{...register("password", {
required: {
value: true,
message: "패스워드는 필수 속성 입니다.",
},
minLength: {
value: 5,
message: "패스워드의 최소 글자수는 5입니다.",
},
})}
/>
{errors.password && (
<p className="error-text">{errors.password.message}</p>
)}
<input type="submit" className="submit-button" />
</form>
</NewFormStyle>
);
}
실행화면
위의 사진과 같이 mode를 onChange를 해두었기 때문에 값이 바뀔때마다 email의 validation check와 password의 required 및 minLength를 체크한다.
위의 사진과 같이 모든 조건이 맞을때만 제출버튼이 클릭되어 결과 data가 log에 찍히게 된다.
type formDataType = {
email: string;
password: string;
language: string; // 새로운 field인 language type 지정
};
interface CustomInputProps {
label: "email" | "password" | "language";
register: UseFormRegister<formDataType>;
required: string | ValidationRule<boolean> | undefined;
} // custom input 컴포넌트 props의 type 지정
function CustomInput({ label, register, required }: CustomInputProps) {
return (
<>
<input
className="input-field"
placeholder="language"
defaultValue=""
{...register(label, { required })}
/>
</>
);
} // custom input 컴포넌트
function NewForm() {
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<formDataType>({
mode: "onChange",
});
const onSubmit = (data: formDataType) => {
console.log(data);
};
console.log(watch("email"));
return (
<NewFormStyle>
<p>newForm</p>
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="text"
defaultValue=""
placeholder="email"
className="input-field"
{...register("email", {
pattern: {
value: /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g,
message: "이메일 형식을 지켜주세요",
},
})}
/>
{errors.email && (
<p className="error-text">{errors.email.message}</p>
)}
<input
type="text"
placeholder="password"
className="input-field"
defaultValue=""
{...register("password", {
required: {
value: true,
message: "패스워드는 필수 속성 입니다.",
},
minLength: {
value: 5,
message: "패스워드의 최소 글자수는 5입니다.",
},
})}
/>
{errors.password && (
<p className="error-text">{errors.password.message}</p>
)}
<CustomInput
label="language"
register={register}
required={{ value: true, message: "언어는 필수 값입니다." }}
/> // custom input 컴포넌트 사용하기
{errors.language && (
<p className="error-text">{errors.language.message}</p>
)}
<input type="submit" className="submit-button" />
</form>
</NewFormStyle>
);
}
실행화면
위와 같이 Input을 따로 컴포넌트로 만들어서 사용하는 경우가 있을것이다.
새로운 입력 field를 추가할 때는 type에 유의하자!
type formDataType = {
email: string;
password: string;
language: string;
antdInput: string; // 새로운 field인 antdInput type 추가
};
interface CustomInputProps {
label: "email" | "password" | "language" | "antdInput";
register: UseFormRegister<formDataType>;
required: string | ValidationRule<boolean> | undefined;
}
function CustomInput({ label, register, required }: CustomInputProps) {
return (
<>
<input
className="input-field"
placeholder="language"
defaultValue=""
{...register(label, { required })}
/>
</>
);
}
function NewForm() {
const {
control, // UI library 사용을 위한 control 추가
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<formDataType>({
mode: "onChange",
});
const onSubmit = (data: formDataType) => {
console.log(data);
};
console.log(watch("email"));
return (
<NewFormStyle>
<p>newForm</p>
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="text"
defaultValue=""
placeholder="email"
className="input-field"
{...register("email", {
pattern: {
value: /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g,
message: "이메일 형식을 지켜주세요",
},
})}
/>
{errors.email && (
<p className="error-text">{errors.email.message}</p>
)}
<input
type="text"
placeholder="password"
className="input-field"
defaultValue=""
{...register("password", {
required: {
value: true,
message: "패스워드는 필수 속성 입니다.",
},
minLength: {
value: 5,
message: "패스워드의 최소 글자수는 5입니다.",
},
})}
/>
{errors.password && (
<p className="error-text">{errors.password.message}</p>
)}
<CustomInput
label="language"
register={register}
required={{ value: true, message: "언어는 필수 값입니다." }}
/>
{errors.language && (
<p className="error-text">{errors.language.message}</p>
)}
<Controller // UI libarary를 감싸주는 Controller
name="antdInput"
defaultValue=""
rules={{ // rule props를 이용하게 규칙지정
required: {
value: true,
message: "antd는 필수값입니다.",
},
}}
control={control}
render={({ field }: any) => (
<div className="input-field">
<Input.Search placeholder="antdInput" {...field} />
</div>
)} // antd의 Input.search 추가
/>
{errors.antdInput && (
<p className="error-text">{errors.antdInput.message}</p> // antd error check 추가
)}
<input type="submit" className="submit-button" />
</form>
</NewFormStyle>
);
}
실행화면
가끔식 UI 컴포넌트 라이브러리를 사용하여 입력창을 꾸밀때가 있는데 그런경우에도 React-hook-form은 Controller를 이용하여 지원한다.
가끔식 사용자에게 값을 받은 후 그것을 서버에서 원하는 형태로 값을 바꿔줘야하는 경우가 있다.
type checkBoxType = [{ personal: boolean }, { marketing: boolean }]; // 새로운 field의 배열 type 지정
type formDataType = {
email: string;
password: string;
language: string;
antdInput: string;
checkBox: checkBoxType; // 새로운 field 추가
};
interface CustomInputProps {
label: "email" | "password" | "language" | "antdInput" | "checkBox";
register: UseFormRegister<formDataType>;
required: string | ValidationRule<boolean> | undefined;
}
function CustomInput({ label, register, required }: CustomInputProps) {
return (
<>
<input
className="input-field"
placeholder="language"
defaultValue=""
{...register(label, { required })}
/>
</>
);
}
function NewForm() {
const {
control,
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<formDataType>({
mode: "onChange",
});
const onSubmit = (data: formDataType) => {
console.log(data);
};
console.log(watch("email"));
return (
<NewFormStyle>
<p>newForm</p>
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="text"
defaultValue=""
placeholder="email"
className="input-field"
{...register("email", {
pattern: {
value: /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g,
message: "이메일 형식을 지켜주세요",
},
})}
/>
{errors.email && (
<p className="error-text">{errors.email.message}</p>
)}
<input
type="text"
placeholder="password"
className="input-field"
defaultValue=""
{...register("password", {
required: {
value: true,
message: "패스워드는 필수 속성 입니다.",
},
minLength: {
value: 5,
message: "패스워드의 최소 글자수는 5입니다.",
},
})}
/>
{errors.password && (
<p className="error-text">{errors.password.message}</p>
)}
<CustomInput
label="language"
register={register}
required={{ value: true, message: "언어는 필수 값입니다." }}
/>
{errors.language && (
<p className="error-text">{errors.language.message}</p>
)}
<Controller
name="antdInput"
defaultValue=""
rules={{
required: {
value: true,
message: "antd는 필수값입니다.",
},
}}
control={control}
render={({ field }: any) => (
<div className="input-field">
<Input.Search placeholder="antdInput" {...field} />
</div>
)}
/>
{errors.antdInput && (
<p className="error-text">{errors.antdInput.message}</p>
)}
<div className="input-field"> // 새로운 field 1(register를 주목!!)
<label>개인정보 수집 동의</label>
<input
type="checkbox"
{...register("checkBox.0.personal", {
required: {
value: true,
message: "개인정보 동의는 필수입니다.",
},
})}
/>
</div>
<div className="input-field"> // 새로운 field 2(register를 주목!!)
<label>광고 및 마케팅 동의</label>
<input
type="checkbox"
{...register("checkBox.1.marketing", {
required: {
value: true,
message: "마켓팅 동의는 필수입니다.",
},
})}
/>
</div>
<input type="submit" className="submit-button" />
</form>
</NewFormStyle>
);
}
실행화면
위의 사진과 같이 동의 checkBox 정보를 배열 형태로 바꾸어서 값을 보내는 것이 가능해진다.
이렇게 오늘은 React-hook-form에 대해서 알아보았다.
사실 이것보다 훨씬 많은 편리한 기능을 가지고 있다. 하지만 그것을 다하기에는 너무 많은 시간이 걸리고, 글이 너무 길어지기 때문에 계속 써보면서 알아보도록 하자.
다행히도 이 라이브러리는 공식 사이트를 운영하고 있어, 설명과 예시코드가 매우 잘 되어 있다. 그러니 한번 쯤 공식 사이트에 들어가서 보는것을 추천한다.
그런 이유에서도 실무에서 굉장히 유용하게 썻던 기억이 난다.
참조
https://react-hook-form.com/get-started#Designandphilosophy