[react] 회원가입 폼 만들기 #3 - react-hook-form 라이브러리

2023. 2. 8. 01:19
728x90
반응형

프롤로그

react 만으로는 해결하지 못했던 상태관리 공유 + 리랜더링 최적화 를 고민했지만 사실 답을 찾기 어려웠다. (답을 찾으면 다시 글로 정리하겠다) 리액트를 쓰며 똑같은 고민을 한 사람들이 조금 더 편하게 쓰기위해 라이브러리를 만들어 놓지 않았을까? 라이브러리의 작동방식을 파헤쳐보는 것이 결국 리액트에서 막혔던 부분들에 대한 답이 아닐까 싶다.
우선은 회원가입 폼을 구현하기 위해 react-hook-form 라이브러리르 써보기로 했다. [그리디브] 팀원의 도움을 받아 옆에서 그의 사고흐름을 지켜보기로 했다.

기술 스택은
react next.js typescript emotion

어떻게 적용할 것인가?

일단 팀원<야잴>이 알려준 팁은 모르면 구글 검색을 하는데 하나의 글만 보지말고 최신순으로 그 페이지 내에 있는 글을 모두 보는 것이다. 하나의 글만 보고 하게 되면 모르는 부분이 있을 뿐더러 오류가 나게 되면 다른 문제로 빠질 경우가 많기 때문에 페이지에 있는 다른 글들을 모두 보는 것을 추천한다고 한다.

날짜도 중요하다. js 라이브러리들은 update 속도가 빠르기 때문에 최신순을 보는 것이 좋다.

공식문서에서 먼저 보는것이 가장 좋다. 공식문서는 라이브러리를 만든 사람의 의도가 들어있기 때문에 공식문서 보는 것을 두려워 하지 말자. 다양한 예들도 많기 때문에 내가 구현하고자 하는 기능들과 비슷 한 예들을 찾아보자.

버전확인을 하자 공식문서를 볼때 docs 에서의 버전확인은 필수사항이다. 내부 설정같은 세밀한 사항들이도 바뀌고, 종속성 있는 버전들도 업그레이드 되기 때문이라고 한다. 버전이 다르면 구현이 안되는 경우도 있다. 버전일치도 확인해 주자.

react-hook-form

react-hook-form 공식문서

초기에 작성했던 코드와 다르게 뭔가 간단해졌다. 블로그와 공식문서를 따라가며 하나씩 치환작업을 진행했다.

1. 각각의 타입을 지정해 주었다.

interface ISignUpForm {
  email: string;
  name: string;
  pw: string;
  checkPw: string;
  phone: string;
  birth: string;
}

2. useForm()이라는 커스텀 훅

상태관리는 useForm() 이 담당해 주는 것 같다. form 을 관리하기 위한 커스텀 훅을 react-hook-form 에서 제공한다. 등록 제출 에러처리 를 관리해준다. 여기서 상태가 변하면 resolver 가 yupResolver 를 통해 schema 로 전달해준다.

const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm<ISignUpForm>({
  mode: "onChange",
  reValidateMode: "onChange",
  resolver: yupResolver(schema),
});

3. schema 에서 유효성 검사를 진행해 준다.

yup 이라는 라이브러리도 설치해 줘야 한다 .
yup은 정규식을 조금 쉽게 처리 할 수 있게 도와주는 라이브러이다.
resolver 로 받아온 값들을 schema 에서 받아 유효성 검사를 실시해 준다.
이부분에서는 값 비교도 가능하다. 값을 그대로 가져와서 비밀번호, 비밀번호 확인까지 진행 가능하다. (내가 원하던 부분을 충족시켜줬다.)

const schema = yup.object().shape({
    email: yup.string().email().required(),
    name: yup.string().min(2).max(10).required(),
    pw: yup.string().matches(regex.pw).required(),
    checkPw: yup
      .string()
      .oneOf([yup.ref("pw"), null])
      .required(),
    phone: yup.string().matches(regex.phone).required(),
    birth: yup.string().required(),
  });

4. 컴포넌트 분리

상태관리를 라이브러리를 써서 해결 되니 component 도 분리가 가능해졌다.
<TextField> 컴포넌트로 분리 해주고 값들은 props 로 받는다. 각각의 타입도 지정해 준다. 각각의 타입은 코드 v1.1.0 에서 확인하자 :-)

const TextField = ({
  text,
  name,
  inputType = "text",
  register,

  errorMsg,
  ...others
}: IProps) => {
  console.log("in TextField", others);
  return (
    <div css={[fieldWrapper]}>
      <label htmlFor={name}>{text}</label>
      <input type={inputType} {...register(name)} css={[inputStyle]} />
      {errorMsg && <span css={[errorMsgStyle]}>{errorMsg}</span>}
    </div>
  );
};

결과

코드 v1.1.0

그래서 전체 코드를 보면 이렇다. emotion 으로 css 도 곁들였다.

signUp.tsx

import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm, SubmitHandler } from "react-hook-form";
import { regex } from "@lib/utils";
import { useEffect, useRef, useState } from "react";
import { TextField } from "components/molecules";
import { css } from "@emotion/react";

interface ISignUpForm {
  email: string;
  name: string;
  pw: string;
  checkPw: string;
  phone: string;
  birth: string;
}

const SignUp = () => {
  const schema = yup.object().shape({
    email: yup.string().email().required(),
    name: yup.string().min(2).max(10).required(),
    pw: yup.string().matches(regex.pw).required(),
    checkPw: yup
      .string()
      .oneOf([yup.ref("pw"), null])
      .required(),
    phone: yup.string().matches(regex.phone).required(),
    birth: yup.string().required(),
  });

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<ISignUpForm>({
    mode: "onChange",
    reValidateMode: "onChange",
    resolver: yupResolver(schema),
  });

  const submitForm: SubmitHandler<ISignUpForm> = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(submitForm)} css={[formWrapper]}>
      <TextField
        text={"이메일"}
        name={"email"}
        inputType={"email"}
        errorMsg={errors.email && "이메일 형식이 맞지 않습니다."}
        register={register}
      />
      <TextField
        text={"닉네임"}
        name={"name"}
        errorMsg={errors.name && "2글자 이상 입력해주세요."}
        register={register}
      />
      <TextField
        text={"비밀번호"}
        name={"pw"}
        errorMsg={
          errors.pw && "숫자+영문자+특수문자 조합으로 8자리 이상 입력해주세요!"
        }
        register={register}
      />
      <TextField
        text={"비밀번호 확인"}
        name={"checkPw"}
        errorMsg={errors.checkPw && "떼잉~ 비밀번호가 똑같지 않아요!"}
        register={register}
      />
      <TextField
        text={"전화번호"}
        name={"phone"}
        errorMsg={errors.phone && "올바른 형식이 아닙니다!"}
        register={register}
      />
      <TextField
        text={"생년월일"}
        name={"birth"}
        inputType="date"
        errorMsg={errors.birth && "생년월일을 입력해주세요."}
        register={register}
      />
      <button type="submit">회원가입</button>
    </form>
  );
};

export default SignUp;

const formWrapper = css`
  width: 500px;
  border: 1px solid #ddd;
  border-radius: 10px;
  margin: 0 auto;
  padding: 50px;
`;

{ TextField } from "components/molecules"

import { css } from "@emotion/react";
import styled from "@emotion/styled";
import React from "react";
import { Path, UseFormRegister } from "react-hook-form";
interface TSignUpFieldValues {
  email: string;
  name: string;
  pw: string;
  checkPw: string;
  phone: string;
  birth: string;
}
interface IProps {
  text: string;
  inputType?: string;

  name: Path<TSignUpFieldValues>;
  register: UseFormRegister<TSignUpFieldValues>;
  errorMsg?: string;
}
const TextField = ({
  text,
  name,
  inputType = "text",
  register,

  errorMsg,
  ...others
}: IProps) => {
  console.log("in TextField", others);
  return (
    <div css={[fieldWrapper]}>
      <label htmlFor={name}>{text}</label>
      <input type={inputType} {...register(name)} css={[inputStyle]} />
      {errorMsg && <span css={[errorMsgStyle]}>{errorMsg}</span>}
    </div>
  );
};

export default TextField;

const fieldWrapper = css`
  padding: 10px;
  margin: 10px;
  padding: 10px;
  display: flex;
  align-items: flex-start;
  flex-direction: column;
  text-align: left;
`;

const inputStyle = css`
  outline: none;
  padding: 10px 0px;
  width: 100%;
  border: none;
  border-bottom: 1px solid #ddd;
  margin-bottom: 5px;
`;

const errorMsgStyle = css`
  font-size: 12px;
  color: #f00;
  line-height: 1.4;
`;

에필로그

react-hook-form 은 form 을 만드는데 최적화 된 라이브러리가 아닐까 생각해보지만 라이브러리를 쓸때마다 제대로 알지 못했다는 느낌이 드는 것은 사실이다. 앞서 말했던 것처럼 이 라이브러리의 작동 방식이 결국 리액트로 풀지 못한 해답일텐데 말이다. 모르는 문제는 검색을 하고 코드를 붙여 넣어보고 하며 뚝딱뚝딱 만들어 해결은 가능하나 원론적인 문제는 해결하지 못하는게 좀 아쉽다. (다시말하지만 해결하면 다시 글을 쓰겠다)

팀원<야잴> 이 해결하는 방식을 옆에서 보는것은 흥미로웠다. 나보다 잘 하는 사람들은 어떤식으로 문제를 해결하는 지 보고 좋은 점은 흡수 해버릴 수 있으니까 말이다. 또한 일주일에 한번씩 (각자 자기가 맡은 역할을 해온 후에) 하는 미팅은 어떻게 코드를 리뷰하고 서로 의사소통을 하는지에 대해서 많이 알 수 있는 시간이다. 주로 나는 듣기만 하지만 연차 쌓인 경력자들을 옆에 두고 함께 프로젝트를 하는 것은 정말이지 운이 좋지 않을 수 없다.

 
728x90
반응형