[react] 회원가입 폼 만들기 #1 - 기본 구조와 유효성 검사 세팅

dev__bokyoung·2022년 6월 8일
5

프롤로그

개발 스터디 [그리디브]에 참여하게 되었다. 팀원 3명이서 각자가 부족한 부분과 해보고 싶었던 부분을 공부하면서 실제 프로덕트를 만들어보기로 했다. 기록 앱 만들고 출시하기가 최종 목표이지만 모두 웹개발자들이라 progresive웹으로 먼저 만들어보기로 결정했다. 나는 리액트를 배우는 단계라서 가장 작은 역할을 부여 받았다.

회원가입 폼을 리액트로 만들어보기

회원가입 폼 기능 리스트

  1. 아이디 글자 수 제한 (한글금지)
  2. 아이디, 닉네임 중복체크 (api)
  3. 비밀번호 확인 로직
  4. 비밀번호 영어,숫자, 특수기호만 가능 (한글제거)
  5. 이메일 정규식
  6. 전화번호 정규식 및 자동 - 넣기
  7. 생년월일 인풋 클릭 시 달력 모달
  8. 마케팅 동의 체크박스
  9. 회원가입 완료 버튼
    1. Validation
    2. 에러 표기
    3. 필수값 표기
    4. API 전송

유효성 검사를 하는 두가지 방식

회원가입 유효성 검사를 하는 방식에는 두가지가 있다.

  1. 제출 시 한꺼번에 검사해서 틀린부분을 나타내주고, 폼 초기화
  2. 하나씩 onChange가 되었을 시, 유효성 검사를 해줌 → 모두 값이 true 라면 제출 버튼을 활성화

모두 입력을 하고 제출을 눌렀을때 잘못된 부분을 알려주거나 초기화 되는 폼이 있고, 입력을 할 때마다 내가 놓친 부분이 있다면 빨간색으로 잘못되었다고 알려주는 폼이 있다. 무엇을 하던 상관없지만 내가 회원가입을 할때 생각해보면 기껏 다 써놓고 초기화가 된다면 귀찮음과 짜증이 올라왔었다. 사용자 경험이 좋지 못했으므로 나는 친절하고 편리한 2번의 방식을 채택했다.

정규 표현식

유효성 검사를 하기 위해서 알아야 할 정규 표현식의 문법들을 몇가지 찾아 정리했다. 이정도만 알아도 해석하기에 무리없었다. 알고나니 보이는 정규식의 패턴들이었다.

/ : 자바스크립트의 정규표현식의 처음과 끝을 의미한다.
[ ] : 문자셋이다. 예를 들면 [a-z]라고 적을경우 정규표현식에 만족해야하는 값들은 반드시 a~z사이의 값만 넣을 수 있다.
^ : 문장의 처음을 뜻한다.
$ : 문장의 마지막을 뜻한다.
{ } : 문자열 길이를 뜻한다. 예를 들어 {4,12}일 경우 최소 길이 4, 최대 길이 12이다.

예를들면 이렇다.

/^[a-zA-z0-9]{4,12}$/ 

위 정규식을 분석해 본다면, 영문 대/소문자, 숫자만 사용할 수 있고 길이는 최소 4, 최대 12 라고 할 수 있다. 이제 이 정규식을 가지고 유효성 검사를 세팅하기 시작했다.

생각 포인트

코드를 작성 하기 전 어떤 식으로 짜야 할지 먼저 정리해 보았다.

  • 상태관리 함수를 이용해 값을 세팅 한다. (value 값, 오류메세지, 유효성 boolean 값)
  • value 값이 바뀌면 그에 따라 값 변경, 조건이 일치하면 true, 메세지 출력 ↔ 일치하지 않으면 false, 오류메세지 출력으로 상태를 바꿔준다.
  • 유효성 검사는 정규 표현식으로 작성한다.
  • 값을 입력 했을 때 if 조건문으로 확인한다. (if조건문에서 test는 정규표현식과 id의 값이 일치하는지 아닌지 확인하는 함수) 일치하면 true를 return 일치하지 않으면 false를 return한다.

코드 작성 흐름도

1. 컴포넌트 생성 & 폼 구조 작성

디자인 없이 가장 원시적인 form 구조를 생성 해 주었다.
label 안에 input 을 넣고 id 입력을 했을때 아래에 “사용가능한 아이디 입니다" 등같은 안내 또는 에러 메세지를 나타내기 위한 p 태그 작성까지 해서 하나의 박스 구조 생성해 준다.

가장 작은 단위는 이렇다.

<div className="form-el">
   <label htmlFor="id">Id</label> <br />
   <input id="id" name="id" value={id} onChange={onChangeId} />
   <p className="message"> {idMessage} </p>
</div>

기획 설계대로 나머지 비밀번호, 이메일 등의 구조도 추가 해주면


return (
    <>
      <h3>Sign Up</h3>
      <div className="form">
        <div className="form-el">
          <label htmlFor="id">Id</label> <br />
          <input id="id" name="id" value={id} onChange={onChangeId} />
          <p className="message"> {idMessage} </p>
        </div>

        <div className="form-el">
          <label htmlFor="name">Nick Name</label> <br />
          <input id="name" name="name" value={name} onChange={onChangeName} />
          <p className="message">{nameMessage}</p>
        </div>
        <div className="form-el">
          <label htmlFor="password">Password</label> <br />
          <input
            id="password"
            name="password"
            value={password}
            onChange={onChangePassword}
          />
          <p className="message">{passwordMessage}</p>
        </div>
        <div className="form-el">
          <label htmlFor="passwordConfirm">Password Confirm</label> <br />
          <input
            id="passwordConfirm"
            name="passwordConfirm"
            value={passwordConfirm}
            onChange={onChangePasswordConfirm}
          />
          <p className="message">{passwordConfirmMessage}</p>
        </div>
        <div className="form-el">
          <label htmlFor="email">Email</label> <br />
          <input
            id="email"
            name="name"
            value={email}
            onChange={onChangeEmail}
          />
          <p className="message">{emailMessage}</p>
        </div>
        <div className="form-el">
          <label htmlFor="phone">Phone</label> <br />
          <input id="phone" name="phone" value={phone} onChange={addHyphen} />
          <p className="message">{phoneMessage}</p>
        </div>
        <div className="form-el">
          <label htmlFor="birth">Birth</label> <br />
          <input
            id="birth"
            name="birth"
            value={birth}
            onChange={onChangeBirth}
          />
          <p className="message">{birthMessage}</p>
        </div>
        <br />
        <br />
        <button type="submit">Submit</button>
      </div>
    </>
  );

2. 상태 관리 초기값 세팅

상태관리를 위해 useState 함수를 사용해줬다. 각 해당하는 필드마다 초기값을 세팅 해 주었다.


// 초기값 세팅 - 아이디, 닉네임, 비밀번호, 비밀번호확인, 이메일, 전화번호, 생년월일
  const [id, setId] = React.useState("");
  const [name, setName] = React.useState("");
  const [password, setPassword] = React.useState("");
  const [passwordConfirm, setPasswordConfirm] = React.useState("");
  const [email, setEmail] = React.useState("");
  const [phone, setPhone] = React.useState("");
  const [birth, setBirth] = React.useState("");

3. 오류 메세지 전달을 위한 상태값 세팅

안내 메세지 부분도 마찬가지로 초기값 세팅을 해준다. useState 이용

// 오류메세지 상태 저장
 const [idMessage, setIdMessage] = React.useState("");
 const [nameMessage, setNameMessage] = React.useState("");
 const [passwordMessage, setPasswordMessage] = React.useState("");
 const [passwordConfirmMessage, setPasswordConfirmMessage] =
   React.useState("");
 const [emailMessage, setEmailMessage] = React.useState("");
 const [phoneMessage, setPhoneMessage] = React.useState("");
 const [birthMessage, setBirthMessage] = React.useState("");

4. 유효성 검사하기 위한 세팅

boolen 값을 넣어주기 위해 마찬가지로 세팅해주었다. 쓰다보니 반복되는 부분이 너무 많아서 하나로 묶을 수 있는 지점이 있지 않을까 생각이 들었다.

// 유효성 검사
 const [isId, setIsId] = React.useState(false);
 const [isname, setIsName] = React.useState(false);
 const [isPassword, setIsPassword] = React.useState(false);
 const [isPasswordConfirm, setIsPasswordConfirm] = React.useState(false);
 const [isEmail, setIsEmail] = React.useState(false);
 const [isPhone, setIsPhone] = React.useState(false);
 const [isBirth, setIsBirth] = React.useState(false);

input 입력값에 따라 조건을 걸어준다.(유효성 검사)
예를 들어 아이디 값을 입력했을때 , 그 값을 저장하고 → currentId
vaule 값을 변경 시켜준다. (상태값 변경) setId(currentId)
그리고 조건을 저장! (정규식 사용)

입력값이 조건에 해당하지 않으면 message 값을 변경 (유효하지 않습니다등의 에러메세지 출력), 유효 상태값을 false

조건에 해당하면 message 값 변경 (사용가능!) , 유효 상태값을 true 로 바꿔준다.

const onChangeId = (e) => {
    const currentId = e.target.value;
    setId(currentId);
    const idRegExp = /^[a-zA-z0-9]{4,12}$/;

    if (!idRegExp.test(currentId)) {
      setIdMessage("4-12사이 대소문자 또는 숫자만 입력해 주세요!");
      setIsId(false);
    } else {
      setIdMessage("사용가능한 아이디 입니다.");
      setIsId(true);
    }
  };

코드 작성 v1.0.0

위의 과정을 거쳐 전체 코드를 작성하면 이러하다.


import { NextPage } from "next";
import { useState } from "react";
import Button from "../components/lib/button";
import React from "react";

const Signup: NextPage = () => {
 // 초기값 세팅 - 아이디, 닉네임, 비밀번호, 비밀번호확인, 이메일, 전화번호, 생년월일
 const [id, setId] = React.useState("");
 const [name, setName] = React.useState("");
 const [password, setPassword] = React.useState("");
 const [passwordConfirm, setPasswordConfirm] = React.useState("");
 const [email, setEmail] = React.useState("");
 const [phone, setPhone] = React.useState("");
 const [birth, setBirth] = React.useState("");

 // 오류메세지 상태 저장
 const [idMessage, setIdMessage] = React.useState("");
 const [nameMessage, setNameMessage] = React.useState("");
 const [passwordMessage, setPasswordMessage] = React.useState("");
 const [passwordConfirmMessage, setPasswordConfirmMessage] =
   React.useState("");
 const [emailMessage, setEmailMessage] = React.useState("");
 const [phoneMessage, setPhoneMessage] = React.useState("");
 const [birthMessage, setBirthMessage] = React.useState("");

 // 유효성 검사
 const [isId, setIsId] = React.useState(false);
 const [isname, setIsName] = React.useState(false);
 const [isPassword, setIsPassword] = React.useState(false);
 const [isPasswordConfirm, setIsPasswordConfirm] = React.useState(false);
 const [isEmail, setIsEmail] = React.useState(false);
 const [isPhone, setIsPhone] = React.useState(false);
 const [isBirth, setIsBirth] = React.useState(false);

 const onChangeId = (e) => {
   const currentId = e.target.value;
   setId(currentId);
   const idRegExp = /^[a-zA-z0-9]{4,12}$/;

   if (!idRegExp.test(currentId)) {
     setIdMessage("4-12사이 대소문자 또는 숫자만 입력해 주세요!");
     setIsId(false);
   } else {
     setIdMessage("사용가능한 아이디 입니다.");
     setIsId(true);
   }
 };

 const onChangeName = (e) => {
   const currentName = e.target.value;
   setName(currentName);

   if (currentName.length < 2 || currentName.length > 5) {
     setNameMessage("닉네임은 2글자 이상 5글자 이하로 입력해주세요!");
     setIsName(false);
   } else {
     setNameMessage("사용가능한 닉네임 입니다.");
     setIsName(true);
   }
 };

 const onChangePassword = (e) => {
   const currentPassword = e.target.value;
   setPassword(currentPassword);
   const passwordRegExp =
     /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,25}$/;
   if (!passwordRegExp.test(currentPassword)) {
     setPasswordMessage(
       "숫자+영문자+특수문자 조합으로 8자리 이상 입력해주세요!"
     );
     setIsPassword(false);
   } else {
     setPasswordMessage("안전한 비밀번호 입니다.");
     setIsPassword(true);
   }
 };
 const onChangePasswordConfirm = (e) => {
   const currentPasswordConfirm = e.target.value;
   setPasswordConfirm(currentPasswordConfirm);
   if (password !== currentPasswordConfirm) {
     setPasswordConfirmMessage("떼잉~ 비밀번호가 똑같지 않아요!");
     setIsPasswordConfirm(false);
   } else {
     setPasswordConfirmMessage("똑같은 비밀번호를 입력했습니다.");
     setIsPasswordConfirm(true);
   }
 };
 const onChangeEmail = (e) => {
   const currentEmail = e.target.value;
   setEmail(currentEmail);
   const emailRegExp =
     /^[A-Za-z0-9_]+[A-Za-z0-9]*[@]{1}[A-Za-z0-9]+[A-Za-z0-9]*[.]{1}[A-Za-z]{1,3}$/;

   if (!emailRegExp.test(currentEmail)) {
     setEmailMessage("이메일의 형식이 올바르지 않습니다!");
     setIsEmail(false);
   } else {
     setEmailMessage("사용 가능한 이메일 입니다.");
     setIsEmail(true);
   }
 };
 const onChangePhone = (getNumber) => {
   const currentPhone = getNumber;
   setPhone(currentPhone);
   const phoneRegExp = /^01([0|1|6|7|8|9])-?([0-9]{3,4})-?([0-9]{4})$/;

   if (!phoneRegExp.test(currentPhone)) {
     setPhoneMessage("올바른 형식이 아닙니다!");
     setIsPhone(false);
   } else {
     setPhoneMessage("사용 가능한 번호입니다:-)");
     setIsPhone(true);
   }
 };

 const addHyphen = (e) => {
   const currentNumber = e.target.value;
   setPhone(currentNumber);
   if (currentNumber.length == 3 || currentNumber.length == 8) {
     setPhone(currentNumber + "-");
     onChangePhone(currentNumber + "-");
   } else {
     onChangePhone(currentNumber);
   }
 };

 const onChangeBirth = (e) => {
   const currentBirth = e.target.value;
   setBirth(currentBirth);
 };

 return (
   <>
     <h3>Sign Up</h3>
     <div className="form">
       <div className="form-el">
         <label htmlFor="id">Id</label> <br />
         <input id="id" name="id" value={id} onChange={onChangeId} />
         <p className="message"> {idMessage} </p>
       </div>

       <div className="form-el">
         <label htmlFor="name">Nick Name</label> <br />
         <input id="name" name="name" value={name} onChange={onChangeName} />
         <p className="message">{nameMessage}</p>
       </div>
       <div className="form-el">
         <label htmlFor="password">Password</label> <br />
         <input
           id="password"
           name="password"
           value={password}
           onChange={onChangePassword}
         />
         <p className="message">{passwordMessage}</p>
       </div>
       <div className="form-el">
         <label htmlFor="passwordConfirm">Password Confirm</label> <br />
         <input
           id="passwordConfirm"
           name="passwordConfirm"
           value={passwordConfirm}
           onChange={onChangePasswordConfirm}
         />
         <p className="message">{passwordConfirmMessage}</p>
       </div>
       <div className="form-el">
         <label htmlFor="email">Email</label> <br />
         <input
           id="email"
           name="name"
           value={email}
           onChange={onChangeEmail}
         />
         <p className="message">{emailMessage}</p>
       </div>
       <div className="form-el">
         <label htmlFor="phone">Phone</label> <br />
         <input id="phone" name="phone" value={phone} onChange={addHyphen} />
         <p className="message">{phoneMessage}</p>
       </div>
       <div className="form-el">
         <label htmlFor="birth">Birth</label> <br />
         <input
           id="birth"
           name="birth"
           value={birth}
           onChange={onChangeBirth}
         />
         <p className="message">{birthMessage}</p>
       </div>
       <br />
       <br />
       <button type="submit">Submit</button>
     </div>
   </>
 );
};

export default Signup;

결과

스타일을 입히지 않고 기능적으로만 봤을 때의 테스트 결과는 이렇게 구현된다.

에필로그

코드 작성을 하다 보니 반복되는 코드가 너무 많아 하나로 합칠 수 있지 않을까 하는 생각이 들었다. useState 도 너무 많이 개별적으로 준게 아닐까 생각해 본다. 하지만 처음 짜본 것 치고 막힘없이 잘 했다고 생각한다. (더 나은 코드로 수정을 해야겠지만 말이다.)

코드를 작성한 과정을 사고의 흐름별로 서술해 봤는데 나같은 초보자들에게는 도움이 되지 않을까 싶다. 혹시 처음부터 회원가입 폼을 작성해 봐야 한다면 나의 생각흐름을 참고해보는 것도 나쁘지 않을지도 (˵ ͡~ ͜ʖ ͡°˵)ノ

profile
개발하며 얻은 인사이트들을 공유합니다.

0개의 댓글