• 리액트 회원가입 폼 만들기 (React Hook Form + Antd + yup)

    2021. 3. 4.

    by. kimyang-Sun

    리액트 (React.js)

     

    리액트로 회원가입과 같은 폼(Form)들을 만들기 위해 여러 고민을 한 결과

    직접 하나하나 구현해서 할 수도 있겠지만 살짝 비효율적이라는 생각이 들어 라이브러리를 이용하여 구현했습니다.

     

    React Hook Form 라이브러리를 베이스로 하였고 디자인 ui는 Ant Desgin을 이용하였습니다.

    antd 자체에도 antd만의 Form 유효성 검사가 따로 있는것 같지만

    저는 React Hook Form이 기능, 성능적으로 더 좋다고 생각하여 antd는 디자인 ui만 이용했습니다.

     

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

     

    Home

    React hook for form validation without the hassle

    react-hook-form.com

     

    ant.design/

     

    Ant Design - The world's second most popular React UI framework

     

    ant.design

     

    yarn add react-hook-form // React Hook Form
    yarn add antd // Ant Design
    

    npm 또는 yarn 으로 antd와 react-hook-form을 설치해주고 코드를 작성해줍니다.

     

    // Types
    type SignUpInputType = {
      userId: string;
      nickname: string;
      password: string;
      password2: string;
      term: boolean;
    };

     

    우선 저는 타입스크립트로 작업했기에 회원가입에서 받아올 정보들의 타입을 정해주었습니다.

     

    const { handleSubmit, errors, control } = useForm<SignUpInputType>({
      resolver: yupResolver(signUpValidation),
      mode: 'onBlur',
    });
    
    const onSubmit = handleSubmit((data: SignUpInputType) => {
      console.log(data);
    });

     

    만들어준 타입을 useForm에 넣어줍니다.

    나머지 코드들은 React Hook Form 공식사이트에도 설명이 아주 잘 되어있는데

    yupResolver는 yup을 React Hook Form과 같이 사용하기 위해 설치해주어야 합니다.

     

    yarn add yup // yup 설치
    
    yarn add @hookform/resolvers // resolvers 설치
    
    // 만약 resolvers가 오류가 난다면 1.3.0 버전 설치
    yarn add @hookform/resolvers@1.3.0

     

     

    // yup.ts
    
    import * as yup from 'yup';
    
    export const signUpValidation = yup.object({
      userId: yup
        .string()
        .required('아이디를 입력해주세요.')
        .max(12, '아이디는 12자리 이하여야 합니다.')
        .min(4, '아이디는 4자리 이상이어야 합니다.'),
      nickname: yup
        .string()
        .required('닉네임을 입력해주세요.')
        .max(15, '닉네임은 15자리 이하여야 합니다.')
        .min(2, '닉네임은 2자리 이상이어야 합니다.'),
      password: yup
        .string()
        .required('비밀번호를 입력해주세요.')
        .max(15, '비밀번호는 15자리 이하여야 합니다.')
        .min(4, '비밀번호는 4자리 이상이어야 합니다.'),
      password2: yup
        .string()
        .oneOf([yup.ref('password'), null], '비밀번호가 일치하지 않습니다.'),
      term: yup.boolean().oneOf([true], '약관에 동의해주세요.'),
    });

     

    유효성 검사는 yup 라이브러리를 이용했습니다. 정말 편한 라이브러리라고 생각했어요.

     

    // SignupForm.tsx
    
    import React from 'react';
    import styled from 'styled-components';
    import { Button, Checkbox, Form, Input } from 'antd';
    import { Controller, useForm } from 'react-hook-form';
    import { yupResolver } from '@hookform/resolvers/yup';
    import { signupValidation } from 'src/yup';
    import FormErrorMessage from 'components/FormErrorMessage';
    
    // Types
    type SignUpInputType = {
      userId: string;
      nickname: string;
      password: string;
      password2: string;
      term: boolean;
    };
    
    // styled components
    const StyledSignUpForm = styled(Form)`
      > div:not(:first-child) {
        margin-top: 30px; // ID 인풋박스만 제외하고
      }
    
      > div:not(:last-child) {
        position: relative; // 버튼 박스만 제외하고
      }
    
      > div > label {
        display: inline-block;
        padding-bottom: 8px;
      }
    `;
    
    // export
    function SignUpForm() {
      const { handleSubmit, errors, control } = useForm<SignUpInputType>({
        resolver: yupResolver(signUpValidation),
        mode: 'onBlur',
      });
    
      const onSubmit = handleSubmit((data: SignUpInputType) => {
        console.log(data);
      });
    
      return (
        <StyledSignUpForm onFinish={onSubmit} size="large">
          <div>
            <label htmlFor="userId">아이디</label>
            <Controller
              as={<Input />}
              type="text"
              name="userId"
              control={control}
              placeholder="아이디를 입력해주세요."
              defaultValue=""
            />
            {errors.userId && (
              <FormErrorMessage errorMessage={errors.userId.message} />
            )}
          </div>
          <div>
            <label htmlFor="nickname">닉네임</label>
            <Controller
              as={<Input />}
              type="text"
              name="nickname"
              control={control}
              placeholder="닉네임을 입력해주세요."
              defaultValue=""
            />
            {errors.nickname && (
              <FormErrorMessage errorMessage={errors.nickname.message} />
            )}
          </div>
          <div>
            <label htmlFor="password">비밀번호</label>
            <Controller
              as={<Input />}
              type="password"
              name="password"
              control={control}
              placeholder="비밀번호를 입력해주세요."
              defaultValue=""
            />
            {errors.password && (
              <FormErrorMessage errorMessage={errors.password.message} />
            )}
          </div>
          <div>
            <label htmlFor="password2">비밀번호</label>
            <Controller
              as={<Input />}
              type="password"
              name="password2"
              control={control}
              placeholder="비밀번호를 확인해주세요."
              defaultValue=""
            />
            {errors.password2 && (
              <FormErrorMessage errorMessage={errors.password2.message} />
            )}
          </div>
          <div>
            <Controller
              name="term"
              control={control}
              defaultValue={false}
              render={({ onChange, value }) => (
                <Checkbox
                  onChange={e => onChange(e.target.checked)}
                  checked={value}
                >
                  약관에 동의합니다.
                </Checkbox>
              )}
            />
            {errors.term && <FormErrorMessage errorMessage={errors.term.message} />}
          </div>
          <div>
            <Button type="primary" htmlType="submit" block>
              가입하기
            </Button>
          </div>
        </StyledSignUpForm>
      );
    }
    
    export default SignUpForm;

     

    코드가 좀 기네요. Controller의 props들 때문에 길어보이는거 같네요 😅

    그치만 저는 React Hook Form과 Antd를 함께 같이 사용하기 위해 이렇게 만들었습니다.

    이유는 React Hook Form 공식사이트에서는 해당 라이브러리와 antd나 material-ui 등과 같은 ui 라이브러리들과 같이 사용할 때,

    Controller를 이용하여 ui 라이브러리들의 컴포넌트와 쉽게 조합하여 사용하는것을 추천하기 때문입니다.

     

    에러 메세지는 FormErrorMessage라는 재사용하기위한 컴포넌트를 만들어줬습니다.

     

    // FormErrorMessage.tsx
    
    import React from 'react';
    import styled from 'styled-components';
    // Types
    type FormErrorMessage = {
      errorMessage: string;
    };
    
    // styled conponents
    const StyledFormErrorMessage = styled.div`
      position: absolute;
      padding-left: 5px;
      color: ${props => props.theme.color.main};
    `;
    
    // export
    function FormErrorMessage({ errorMessage }: FormErrorMessage) {
      return <StyledFormErrorMessage>{errorMessage}</StyledFormErrorMessage>;
    }
    
    export default FormErrorMessage;

     

    React Hook Form을 사용한 가장 큰 이유 중 하나는 리액트에서 일반적인 Form을 다룰 때,

    useState 등으로 input 값들을 따로 하나하나 만들어놓고

    onChange 함수를 각각 넣어주고 setState를 하여 하나하나 업데이트를 해야하는데 그렇게되면 이렇게 input이 많은 경우에는

    중복되는 코드가 많아져 따로 중복되는 코드들을 합치는 작업을 해야하는 번거로움이 있기 때문입니다.

     

    "React Hook Form은 비제어 컴포넌트를 활용하기 때문에 onCHange를 사용하여 state를 바꾸고, 그 값을 인풋의 value에 반영해줄 필요가 없습니다. 따라서 value 자체가 필요 없습니다. 사실 초기 값을 지정하고자 할 대 defaultValue만 넣어주면 됩니다." _공식사이트

     

    예외로 만약 지정된 인풋을 관찰하고, 값들을 반환하며 렌더링할 대상을 결정해야 할때는 watch라는 기능이 또 따로 있기 때문에 그걸 사용하면 될 것 같습니다.

     

    기본 상태

     

    유효성 검사 불통과

     

    유효성 검사 통과

     

    이제 가입하기 버튼을 누릅니다.

     

     

    객체로 잘 Submit 되는것을 확인할 수 있습니다.

    댓글