React hook form 사용해보기

김현진·2022년 6월 8일
0

form은 사용자가 웹페이지를 사용하면서 서로 상호작용하는 필수적인 요소 중 하나입니다. form을 통해 전달된 사용자 데이터의 유효성을 검사하는 것은 개발자에겐 중요합니다.

React Hook Form은 React에서 양식을 검증하는데 도움이 되는 라이브러리 중 하나입니다. 사용이 간단하고 코드양이 현저히 줄어듭니다.

사용해보기

사용하는 패키지
typescript
react
react-hook-form
styled-components

import { useForm } from "react-hook-form";
const { register, handleSubmit } = useForm();


.
.
.
.
<input
  {...register("email", {
  required: "필수정보입니다.",
  pattern: {
  value:
  /^[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*\.	[a-zA-Z]{2,3}$/,
	message: "이메일형식으로 기입해주세요.",
  },
  onBlur: () => {
  validateEmail();
  },
 })}

useForm은 몇가지 속성을 포함하는 개체를 반환합니다.

register

register를 사용하면 유효성 검사를 할 수 있도록 해주고 해당 값을 추적할 수 있도록 도와줍니다. 첫번째 파라미터 값으로 이름을 지정 할 수 있고, 두번째 파라미터는 옵션값인데 옵션값에 대해서 알아보겠습니다.

  • required 속성은 필수항목 인지 확인하는 옵션이고 boolean 또는 string으로 값을 설정할 수 있는데 string으로 넣으면 자동으로 true로 설정이 되고 string으로 설정한 문자열이 나중에 에러 message로 사용가능 합니다.

  • pattern 속성은 정규표현식으로 유효성 검사를 가능합니다. value 속성값에 정규식을 넣고 message에 어떤 에러 메세지를 띄울지 설정합니다.

  • validate 속성은 커스텀하게 validation을 가능하게 해주는 속성이다. 값이 false이면 에러가 발생하고 true이면 에러가 발생하지 않는다. 아래처럼 문자열을 적어주면 해당 값이 에러메세지로 넘어간다.

    // example
    <input
    {...register("firstName", {
     required: "필수 속성입니다.",
     validate: {
    	noSpecialCharacters: (value) =>
    		value.includes("특정값") ? "특수문자는 허용되지 않습니다." : true,
    })}
    />
                
                
  • 이외에도 maxLength, minLength 등 여러가지가 있긴한데 쉬우므로 넘어가겠습니다. 공식문서 참고

중첩 데이터 다루는 방법

  • name(이름)은 고유해야 하며, 온점, 대괄호를 통해서 중첩필드를 쉽게 생성 할수 있습니다.
// "value"값은 사용자가 입력한 값입니다.
<input
   {...register("name.firstName", {
     required: "필수정보입니다",
   })}
   type="text"
   placeholder="이름을 입력하세요."
/> 
// { name: {firstName: "value" } }

<input
   {...register("name.firstName[0]", {
     required: "필수정보입니다",
   })}
   type="text"
   placeholder="이름을 입력하세요."
/>

 // { name: {firstName: ["value"] } }
     
<input
   {...register("name.[0]", {
     required: "필수정보입니다",
   })}
   type="text"
   placeholder="이름을 입력하세요."
/>

 // { name: ["value"] }

handleSubmit

handleSubmit는 이름에서도 알 수 있듯이 양식 제출을 관리합니다. form 태그의 onSubmit event에 전달하면 됩니다. 인수로 전달된 첫번째 함수는 양식 유효성 검사가 성공하면 등록된 (register)필드 값과 함께 호출되며, 두번째 인수는 유효성 검사가 실패하면 오류와 함께 호출됩니다.

  const onSubmit = async (data: IForm) => {
    try {
      console.log(data);
    } catch (err) {
      console.log(err, "err");
    }
  };

const onErrors = errors => console.error(errors);

<form onSubmit={handleSubmit(onSubmit), onErrors} className="content">
	{/* ... */}
</form>

onSubmit event가 발생되면 유효성 검사에서 에러가 없고, 그러면 onSubmit 함수가 실행이 되는데 register로 등록된 값이 data로 넘어 옵니다.

errors

유효성 검사에서 에러가 뜬 항목들은 errors로 넘어옵니다.

import { useForm } from 'react-hook-form';

const {
    register,
    handleSubmit,
    formState: { errors },
    setError,
    clearErrors,
} = useForm<IForm>();


return (
.
.
.
.
 <div className="join_row">
   <h3 className="join_title">
     <label htmlFor="password">비밀번호</label>
   </h3>
   <span className="ps_box">
     <input
       {...register("password", {
         required: "필수정보입니다.",
       })}
      type="text"
      id="password"
     placeholder="입력하세요."
   />
  </span>
  <ErrorText>{errors?.password?.message}</ErrorText>
</div>

ErrorText 컴포넌트는 styled-components로 스타일링 한 컴포넌트이며 useForm hook에서 뺴온 formState.errors로 에러가 확인이 가능합니다. errors.해당필드명.messages 이런식으로 에러를 표현하면 됩니다. 아래이미지는 필수로 입력을 해야되는 필드창인데 입력하지 않아 required에서 설정한 문자열이 에러명으로 넘가있습니다.

setError

setError를 통해서 특정필드에 에러를 발생시킬 수 있습니다. 첫번째 파라미터는 name, 두번째파라미터는 해당에러의 타입, 메세지를 설정 가능하고, 세번째 파라미터는 { shouldFocus?: boolean } 설정이 가능합니다.

  const validateEmail = async () => {
    try {
      await rejectHandler(3000);
      clearErrors("email");
    } catch (err) {
      if (err instanceof Error && err.message === "duplicate email") {
        setError(
          "email",
          {
            message: "중복된 이메일입니다.",
          },
          { shouldFocus: true }
        );
      }
    }
  };

{ shouldFocus: true }는 setError로 설정한 필드에 에러가 발생했을때 foucs가 이동되는 옵션입니다.

reset

reset을 이용해서 서버에서 받아온 데이터를 설정도 가능합니다.


  const bootstrap = async () => {
    const myInfo = await getUserData();
    reset(myInfo);
    // reset({
    //   email: myInfo.email,
    //   name: myInfo.name
    // })
 
  };

  useEffect(() => {
    bootstrap();
  }, []);

전체코드

import styled from "styled-components";
import { useForm } from "react-hook-form";
import { useEffect } from "react";

const SignUpContainer = styled.div`
  /* :root .sel {
    background: #fff url('https://static.nid.naver.com/images/join/pc/sel_arr_2x.gif') 100% 50% no-repeat;
    background-size: 20px 8px;
  } */

  margin: 0 auto;
  max-width: 768px;
  min-width: 460px;
  label {
    cursor: pointer;
    color: #000;
  }
  .content {
    width: 460px;
    margin: 200px auto;
  }

  .join_content {
  }
  .row_group {
    overflow: hidden;
    width: 100%;
  }

  .join_row {
  }
  .pass_check::after {
    content: "";
    display: inline-block;
    position: absolute;
    top: 50%;
    right: 13px;
    width: 24px;
    height: 24px;
    margin-top: -12px;
    background-image: url("https://static.nid.naver.com/images/ui/join/m_icon_pw_step.png");
    background-repeat: no-repeat;
    background-position: 0 0;
    background-size: 125px 75px;
    cursor: pointer;
  }

  .join_title {
    margin: 19px 0 8px;
    font-size: 14px;
    font-weight: 700;
  }
  .ps_box {
    background: #fff;
    outline: 0;
    display: block;
    position: relative;
    width: 100%;
    height: 51px;
    border: solid 1px #dadada;
    padding: 10px 10px 10px 14px;
    background: #fff;
    box-sizing: border-box;
    vertical-align: top;
    input {
      outline: 0;
      display: block;
      position: relative;
      width: 100%;
      height: 29px;
      padding-right: 25px;
      line-height: 29px;
      border: none;
      background: #fff;
      font-size: 15px;
      box-sizing: border-box;
      z-index: 10;
    }
  }
  .bir_wrap {
    display: table;
  }

  .bir_yy {
    display: table-cell;
    table-layout: fixed;
    width: 147px;
    vertical-align: middle;
  }

  .bir_mm {
    padding-left: 10px;
  }

  .sel {
    width: 100%;
    height: 29px;
    padding: 7px 8px 6px 7px;
    font-size: 15px;
    line-height: 18px;
    color: #000;
    border: none;
    border-radius: 0;
    *height: auto;
    -webkit-appearance: none;
  }
  option {
    font-weight: normal;
    display: block;
    white-space: nowrap;
    min-height: 1.2em;
    padding: 0px 2px 1px;
  }
  .int {
    display: block;
    position: relative;
    width: 100%;
    height: 29px;
    padding-right: 25px;
    line-height: 29px;
    border: none;
    background: #fff;
    font-size: 15px;
    box-sizing: border-box;
    z-index: 10;
  }

  .btn_area {
    margin: 30px 0 9px;
    button {
      color: #fff;
      border: solid 1px rgba(0, 0, 0, 0.08);
      background-color: #12b886;
      display: block;
      width: 100%;
      padding: 15px 0 15px;
      font-size: 18px;
      font-weight: 700;
      text-align: center;
      cursor: pointer;
      box-sizing: border-box;
    }
  }
`;

const ErrorText = styled.span`
  display: block;
  margin: 9px 0 -2px;
  font-size: 14px;
  line-height: 14px;
  color: red;
  font-weight: 500;
`;

interface IForm {
  email: string;
  password: string;
  passwordCheck: string;
  name: string;
  yy: string;
  mm: string;
  dd: string;
}

function SignUpPage() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    setError,
    clearErrors,
    reset,
  } = useForm<IForm>();

  const onSubmit = async (data: IForm) => {
    try {
      console.log(data, "data");
    } catch (err) {
      console.log(err, "err");
    }
  };

  const validateEmail = async () => {
    try {
      await resolveHandler(3000);
      clearErrors("email");
    } catch (err) {
      if (err instanceof Error && err.message === "duplicate email") {
        setError(
          "email",
          {
            message: "중복된 이메일입니다.",
          },
          { shouldFocus: true }
        );
      }
    }
  };

  function rejectHandler(ms: number) {
    return new Promise((resolve, reject) =>
      setTimeout(() => {
        reject(new Error("duplicate email"));
      }, 1000)
    );
  }

  const getUserData = async (): Promise<{ email: string; name: string }> => {
    return new Promise((resolve) =>
      setTimeout(() => {
        resolve({ email: "wmc1415@naver.com", name: "김현진" });
      }, 1000)
    );
  };

  function resolveHandler(ms: number) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  const bootstrap = async () => {
    const myInfo = await getUserData();
    reset(myInfo);
  };

  useEffect(() => {
    bootstrap();
  }, []);

  return (
    <>
      <SignUpContainer>
        <form onSubmit={handleSubmit(onSubmit)} className="content">
          <div className="join_content">
            <div className="row_group">
              <div className="join_row">
                <h3 className="join_title">
                  <label htmlFor="id">이메일</label>
                </h3>
                <span className="ps_box">
                  <input
                    {...register("email", {
                      required: "필수정보입니다.",
                      pattern: {
                        value:
                          /^[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/,
                        message: "이메일형식으로 기입해주세요.",
                      },
                      onBlur: () => {
                        validateEmail();
                      },
                    })}
                    className="id_input"
                    type="text"
                    id="id"
                    placeholder="이메일을 입력하세요."
                  />
                </span>
              </div>
              <ErrorText>{errors?.email?.message}</ErrorText>
              <div className="join_row">
                <h3 className="join_title">
                  <label htmlFor="password">비밀번호</label>
                </h3>
                <span className="ps_box">
                  <input
                    {...register("password", {
                      required: "필수정보입니다.",
                    })}
                    className="id_input"
                    type="text"
                    id="password"
                    placeholder="입력하세요."
                  />
                </span>
                <ErrorText>{errors?.password?.message}</ErrorText>
              </div>

              <div className="join_row">
                <h3 className="join_title">
                  <label htmlFor="pass_check">비밀번호 확인</label>
                </h3>
                <span className="ps_box pass_check">
                  <input
                    {...register("passwordCheck", {
                      required: "필수정보입니다.",
                    })}
                    className="id_input"
                    type="text"
                    id="pass_check"
                    placeholder="비밀번호를 한번 더 입력하세요."
                  />
                </span>
                <ErrorText>{errors?.passwordCheck?.message}</ErrorText>
              </div>
            </div>
            <div className="join_row">
              <h3 className="join_title">
                <label htmlFor="name">이름</label>
              </h3>
              <span className="ps_box">
                <input
                  {...register("name", {
                    required: "필수정보입니다",
                  })}
                  className="id_input"
                  type="text"
                  id="name"
                  placeholder="이름을 입력하세요."
                />
              </span>
              <ErrorText>{errors?.name?.message}</ErrorText>
            </div>
          </div>

          <div className="btn_area">
            <button type="submit">가입하기</button>
          </div>
        </form>
      </SignUpContainer>
    </>
  );
}

export default SignUpPage;
profile
기록의 중요성

0개의 댓글