React 회원가입 기능, Hook Form 유혹을 뿌리치고 하드 코딩을 해보았다.

Taehee Kim·2022년 8월 16일
0

React

목록 보기
6/7
post-thumbnail

사건 발단

나는 요즘 React로 REST API를 가지고 쇼핑몰 기능을 조금씩 구현하고 있다. 멋쟁이 사자처럼 팀플에서는 바닐라자바스크립트를 뿌셨기(부서지는 건 역시 나였지만) 때문에, React 혼자 공부하며 차근차근 만들어 가게 되었다. 왜냐하면 팀플이다보니, 나는 post 요청이나 로그인, 회원가입, 토큰 인증 등 FE에서 핵심이라고 할 수 있 기능들을 해보지 못 한 채 부트 캠프가 끝나버렸다.

사건 전개

로그인은 onChange로 input 값을 추적하는 것만 해주면 큰 어려움이 없었다. (혼자 만족, 이때부터 회원가입도 금방 해버릴 수 있을 거라는 깊은 착각과 오만의 늪에 빠지기 시작함) .

회원가입 UI를 다 만들고 어떤 것부터 해야할지 몰랐다. 일단 로그인 기능구현처럼 input 값들을 하나씩 useState에 담기 시작했다. 그리고 post 요청을 냅다 했더니 성공! 하지만, 예외처리랑 error 메세지를 어떻게 해야 하는지 몰랐다. 정말 많은 블로그 글들과 강의들을 찾아보면서 알게된 것 바로 Form Hook 라이브러

하지만, React 회원가입 기능 동작을 기초부터 알아가고 싶었고 공부하는 단계이기 때문에 hook form을 사용을 하지 않고 눈물을 흘리며 하나씩 코드를 짰다.

Form Hook 미사용 코드

순서)
1. useState에 input 값을 담고 onChagne 이벤트 핸들러를 이용해서 실시간 추적
2. useState로 Error 메세지와 예외 처리에 사용할 true, false 상태 담기
3. 예외 처리(정규식 사용)
4. error 메세지 생성

  const signUp = async () => {
    try {
      const response = await AxiosInstance.post("accounts/signup/", {
        username: username, // 아이디
        password: password1,
        password2: password2,
        phone_number: phoneNum,
        name: name, // 이름
      });
      console.log(response);
      if (response.status === 201) {
        successJoin();
      }
    } catch {
      console.error("ERROR");
    }
  };

  const navigate = useNavigate();
  const successJoin = () => {
    navigate("/login");
  };

  // 가입하기 버튼 누르기
  const handleSubmitJoin = (e) => {
    e.preventDefault();
    signUp();
  };
  console.log(username, password1, password2, name, phoneNum);

  // input값 입력 & 유효성 검사
  //아이디
  // 20자 이내의 영어 소문자,대문자, 숫자만 가능
  const handleUsername = useCallback((e) => {
    const patternUsername = /^[a-zA-Z0-9]{1,20}$/;
    const usernameCurrent = e.target.value;
    setUsername(usernameCurrent);

    if (!patternUsername.test(usernameCurrent)) {
      setUsernameMsg("20자 이내의 영어 소문자,대문자, 숫자만 가능합니다.");
      setIsUsername(false);
    } else {
      setUsernameMsg("멋진 아이디네요 :)");
      setIsUsername(true);
    }
  }, []);

  // 비밀번호
  // 비번 10자리 이상, 영소문자 포함,
  const handlePw1 = useCallback((e) => {
    const patternPw1 = /^[a-zA-Z0-9!@#$%^&*]{10,50}$/;
    const password1Current = e.target.value;
    setPassword1(password1Current);

    if (!patternPw1.test(password1Current)) {
      setPassword1Msg("영소문자, 숫자를 포함한 10자 이상이어야 합니다.");
      setIsPassword1(false);
    } else {
      setPassword1Msg("안전한 비밀번호에요 :)");
      setIsPassword1(true);
    }
  }, []);

  // 비밀번호 확인
  const handlePw2 = useCallback(
    (e) => {
      const password2Current = e.target.value;
      setPassword2(password2Current);

      if (password1 === password2Current) {
        setPassword2Msg("비밀번호가 일치합니다 :)");
        setIsPassword2(true);
      } else {
        setPassword2Msg("비밀번호와 일치하지 않습니다. 다시 입력해주세요.");
        setIsPassword2(false);
      }
    },
    [password1]
  );

  //이름
  const handleName = (e) => {
    setName(e.target.value);
    setNameMsg("멋진 이름이네요 :)");
    setIsName(true);
  };

  //휴대전화
  //핸드폰 번호는 010으로 시작하는 10-11자리 숫자
  //^010([0-9]{3,4})([0-9]{4})$
  const handlephoneNum = useCallback((e) => {
    const patternPhoneNum = /^010([0-9]{3,4})([0-9]{4})$/;
    const phoneNumCurrent = e.target.value;
    setPhoneNum(phoneNumCurrent);
    setIsPhoneNum(true);

    if (!patternPhoneNum.test(phoneNumCurrent)) {
      setPhoneNumMsg("- 없이 010으로 시작하는 10-11자리 숫자를 입력하세요");
      setIsPhoneNum(false);
    } else {
      setPhoneNumMsg("올바른 형식입니다 :)");
      setIsPhoneNum(true);
    }
  }, []);

  return (
    <LoginSection>
      <h2 className="ir">로그인 페이지</h2>
      <MainLogo src={mainLogo} alt="메인로고" />
      <LoginDiv>
        <form onSubmit={handleSubmitJoin}>
          <JoinLabel htmlFor="username">
            아이디
            <FlexDiv1>
              <JoinIdInput
                id="username"
                name="username"
                type="text"
                required
                onChange={handleUsername}
              />
              <CheckOverlapBtn>중복확인</CheckOverlapBtn>
            </FlexDiv1>
            {(username.length > 0 && !isUsername && (
              <ErrorMsg>{usernameMsg}</ErrorMsg>
            )) ||
              (username.length > 0 && isUsername && (
                <SuccessMsg>{usernameMsg}</SuccessMsg>
              ))}
          </JoinLabel>
          <JoinLabel htmlFor="password">
            비밀번호
            <JoinInput
              id="password"
              name="password"
              type="password"
              onChange={handlePw1}
              required
            />
            {(password1.length > 0 && !isPassword1 && (
              <ErrorMsg>{password1Msg}</ErrorMsg>
            )) ||
              (password1.length > 0 && isPassword1 && (
                <SuccessMsg>{password1Msg}</SuccessMsg>
              ))}
          </JoinLabel>
          <JoinLabel htmlFor="checkPW">
            비밀번호 재확인
            <JoinInput
              id="checkPW"
              name="password"
              type="password"
              onChange={handlePw2}
              required
            />
            {(password2.length > 0 && !isPassword2 && (
              <ErrorMsg>{password2Msg}</ErrorMsg>
            )) ||
              (password2.length > 0 && isPassword2 && (
                <SuccessMsg>{password2Msg}</SuccessMsg>
              ))}
          </JoinLabel>
          <JoinLabel htmlFor="name">
            이름
            <JoinInput
              id="name"
              name="name"
              type="text"
              onChange={handleName}
              required
            />
            {(name.length > 0 && !isName && <ErrorMsg>{nameMsg}</ErrorMsg>) ||
              (name.length > 0 && isName && <SuccessMsg>{nameMsg}</SuccessMsg>)}
          </JoinLabel>
          <JoinLabel htmlFor="number">
            휴대폰번호
            <JoinInput
              id="phoneNum"
              name="phoneNum"
              type="text"
              onChange={handlephoneNum}
              required
            />
            {(phoneNum.length > 0 && !isPhoneNum && (
              <ErrorMsg>{phoneNumMsg}</ErrorMsg>
            )) ||
              (phoneNum.length > 0 && isPhoneNum && (
                <SuccessMsg>{phoneNumMsg}</SuccessMsg>
              ))}
          </JoinLabel>
          <JoinLabel htmlFor="이메일">
            이메일
            <JoinInput id="email" name="email" type="email" />
          </JoinLabel>
        </form>
      </LoginDiv>
      <form onSubmit={handleSubmitJoin}>
        <CheckBoxP>
          <input type="checkbox" required />
          호두샵의 <a href="#none">이용약관</a>{" "}
          <a href="#none">개인정보처리방침</a>에 대한 내용을 확인하였고
          동의합니다.
        </CheckBoxP>
        <LoginBtn>가입하기</LoginBtn>
      </form>
    </LoginSection>
  );
};

✨ React Hook Form

🔗 참고 강의: https://www.youtube.com/watch?v=tWOn7g_3wKU

💖 내가 느낀 장점

  1. watch{...required('name' ,{예외처리})}을 사용하면 input 값 추적이 너무 손쉽다. 폼 hook을 쓰지 않으면 useState와 onChange를 사용해야 하는데 코드가 훨씬 간결하다.
  2. {...required('name' ,{예외처리})} 에 name을 명시하면 굳이 따로 id, name 등을 쓰지 않아도 된다. 한마디로 하나로 통일에서 사용 가능하다.
  3. 예외처리 코드도 너무 간결하고 직관적이라 정말 라이브러리로서 역할을 한다. 개발 시간도 엄청 단축되는 기분이다.
  4. 공식 홈페이지에 들어가면 기본 UI 코드가 있어서 빠르게 개발이 가능한 거 같다. 접근이 너무 쉽다.

form 라이브러리는 정말 나에게 신세계를 열어주었다. 실무에서도 많이 사용하는지 알 수 없지만 기본 동작 원리를 알았기 때문에 앞으로는 이 form hook을 많이 사용할 것 같다.

import React, { useRef } from "react";
import { useForm } from "react-hook-form";
import "../src/app.css";

export default function App() {
  const {
    register,
    watch,
    formState: { errors },
    handleSubmit,
  } = useForm();
  console.log(watch("email")); // email input 값 추적
  console.log(watch("name")); // name input 값 추적
  console.log(watch("password")); // password input 값 추적
  console.log(watch("password_confirm")); // password_confirm input 값 추적

  //password_confrim
  const password = useRef(); //dom을 선택할 수 있게 ref 생성
  password.current = watch("password"); // password 필드 값 가져오기

  // event대신 data
  const onSubmit = (data) => {
    console.log(data, "data");
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>Email</label>
      <input
        type="email"
        {...register("email", { required: true, pattern: /^\S+@\S+$/i })}
      />
      {errors.email && <p>이메일 입력은 필수입니다</p>}

      <label>Name</label>
      <input {...register("name", { required: true, maxLength: 8 })} />
      {errors.name && errors.name.type === "required" && (
        <p>이름은 입력은 필수입니다</p>
      )}
      {errors.name && errors.name.type === "maxLength" && (
        <p>최대 8글자까지 입력이 가능합니다.</p>
      )}

      <label>Password</label>
      <input
        type="password"
        {...register("password", { required: true, minLength: 6 })}
      />
      {errors.password && errors.password.type === "required" && (
        <p>비밀번호는 입력은 필수입니다</p>
      )}
      {errors.password && errors.password.type === "minLength" && (
        <p>최소 6글자 이상 입력해야 합니다.</p>
      )}

      <label>Password Confirm</label>
      <input
        type="password"
        {...register("password_confirm", {
          required: true,
          validate: (value) => value === password.current,
        })}
      />
      {errors.password_confirm &&
        errors.password_confirm.type === "required" && (
          <p>비밀번호는 재확인은 필수입니다</p>
        )}
      {errors.password_confirm &&
        errors.password_confirm.type === "validate" && (
          <p>비밀번호가 일치하지 않습니다</p>
        )}

      <input type="submit" />
    </form>
  );
}

0개의 댓글