거의 알고리즘 일기장
react-hook-form + yup을 이용한 손쉽고 간편하고 세계가 놀라고 일본이 경악하는 validation check 본문
react-hook-form + yup을 이용한 손쉽고 간편하고 세계가 놀라고 일본이 경악하는 validation check
건우권 2023. 8. 2. 21:03
이번에 설명할 두가지 라이브러리는 간단하게 input들의 관리가 가능하게 도와준다.
이 친구들을 만나고 다양하게 빡치던 input validation check 로직의 쾌적함이 달라졌다.
밑의 설명을 읽어도 되지만, 여기까지 읽었는데 혹한다 싶으면 그냥 공식문서 읽으러 가길 바란다.
https://react-hook-form.com/get-started
https://github.com/jquense/yup
react-hook-form과 yup이 뭔가여?
react-hook-form
react는 기본적으로 제어 컴포넌트이고 제어 컴포넌트로 작성하는것을 추천한다. 또 antd, mui 등의 ui 라이브러리들도 다 제어 컴포넌트 기반이다.
하지만, 회사생활을 하다보면 어드민을 만들어본 경험이 다들 있을것이다.
무수히 많은 input과 validation check와 다양한 error message를 다 컨트롤하려다 보면 어질어질해진다.
(state 변경으로 인한 rerender가 미치도록 되는 문제도 있다.)
그래서 이런경우에는 비제어로 가는걸 추천한다. 그리고 이때 react-hook-form을 같이 사용하면 매우매우 편하다.
yup
react-hook-form의 rule과 정규식을 통해서 validation을 관리해도 되나 yup을 쓰면 훨씬 관리가 편하다.
// 이런식으로 schema를 정의해놓고
const userSchema = object({
name: string().required("이름이 필요합니다."),
age: number().required("나이가 필요합니다.").positive().integer(),
email: string().email("이메일 형식이 아닙니다"),
website: string().url("url 형식이 아닙니다.").nullable(),
createdOn: date().default(() => new Date()),
});
// hookform/resolvers 모듈에 있는 yupResolver를 넣어주면 error message를 훨씬 간편하게 관리할수있다.
import { yupResolver } from '@hookform/resolvers/yup';
const methods = useForm({
//...
resolver: yupResolver(signUpSchema),
});
그러므로 react-hook-form + yup을 같이 쓰면 코드관리가 편하다.
정리하자면
1. 제어 컴포넌트로 작성된 antd, mui.. 등의 다양한 input들을 비제어 컴포넌트로 다룰수 있다. -> react-hook-form
2. 그로인해 rerender performence 이슈가 있다면 개선됨 -> react-hook-form
(rerender는 나쁜게 아니지만 가끔 너무 잦으면 ux에 악영향을 끼치기도 한다)
3. validation check와 그에 따른 error message를 컨트롤하기 편함 -> react-hook-form
4. rule을 하나하나 작성하면 코드가 난잡해지는데, 이때 yup으로 미리 schema를 작성해놓고 resolver로 hook-form에 연동해주면 쾌적한 코드 구성이 가능함 -> yup
더 다양한 목적이 있을수도 있지만, 대부분의 use case는 이렇지 않을까 싶다.
간단한 데모
데모 코드
screen
import { Button } from '@rneui/themed';
import React from 'react';
import { FormProvider } from 'react-hook-form';
import { StyleSheet, View } from 'react-native';
import RHFInput from '@/components/hook-form/RHFInput';
import Space from '@/constants/Space';
import useSignUp from '@/screens/SignUp/useSignUp';
const SignUpScreen = () => {
const { methods, onSubmit } = useSignUp();
return (
<FormProvider {...methods}>
<View style={styles.container}>
{/* form */}
<View style={styles.inputGroup}>
<RHFInput
autoFocus
name={'email'}
placeholder={'이메일'}
helperText={'이메일 형식으로 입력해주세요'}
/>
<RHFInput
name={'password'}
placeholder={'비밀번호'}
helperText={'영문/숫자/특수문자가 최소 1개 이상, 8자 이상'}
secureTextEntry={true}
/>
<RHFInput
name={'passwordConfirm'}
placeholder={'비밀번호 확인'}
helperText={'비밀번호를 한번 더 입력해주세요'}
secureTextEntry={true}
/>
</View>
{/* submit */}
<Button onPress={onSubmit}>회원가입</Button>
</View>
</FormProvider>
);
};
const styles = StyleSheet.create({
container: {
paddingHorizontal: Space.md,
marginTop: Space.md,
gap: Space.md,
},
inputGroup: {
gap: Space.sm,
},
});
export default SignUpScreen;
custom hook
import { yupResolver } from '@hookform/resolvers/yup';
import { useRouter } from 'expo-router';
import { useForm } from 'react-hook-form';
import Toast from 'react-native-toast-message';
import { useMutation } from 'react-query';
import { signUp } from '@/api/auth';
import { signUpSchema } from '@/screens/SignUp/schema';
import { useAuthStore } from '@/store/auth';
const useSignUp = () => {
const router = useRouter();
const signIn = useAuthStore((state) => state.signIn);
const { mutate: signUpMutate } = useMutation(signUp, {
onSuccess: ({ token }) => {
signIn({ token });
router.back();
},
onError: () => {
Toast.show({
type: 'error',
text1: '회원가입 실패',
});
},
});
const methods = useForm<{
email: string;
password: string;
passwordConfirm: string;
}>({
defaultValues: {
email: '',
password: '',
passwordConfirm: '',
},
resolver: yupResolver(signUpSchema),
});
const onSubmit = methods.handleSubmit(async (data) => {
signUpMutate(data);
});
return {
methods,
onSubmit,
};
};
export default useSignUp;
yup schema
import { object, ref, string } from 'yup';
export const signUpSchema = object({
email: string()
.required('이메일은 필수 입력입니다.')
.email('이메일 형식이 올바르지 않습니다.'),
password: string()
.required('비밀번호는 필수 입력입니다.')
.min(8, '비밀번호는 최소 8글자 이상이어야 합니다.')
.matches(
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()\-_=+{};:,<.>]).*$/,
'비밀번호는 영문, 숫자, 특수문자를 최소 1개씩 포함해야 합니다.',
),
passwordConfirm: string()
.required('비밀번호 확인은 필수 입력입니다.')
.oneOf([ref('password')], '비밀번호가 일치하지 않습니다.'),
});
여기서 RHFInput은 react native element ui를 react-hook-form이랑 같이 사용할때 좀 편하려고 만든 컴포넌트다.
import { ErrorMessage } from '@hookform/error-message';
import { Input, InputProps, useTheme } from '@rneui/themed';
import React from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { StyleSheet, View } from 'react-native';
import StyledText from '@/components/common/StyledText';
type RHFInputProps = Omit<InputProps, 'ref'> & {
name: string;
helperText?: string;
};
/**
* @description react hook form + react native elements input
*/
const RHFInput = ({ name, helperText, ...rest }: RHFInputProps) => {
const {
control,
formState: { errors },
} = useFormContext();
const { theme } = useTheme();
return (
<View style={styles.container}>
<Controller
control={control}
render={({ field: { onChange, onBlur, value } }) => (
<Input
onChangeText={onChange}
onBlur={onBlur}
value={value}
renderErrorMessage={false}
{...rest}
/>
)}
name={name}
/>
{helperText && (
<View style={styles.textContainer}>
<StyledText
style={{
...styles.helperText,
color: theme.colors.grey4,
}}
>
{helperText}
</StyledText>
</View>
)}
<ErrorMessage
errors={errors}
name={name}
render={({ message }) => (
<View style={styles.textContainer}>
<StyledText
style={{
...styles.errorText,
color: theme.colors.error,
}}
>
{message}
</StyledText>
</View>
)}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {},
textContainer: {
marginTop: 5,
},
helperText: {},
errorText: {
fontSize: 12,
},
});
export default RHFInput;
후기
이게 진짜 편하긴 하다.
엄청난 dx의 개선이므로 다들 써봤으면 좋겠다.
(혹시.. 나만 안썼던건 아니쥬?ㅠ..)