-
리액트로 회원가입과 같은 폼(Form)들을 만들기 위해 여러 고민을 한 결과
직접 하나하나 구현해서 할 수도 있겠지만 살짝 비효율적이라는 생각이 들어 라이브러리를 이용하여 구현했습니다.
React Hook Form 라이브러리를 베이스로 하였고 디자인 ui는 Ant Desgin을 이용하였습니다.
antd 자체에도 antd만의 Form 유효성 검사가 따로 있는것 같지만
저는 React Hook Form이 기능, 성능적으로 더 좋다고 생각하여 antd는 디자인 ui만 이용했습니다.
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 되는것을 확인할 수 있습니다.
'코딩 기록 > 리액트' 카테고리의 다른 글
리액트 리덕스 툴킷 - 리덕스 사가 (React + Redux Toolkit + Redux Saga + TypeScript) - with Next.js (1) 2021.03.08 리액트 리덕스 툴킷으로 상태관리하기 (React + Redux Toolkit + TypeScript) - with Next.js (0) 2021.03.04 (React) 리액트 아이콘(react-icons) 사용하기 (0) 2021.02.20 리액트 미니 안경 쇼핑몰 - Glasses Shop App (React, Firebase) (2) 2021.01.21 리액트 투두리스트 앱 - TODO List App (React.js) (0) 2020.12.20 댓글