거의 알고리즘 일기장

react-hook-form + yup을 이용한 손쉽고 간편하고 세계가 놀라고 일본이 경악하는 validation check 본문

react-native

react-hook-form + yup을 이용한 손쉽고 간편하고 세계가 놀라고 일본이 경악하는 validation check

건우권 2023. 8. 2. 21:03

어그로 죄송합니다

이번에 설명할 두가지 라이브러리는 간단하게 input들의 관리가 가능하게 도와준다.

이 친구들을 만나고 다양하게 빡치던 input validation check 로직의 쾌적함이 달라졌다.


밑의 설명을 읽어도 되지만, 여기까지 읽었는데 혹한다 싶으면 그냥 공식문서 읽으러 가길 바란다.

https://react-hook-form.com/get-started

 

Get Started

Performant, flexible and extensible forms with easy-to-use validation.

react-hook-form.com

https://github.com/jquense/yup

 

GitHub - jquense/yup: Dead simple Object schema validation

Dead simple Object schema validation. Contribute to jquense/yup development by creating an account on GitHub.

github.com


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의 개선이므로 다들 써봤으면 좋겠다. 

(혹시.. 나만 안썼던건 아니쥬?ㅠ..)

반응형
Comments