회원가입 페이지 유효성 검사

라용·2022년 9월 25일
1

위코드 - 스터디로그

목록 보기
57/100

위코드 1차 팀프로젝트를 진행하며 정리한 내용입니다.

이메일, 비밀번호, 이름, 성별, 휴대폰, 생년월일, 개인정보 유효기간을 입력하는 회원가입 페이지입니다. 버튼 하나로 가입하는 소셜로그인을 생각한다면 사용자에게는 꽤 번거로운 UI 입니다. 그래도 연습이라 생각해 간소화하지 않고 그대로 작업했습니다. 대신 모든 창을 다 입력하고 버튼을 눌렀는 데 재입력을 요청하지 않도록 주요 인풋값에 유효성 검사를 넣어 안내를 했고, 필수 값의 입력 및 유효성 검사가 끝난 후에 가입하기 버튼을 활성화 했습니다.

form 태그 사용해 데이터 한번에 받기

form 태그의 자식요소로 input 태그를 사용할 경우 onChange 에 하나의 handleInput 함수를 연결해 입력값을 가져올 수 있습니다. 각 Input 에는 name 을 입력해야 하고 ('name : 입력값' 형태로 저장가 위함) 라디오 버튼의 경우 name 을 통일해야 하나의 라디오만 선택되고 선택한 버튼의 value 속성값을 가져옵니다.

// 이메일, 비밀번호 입력 

<form>
    <input
      onChange={handleInput}
      className="userInputEmail input"
      name="email"
      type="text"
      placeholder="이메일"
      autoComplete="username"
    />
    <input
      onChange={handleInput}
      className="userInputPw input"
      name="pw"
      type="password"
      placeholder="비밀번호"
      autoComplete="current-password"
    />
    ..
</form>

// 라디오 버튼 입력, label 태그안에 input 태그와 span 태그를 담아서 사용

<form>
    <label className="userMale label">
      <input
        onChange={handleInput}
        className="radio"
        name="gender"
        type="radio"
        value="man"
      />
      <span className="text">남자</span>
    </label>
    <label className="userFemale label">
      <input
        onChange={handleInput}
        className="radio"
        name="gender"
        type="radio"
        value="woman"
      />
      <span className="text">여자</span>
    </label>
</form>

이제 최상단에서 state 로 input 값을 선언하고 handleInput 함수로 인풋값을 모아서 state 에 담아줍니다.

const [userInput, setUserInput] = useState({
    email: '',
    pw: '',
    pwCheck: '',
    name: '',
    gender: '',
    phoneNum: '',
    year: '',
    month: '',
    day: '',
    time: '',
});

const { email, pw, pwCheck, name, gender, phoneNum, year, month, day, time } = userInput;  // 구조분해 할당하기

const handleInput = e => {
    const { name, value } = e.target;
    setUserInput({ ...userInput, [name]: value });
};

정규표현식을 사용한 유효성 검사

이메일과 비밀번호 유효성 검사에 정규표현식을 사용했습니다. 원하는 조건으로 정규표현식을 검색하면 해당하는 식을 찾을 수 있습니다. 그 외의 값들은 각자의 조건에 맞게 검사를 진행하고 최종 값을 불린값으로 저장한 후 모든 불린 값을 확인해 가입하기 버튼을 활성화합니다.

// 이메일 유효성 검사 (xx@xxxx.xx)

const isEmail = email => {
    const emailRegex = /^[a-z0-9_+.-]+@([a-z0-9-]+\.)+[a-z0-9]{2,4}$/;
    return emailRegex.test(email);
};

const isEmailValid = isEmail(email); 

// 비밀번호 유효성 검사 (대소문자,숫자,특수문자 포함 8자리 이상)

const isPw = pw => {
    const pwRegex = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/;
    return pwRegex.test(pw);
};

const isPwValid = isPw(pw);

유효성 검사 안내 문구 삽입

해당 입력창에 값을 입력할 경우 안내문구가 나오고, 유효성 검사를 통과하면 안내문구가 사라지게 설정했습니다. && 연산자를 사용한 조건부 렌더링으로 동작합니다.

{!isEmailValid && ( // 조건부 렌더링
  <p
    className="inputCheck"
    style={{ display: email.length > 0 ? 'block' : 'none' }} // 삼항 연산자
  >
    * 이메일 양식을 맞춰주세요!
  </p>
)}

생년월일 상수데이터 사용

select, option 태그를 활용한 생년월일 입력은 각각의 상수데이터를 별도로 만들어 사용했습니다.

<select className="select" name="year" onChange={handleInput}>
  {YEAR.map(y => {
    return <option key={y}>{y}</option>;
  })}
</select>
<select className="select" name="month" onChange={handleInput}>
  {MONTH.map(m => {
    return <option key={m}>{m}</option>;
  })}
</select>
<select className="select" name="day" onChange={handleInput}>
  {DAY.map(d => {
    return <option key={d}>{d}</option>;
  })}
</select>

// 상수 데이터
// 년
export const YEAR = [];

const nowYear = new Date().getFullYear();
for (let i = 1980; i <= nowYear; i++) {
  YEAR.push(i);
}

// 월
export const MONTH = [];

for (let i = 1; i <= 12; i++) {
  let m = String(i).padStart(2, '0');
  MONTH.push(m);
}

// 일
export const DAY = [];
for (let i = 1; i <= 31; i++) {
  let d = String(i).padStart(2, '0');
  DAY.push(d);
}

가입하기 버튼 활성화

전체 유효성 검사를 묶어서 isAllValid 값을 선언하고 이를 활용해 버튼에 입력할 값을 설정합니다. 이를 활용해 버튼에 특정 클래스 명을 추가할 수 있습니다.

 // 전체 유효성 검사

const isAllValid =
    isEmailValid &&
    isPwValid &&
    isPwSame &&
    isPhoneNumValid &&
    isBirth &&
    isTimeValid;

const activeBtn = isAllValid ? 'undefined' : 'disabled';

// 버튼 활성화

<div className={`signupBtn ${activeBtn}`} onClick={checkSignUp}>
  가입하기
</div>

// 스타일 속성

.disabled {
  opacity: 0.3;
  pointer-events: none;
}

그 외에 사진 첨부 기능이나 백엔드 통신은 따로 포스팅할 예정입니다. 아래 전체코드를 첨부하지만 이후 리팩토링을 진행할 예정입니다.

import React, { useState, useRef } from 'react';
import { Link } from 'react-router-dom';
import { YEAR } from './YEAR';
import { MONTH } from './MONTH';
import { DAY } from './DAY';
import { LIMIT_TIME } from './LIMIT_TIME';
import './SignUp.scss';
function SignUp() {
  const [imageUrl, setImageUrl] = useState(null);
  const [userInput, setUserInput] = useState({
    email: '',
    pw: '',
    pwCheck: '',
    name: '',
    gender: '',
    phoneNum: '',
    year: '',
    month: '',
    day: '',
    time: '',
  });
  const { email, pw, pwCheck, name, gender, phoneNum, year, month, day, time } =
    userInput;
  const handleInput = e => {
    const { name, value } = e.target;
    setUserInput({ ...userInput, [name]: value });
  };
  // 프로필 사진 입력
  const imgRef = useRef();
  const onChangeImage = () => {
    const reader = new FileReader();
    const file = imgRef.current.files[0];
    reader.readAsDataURL(file);
    reader.onloadend = () => {
      setImageUrl(reader.result);
    };
  };
  // 이메일 유효성 검사
  const isEmail = email => {
    const emailRegex = /^[a-z0-9_+.-]+@([a-z0-9-]+\.)+[a-z0-9]{2,4}$/;
    return emailRegex.test(email);
  };
  const isEmailValid = isEmail(email);
  // 패스워드 유효성 검사
  const isPw = pw => {
    const pwRegex =
      /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/;
    return pwRegex.test(pw);
  };
  const isPwValid = isPw(pw);
  // 패스워드 재확인
  const isPwSame = pw === pwCheck;
  const pwDoubleCheck = !isPwSame ? 'pwDoubleCheck' : undefined;
  // 휴대폰 번호 유효성 검사
  const isPhoneNum = phoneNum => {
    const phoneNumRegex = /01[016789]-[^0][0-9]{2,3}-[0-9]{4,4}/;
    return phoneNumRegex.test(phoneNum);
  };
  const isPhoneNumValid = isPhoneNum(phoneNum);
  // 생년월일 입력여부 확인
  const isBirth = Boolean(year && month && day);
  // 개인정보 유효기간
  const isTimeValid = Boolean(time);
  // 전체 유효성 검사 후 버튼 활성화
  const isAllValid =
    isEmailValid &&
    isPwValid &&
    isPwSame &&
    isPhoneNumValid &&
    isBirth &&
    isTimeValid;
  const activeBtn = isAllValid ? 'undefined' : 'disabled';
  // 통신
  const checkSignUp = e => {
    e.preventDefault();
    fetch('https://8075-211-106-114-186.jp.ngrok.io/users/signup', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json;charset=utf-8',
      },
      body: JSON.stringify({
        email: email,
        password: pw,
        name: name,
        birthday: `${year}-${month}-${day}`,
        phone_number: phoneNum,
        gender: gender,
        time: time,
      }),
    })
      .then(response => {
        if (response.ok === true) {
          return response.json();
        }
        throw new Error('에러 발생!');
      })
      .catch(error => alert(error))
      .then(data => {
        if (data.ok === '회원가입 성공') {
          alert('회원가입 성공');
          <Link to="/login" />;
        } else {
          alert('회원가입 실패');
        }
      });
  };
  return (
    <div className="signUp">
      <form className="signUpBox">
        <div className="profileBox">
          <label className="imgBoxLabel" htmlFor="profileImg">
            {imageUrl ? (
              <img className="labelImg" src={imageUrl} alt="uploadImg" />
            ) : null}
            <div className="imgUploadBtn">
              <i className="fa-sharp fa-solid fa-camera" />
            </div>
            <input
              id="profileImg"
              className="profileImgInput"
              type="file"
              name="imageUrl"
              ref={imgRef}
              onChange={onChangeImage}
            />
          </label>
        </div>
        {/* 이메일 비밀번호 입력 */}
        <input
          onChange={handleInput}
          className="userInputEmail input"
          name="email"
          type="text"
          placeholder="이메일"
          autoComplete="username"
        />
        <input
          onChange={handleInput}
          className="userInputPw input"
          name="pw"
          type="password"
          placeholder="비밀번호"
          autoComplete="current-password"
        />
        <input
          onChange={handleInput}
          className={`userInputPwCheck input ${pwDoubleCheck}`}
          name="pwCheck"
          type="password"
          placeholder="비밀번호 확인"
          autoComplete="current-password"
        />
        {!isEmailValid && (
          <p
            className="inputCheck"
            style={{ display: email.length > 0 ? 'block' : 'none' }}
          >
            * 이메일 양식을 맞춰주세요!
          </p>
        )}
        {!isPwValid && (
          <p
            className="inputCheck"
            style={{ display: pw.length > 0 ? 'block' : 'none' }}
          >
            * 비밀번호는 대소문자, 숫자, 특수문자 포함 8자리 이상 적어주세요!
          </p>
        )}
        {/* 이름 입력 */}
        <p className="userName title mustInput">이름</p>
        <input
          onChange={handleInput}
          className="userInputName input"
          name="name"
          type="text"
          placeholder="이름을(를) 입력하세요"
          autoComplete="username"
        />
        {/* 성별 입력 */}
        <p className="userGender title mustInput">성별</p>
        <label className="userMale label">
          <input
            onChange={handleInput}
            className="radio"
            name="gender"
            type="radio"
            value="man"
          />
          <span className="text">남자</span>
        </label>
        <label className="userFemale label">
          <input
            onChange={handleInput}
            className="radio"
            name="gender"
            type="radio"
            value="woman"
          />
          <span className="text">여자</span>
        </label>
        {/* 휴대폰 입력 */}
        <p className="userPhoneNum title mustInput">휴대폰</p>
        <input
          onChange={handleInput}
          className="userInputNumber input"
          name="phoneNum"
          type="text"
          placeholder="000-0000-0000 형식으로 입력하세요"
          autoComplete="username"
        />
        {!isPhoneNumValid && (
          <p
            className="inputCheck"
            style={{ display: phoneNum.length > 0 ? 'block' : 'none' }}
          >
            * 숫자 사이에 하이픈(-)을 넣어주세요.
          </p>
        )}
        {/* 생년월일 입력 */}
        <div className="userBirth">
          <p className="title mustInput">생년월일</p>
          <div className="selectBox">
            <select className="select" name="year" onChange={handleInput}>
              {YEAR.map(y => {
                return <option key={y}>{y}</option>;
              })}
            </select>
            <select className="select" name="month" onChange={handleInput}>
              {MONTH.map(m => {
                return <option key={m}>{m}</option>;
              })}
            </select>
            <select className="select" name="day" onChange={handleInput}>
              {DAY.map(d => {
                return <option key={d}>{d}</option>;
              })}
            </select>
          </div>
        </div>
        {/* 개인정보 유효기간 */}
        <div className="userDataSave">
          <p className="name title">개인정보 유효기간</p>
          {LIMIT_TIME.map(time => {
            return (
              <label key={time.id} className="one label">
                <input
                  className="radio"
                  name="time"
                  type="radio"
                  value={time.value}
                  onChange={handleInput}
                />
                <span className="text">{time.text}</span>
              </label>
            );
          })}
        </div>
        <div className={`signupBtn ${activeBtn}`} onClick={checkSignUp}>
          가입하기
        </div>
      </form>
    </div>
  );
}
export default SignUp;
@import '../../styles/variables.scss';
.signUp {
  font-family: $NotoSans;
  .signUpBox {
    max-width: 500px;
    margin: 50px auto 100px;
    padding: 0 20px;
    font-size: $fontSmall;
    color: $colorDarkGray;
    .title {
      padding: 22px 0 13px;
      font-weight: $weightSemiBold;
    }
    .mustInput {
      &:after {
        content: '';
        display: inline-block;
        width: 5px;
        height: 5px;
        margin: 0 0 3px 6px;
        background: $colorRed;
        border-radius: 50%;
      }
    }
    .input {
      width: 100%;
      padding: 10px 15px;
      border: 0.5px solid $colorGray;
      font-size: $fontSmall;
      outline: none;
      &:nth-child(3) {
        border-bottom: 0px;
        border-top: 0px;
      }
      &:nth-child(4) {
        margin-bottom: 5px;
      }
    }
    .label {
      display: flex;
      padding-bottom: 4px;
      .text {
        padding-left: 5px;
      }
    }
    .inputCheck {
      padding: 5px 0 0 5px;
      color: $colorRed;
      font-size: $fontMicro;
    }
    .pwDoubleCheck {
      border: 1px solid red;
    }
    .profileBox {
      @include flexSort(center, center);
      flex-direction: column;
      margin-bottom: 30px;
      .imgBoxLabel {
        position: relative;
        background: $colorLightGray;
        width: 80px;
        height: 80px;
        border-radius: 50%;
        .labelImg {
          width: 100%;
          height: 100%;
          border-radius: 50%;
          object-fit: cover;
        }
        .profileImgInput {
          display: none;
        }
        .imgUploadBtn {
          @include flexSort(center, center);
          position: absolute;
          bottom: 0;
          right: 0;
          width: 30px;
          height: 30px;
          background: $colorDarkGray;
          color: white;
          border-radius: 50%;
          cursor: pointer;
        }
      }
    }
    .selectBox {
      .select {
        padding: 8px 20px;
        margin-right: 5px;
        border: 0.5px solid $colorGray;
        font-size: $fontSmall;
        outline: none;
      }
    }
    .signupBtn {
      padding: 10px 15px;
      margin-top: 40px;
      color: $colorWhite;
      background: $colorDarkGray;
      font-size: $fontSmall;
      text-align: center;
      cursor: pointer;
    }
    .disabled {
      opacity: 0.3;
      pointer-events: none;
    }
  }
}
profile
Today I Learned

0개의 댓글