Project/[SAFU] 1st Project

4. [Client & Server] Signup 구현 (Oct 17, 2020 ~ Oct 20, 2020 회고)

HJChung 2020. 11. 5. 18:06

처음 맡게 된 기능 구현 이슈카드는 <Signup 컴포넌트>였다. 

해야할 것은 

  1.  회원가입 컴포넌트 틀 구성
  2.  아이디, 이메일 유효성 검사
  3.  제출하기 버튼 -> redirect -> login 페이지

1. 회원가입 컴포넌트 틀 구성 - (Oct 17~18, 2020) 회고

회원가입 컴포넌트 틀을 짜는 것은 거의 HTML 같으니까 뭐... 라고 생각했지만

<div>와 <button> 그리고 <ul><li>만 쓰고 있는 나 자신을 발견했다. 

시멘틱 마크업이 뭐길래? 아무튼 배우고 싶고 배워야 할 건 너어무 많다. 

 - <김버그의 HTML은 재밌다> 라는 강의가 있는데, 수강하고 싶다. 

아무튼 예전에 HTML 태그 복습 해놓은게 있어서 그나마 다행이었다. 

2. 아이디, 이메일 유효성 검사 - (Oct 18~19, 2020) 회고

아이디는 2개, 비밀번호는 1개의 유효성 검사를 실시한다. 

1) 아이디: 아이디를 이메일로 받고 있기 때문에

     1. key가 email인 경우, => 이메일 형식을 맞춰야 하고, 중복된 이메일이 존재하면 안된다. (이미 회원가입 되어있다는 의미이므로)
     1-1. 이메일 형식이 맞지 않으면 => 올바른 이메일 형식이 아닙니다. 출력
     1-2. 이메일 형식이 맞으면
          1-2-1. 중복 검사 => 이미 있는 이메일이면 이미 존재하는 email입니다. 출력하고 올바른 이메일 입력할 때까지 break
          1-2-2.         => 없는 이메일이면 통과!

이렇게 한 글자 씩 입력해주면서 바로바로 중복검사가 이루어지기 때문에 모든 회원 정보를 다 입력하고 <제출하기>버튼을 눌렀을 때 post 요청에서는 DB에서 따로 중복 검사를 해주지 않아도 된다. 

2) 비밀번호

     2. key가 password인 경우, => 8자 이상이어야 하고, 숫자/소문자를 모두 포함해야 한다.

 

아래의 코드는 아직 API를 고려하기 전 단계로, 이메일 중복검사를 비롯한 여러 유효성 검사가 잘 작동하는지를 알아보기 위해 fakeUsersData를 사용했었다. 그러나 실제로는 check UsersId API를 통해서 중복검사를 진행하게 된다. 

(코드는 더보기 클릭)

더보기

 

//Signup.js - state에 따라 or 라우팅에 따라) 변경되는 부분: x
import React from 'react';
import axios from 'axios';
import { withRouter } from 'react-router-dom';
import { fakeUsersData } from './__test__/fakeUsersData';

// console.log('fakeUsersData: ', fakeUsersData); //출력 잘 됨.
class SignUp extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      email: '',
      password: '',
      passwordCheck: '',
      githubId: '',

      isAvailedEmail: '',
      isAvailedPassword: '',
      isAvailedPasswordCheck: '',
    };
  }
  handleSignUpValue = (key) => (e) => {
    //여기서 유효성 검사를 한다.
    // 1. key가 email인 경우, => 이메일 형식을 맞춰야 하고, 중복된 이메일이 존재하면 안된다. (이미 회원가입 되어있다는 의미이므로)
    // 1-1. 이메일 형식이 맞지 않으면 => 올바른 이메일 형식이 아닙니다. 출력
    // 1-2. 이메일 형식이 맞으면
    //      1-2-1. 중복 검사 => 이미 있는 이메일이면 이미 존재하는 email입니다. 출력하고 올바른 이메일 입력할 때까지 break
    //      1-2-2.         => 없는 이메일이면 통과!

    if (key === 'email') {
      var emailreg = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i;
      var email = e.target.value;
      if (email.length > 0 && false === emailreg.test(email)) {
        this.setState({ isAvailedEmail: '올바른 이메일 형식이 아닙니다.' });
      } else {
        //모든 이메일 형식을 잘 갖춰서 작성해 주었을 때, 중복 검사를 한다.
        for (let userInfo of fakeUsersData) {//(*)이 부분이 현재는 fakeUsersData로 되어 있지만 나중에 check Userid API가 완성되면, 이 부분은 없어지고
          console.log(userInfo);
          if (userInfo.email === email) {
            console.log(userInfo.email); //(*)여기서 글자 입력시마다 실시간으로 Check UsersId API를 통해 중복검사를 진행한는 코드를 추가할 계획이다. 
            this.setState({ isAvailedEmail: '이미 존재하는 email입니다.' });
            break;
          } else {
            this.setState({ isAvailedEmail: '' });
            this.setState({ [key]: e.target.value });
          }
        }
      }
    }

    //2. key가 password인 경우, => 8자 이상이어야 하고, 숫자/소문자를 모두 포함해야 한다.
    // reference: http://blog.naver.com/PostView.nhn?blogId=yjhyjh5369&logNo=221289465679&parentCategoryNo=&categoryNo=16&viewDate=&isShowPopularPosts=true&from=search
    if (key === 'password') {
      var reg = /^(?=.*?[a-z])(?=.*?[0-9]).{8,}$/;
      var password = e.target.value;
      if (password.length > 0 && false === reg.test(password)) {
        this.setState({
          isAvailedPassword: '비밀번호는 8자 이상이어야 하며, 숫자/소문자를 모두 포함해야 합니다.',
        });
      } else {
        this.setState({ isAvailedPassword: '' });
        this.setState({ [key]: e.target.value });
      }
    }

    //3. key가 passwordCheck인 경우 => 앞선 this.state.password와 같아야한다.
    if (key === 'passwordCheck') {
      var passwordCheck = e.target.value;
      if (passwordCheck.length > 0 && this.state.password !== passwordCheck) {
        this.setState({ isAvailedPasswordCheck: '비밀번호가 일치하지 않습니다.' });
      } else {
        this.setState({ isAvailedPasswordCheck: '' });
        this.setState({ [key]: e.target.value });
      }
    }
  };

  handleLoginButton = () => {
    // 제출하기(회원가입) 버튼을 누르면 이 event가 발생
    // 이 버튼은 서버에 회원가입을 요청 후 로그인 페이지로 리다이렉트 해줌
    // 이미 회원가입이 되어 있는 경우, email 유효성 검사에서 걸러지므로 따로 확인 필요없음.
    // axios 공식문서 https://xn--xy1bk56a.run/axios/guide/api.html#%EA%B5%AC%EC%84%B1-%EC%98%B5%EC%85%98 에서 //post 요청 전송 을 참고
    axios({
      method: 'post',
      url: 'http://localhost:4000/user/signup',
      data: {
        email: this.state.email,
        password: this.state.password,
        githubId: this.state.githubId,
      },
    })
      .then((res) => {
        //200(OK), 201(Created)
        console.log('SignUp res: ', res);
        //history.pushState('/reviews');
      })
      .catch((err) => {
        //500(err)
        console.error(err);
      });
  };

  render() {
    const { history } = this.props;
    return (
      <div>
        <ul>
          <li>
            <label htmlFor="email">
              email
              <input type="email" onChange={this.handleSignUpValue('email').bind(this)}></input>
              <div>{this.state.isAvailedEmail}</div>
            </label>
          </li>
          <li>
            <label htmlFor="password">
              pw
              <input
                type="password"
                onChange={this.handleSignUpValue('password').bind(this)}
              ></input>
              <div>{this.state.isAvailedPassword}</div>
            </label>
          </li>
          <li>
            <label
              htmlFor="password check"
              onChange={this.handleSignUpValue('passwordCheck').bind(this)}
            >
              pw 확인
              <input type="password"></input>
              <div>{this.state.isAvailedPasswordCheck}</div>
            </label>
          </li>
          <li>
            <label htmlFor="Github ID" onChange={this.handleSignUpValue('githubId').bind(this)}>
              Github ID
              <input></input>
            </label>
          </li>
        </ul>
        {/* <button onClick={this.handleLoginButton().bind(this)}>제출하기</button> */}
        <button
          onClick={(e) => {
            // console.log(this.state);
            e.preventDefault();
            this.handleLoginButton.bind(this);
          }}
        >
          제출하기
        </button>
      </div>
    );
  }
}

// export default withRouter(SignUp);
export default SignUp;

※ 이 과정에서 배운 것: 

1) axios 의 사용

생각보다 공식문서가 잘 되어 있었다. 

xn--xy1bk56a.run/axios/guide/api.html#%EA%B5%AC%EC%84%B1-%EC%98%B5%EC%85%98

 

API | Axios 러닝 가이드

API 구성(configuration) 설정을axios()에 전달하여 요청할 수 있습니다. axios(config) axios({ method: 'post', url: '/user/12345', data: { firstName: 'Fred', lastName: 'Flintstone' } }); 1 2 3 4 5 6 7 8 9 axios({ method:'get', url:'http://bit

xn--xy1bk56a.run

2) 정규표현식

정규표현식이란

문자열을 검색하고 대체하는 데 사용 가능한 일종의 형식 언어(패턴)로, 

이 표현식을 문자열에 적용하여 간단한 문자 검색부터 이메일, 패스워드 검사 등의 복잡한 문자 일치 기능 등을 빠르게 수행 할 수 있다. 

규표현식 학습의 필요성과 임하는 학습 목표

이메일의 형식이 맞는지, 비밀번호가 소문자/숫자 포함 8글자 이상인지를 구글에 검색해서 쉽게 구현하였지만 내 파일에 있는 몇 줄의 코드를 이해할 수 없다는 것에서 오는 찝찝함.

그리고 이메일, 비밀번호 형식등 일반적인 경우가 아니라 어떤 특정 문자패턴을 찾아내야하는 상황이 생긴다면? 이렇듯 어짜피 한 번은 공부해야 할거라고 생각했고, 이번이 그 기회라고 생각한다.

하지만 모든 경우의 수를 암기 하진 못 할 것이고, 나의 학습 목표는 표현식들이 모여있는 것을 뜯어보고 해독할 수 있는 수준, 정규식이 제공하는 기능을 알고 있는 것 이다.

<불규칙 속에서 규칙을 찾아내는 정규표현식>과 <정규표현식, 이렇게 시작하자!>를 많이 참고해서 공부중이다. 아직 정리 중이라 다 하면 첨부할 것이다!

우선, 내가 구글링으로 찾아서 사용한 정규표현식은 아래와 같다. 

1. 이메일 형식 

var emailreg = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i;

2. 소문자/숫자 포함 8글자 이상

var reg = /^(?=.*?[a-z])(?=.*?[0-9]).{8,}$/;

그 외에도 ,
- 하나 이상의 영문 대문자 (?=.*?[A-Z])

- 하나 이상의 영문 소문자 (?=.*?[a-z])

- 하나 이상의 숫자 (?=.*?[0-9])

- 하나 이상의 특수 문자 (?=.*?[#?!@$%^&*-])

- 최소 길이 8자 .{8,} (앵커 포함)

이런게 있었다. 

그리고 정규표현식 테스트 사이트인 https://regexr.com/을 사용해서 연습하였다. 

※. test() 메서드는 주어진 문자열이 정규 표현식을 만족하는지 판별하고, 그 여부를 true 또는 false로 반환합니다.

3. API를 통한 유효성 검사 & 제출하기 버튼 -> redirect -> login 페이지 - (Oct 20, 2020) 회고

server 쪽에서 회원가입에 필요한 API가 1차 완성되었다.
우선  SAFU 서비스의 회원가입 flow는 아래와 같다. 

  1. 이메일을 입력한다.
  2. 이메일 한글자 입력시마다 check UserId API로 그걸 server에 보내주면
  3. server는 그걸 user DB에서 찾고
  4. 있으면 해당 data를 응답 -> 그러면 ㄴclient는 ‘존재하는 이메일입니다’라고 출력
  5. 없으면 nulll을 응답 -> 그러면 client는  아무 메세지도 출력하지 않고 통과시킴
  6. 그러면 그때서야 사용자는 password, GithubID를 입력하고, 다 입력하면 submit 버튼을 누른다. 그러면 그때 signup API가 작동하여서
  7. server에서는 users 테이블에 해당 데이터를 create.

즉, 우리가 필요한 API는 check UserId API, signup API이다.

signup API 의 경우 그냥 통으로 데이터를 보내주면 Check UserId API를 통해 이미 중복검사가 되어있으므로, DB에선 create만 해주면 되기때문에 client 쪽에서는 크게 어려운건 없었다. 다만 이메일 중복검사가 약간 헷갈릴 수 있는데 이럴때 gitbook(우리가 사용하는 API 문서)이 언제나 좋은 길잡이가 되었다. server와 client의 통신인 만큼 API문서가 가장 좋은 커뮤니케이션 기준, 코드 구현 기준이다. 
다음 project에는 response의 데이터 형식을 완전 json으로 좀 더 구체적으로 적고, DB column명과도 일치시키면 더 좋을 것 같다. 

1) check UserId API

아무튼 Check UserId API는 아래와 같이 기획하였기 때문에 

fakeUsersData로 email 중복검사에 했던 코드 부분은 이렇게 바뀔 것이다. 

client code)

handleSignUpValue = (key) => (e) => {
    if (key === 'useremail') {
      var emailreg = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i;
      var useremail = e.target.value;
      this.setState({ [key]: useremail });
      if (useremail.length > 0 && false === emailreg.test(useremail)) {
        this.setState({ isAvailedEmail: '올바른 이메일 형식이 아닙니다.' });
      } else {
        axios({
          method: 'post',
          url: 'http://localhost:4000/users/signup/checkId',
          data: {
            useremail: e.target.value,// 이렇게 하면 씽크가 맞다. jhjyj5414@naver.com으로 검사를 진행하게 된다.  
            // useremail: this.state.useremail, //이렇게 하면 한 박자 느리다. jhjyj5414@naver.co으로 검사를 진행하게 된다. 왜일까?
          },
        })
          .then((res) => {
            //status code: 200
            if (res.data !== null) { //DB의 데이터가 담겨서 오는 것은 res.data이다. 
              this.setState({ isAvailedEmail: '이미 존재하는 email입니다.' });
            } else {
              this.setState({ isAvailedEmail: '' });
              this.setState({ [key]: useremail });
            }
          })
          .catch((err) => {
            //status code: 500
            console.error(err);
          });
      }
    }

추가로 server code까지 보면..ㅎㅎ)

const { users } = require('../../models');

module.exports = {
  post: (req, res) => {
    const { useremail } = req.body;
    users
      .findOne({
        where: {
          email: useremail,
        },
      })
      .then((data) => {
        console.log(data);
        if (data !== null) {
          res.status(201).json(data);
        } else {
          res.status(201).json(data);
        }
      })
      .catch((err) => {
        res.status(500).send('err');
      });
  },
};

2) signup API

signup submit 버튼 눌렀을 때 DB의 users 테이블에 잘 들어가는 것 까지 확인하였다. 원래 서비스 기능 flow상,

제출하기 버튼을 누르면 login 페이지로  redirect 된다. 하지만 아직 login이 구현되기 전이라 이 부분은 주석처리 해 놓았고  API 통신이 잘 되는지만 확인하려고 console로 '회원가입 완료'를 찍는 것으로 마무리하였다. 

signup submit 버튼 눌렀을 때 client)

  handleSignUpButton = () => {
    axios({
      method: 'post',
      url: 'http://localhost:4000/users/signup',
      data: {
        useremail: this.state.useremail,
        password: this.state.password,
        githubId: this.state.githubId,
      },
    })
      .then((res) => {
        //200(OK), 201(Created)
        // this.props.history.push('/users/login');
         console.log('회원가입 완료');
      })
      .catch((err) => {
        //500(err)
        console.error(err);
      });
  };

signup submit 버튼 눌렀을 때 server)

const { users } = require('../../models');

module.exports = {
  post: (req, res) => {
    const { useremail, password, githubId } = req.body;
    users
      .create({
        email: useremail,
        password: password,
        githubId: githubId,
        active: true,
      })
      .then((data) => {
        res.status(201).json(data);
      })
      .catch((err) => {
        res.status(500).send('err');
      });
  },
};

여기까지 완성된 code

//Signup.js - state에 따라 or 라우팅에 따라) 변경되는 부분: x

import React from 'react';
import axios from 'axios';
import { withRouter } from 'react-router-dom';

class SignUp extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      useremail: '',
      password: '',
      passwordCheck: '',
      githubId: '',
      isAvailedEmail: '',
      isAvailedPassword: '',
      isAvailedPasswordCheck: '',
    };
  }
  handleSignUpValue = (key) => (e) => {
  	//여기서 유효성 검사를 한다.
    // 1. key가 email인 경우, => 이메일 형식을 맞춰야 하고, 중복된 이메일이 존재하면 안된다. (이미 회원가입 되어있다는 의미이므로)
    // 1-1. 이메일 형식이 맞지 않으면 => 올바른 이메일 형식이 아닙니다. 출력
    // 1-2. 이메일 형식이 맞으면
    //      1-2-1. 중복 검사 => 이미 있는 이메일이면 이미 존재하는 email입니다. 출력하고 올바른 이메일 입력할 때까지 break
    //      1-2-2.         => 없는 이메일이면 통과!
    if (key === 'useremail') {
      var emailreg = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i;
      var useremail = e.target.value;
      this.setState({ [key]: useremail });
      if (useremail.length > 0 && false === emailreg.test(useremail)) { //test() 메서드는 주어진 문자열이 정규 표현식을 만족하는지 판별하고, 그 여부를 true 또는 false로 반환한다.
        this.setState({ isAvailedEmail: '올바른 이메일 형식이 아닙니다.' });
      } else {//모든 이메일 형식을 잘 갖춰서 작성해 주었을 때, check UserId API를 통해 중복 검사를 한다.
        axios({
          method: 'post',
          url: 'http://localhost:4000/users/signup/checkId',
          data: {
            useremail: e.target.value,
          },
        })
          .then((res) => {
            if (res.data !== null) {
              this.setState({ isAvailedEmail: '이미 존재하는 email입니다.' });
            } else {
              this.setState({ isAvailedEmail: '' });
              this.setState({ [key]: useremail });
            }
          })
          .catch((err) => {
            console.error(err);
          });
      }
    }
     //2. key가 password인 경우, => 8자 이상이어야 하고, 숫자/소문자를 모두 포함해야 한다.
    if (key === 'password') {
      var reg = /^(?=.*?[a-z])(?=.*?[0-9]).{8,}$/;
      var password = e.target.value;
      if (password.length > 0 && false === reg.test(password)) {
        this.setState({
          isAvailedPassword: '비밀번호는 8자 이상이어야 하며, 숫자/소문자를 모두 포함해야 합니다.',
        });
      } else {
        this.setState({ isAvailedPassword: '' });
        this.setState({ [key]: e.target.value });
      }
    }
    //3. key가 passwordCheck인 경우 => 앞선 this.state.password와 같아야한다.
    if (key === 'passwordCheck') {
      var passwordCheck = e.target.value;
      if (passwordCheck.length > 0 && this.state.password !== passwordCheck) {
        this.setState({ isAvailedPasswordCheck: '비밀번호가 일치하지 않습니다.' });
      } else {
        this.setState({ isAvailedPasswordCheck: '' });
        this.setState({ [key]: e.target.value });
      }
    }
    if (key === 'githubId') {
      this.setState({ [key]: e.target.value });
    }
  };
  handleSignUpButton = () => {
  	// 제출하기(회원가입) 버튼을 누르면 이 event가 발생
    // 이 버튼은 서버에 회원가입을 요청 후 로그인 페이지로 리다이렉트 해줌
    // 이미 회원가입이 되어 있는 경우, email 유효성 검사에서 걸러지므로 따로 확인 필요없음.
    axios({
      method: 'post',
      url: 'http://localhost:4000/users/signup',
      data: {
        useremail: this.state.useremail,
        password: this.state.password,
        githubId: this.state.githubId,
      },
    })
      .then((res) => {
        //200(OK), 201(Created)
        // this.props.history.push('/users/login');
        console.log('회원가입 완료');
      })
      .catch((err) => {
        //500(err)
        console.error(err);
      });
  };
  render() {
    const { history } = this.props;
    return (
      <div>
        <ul>
          <li>
            <label htmlFor="useremail">
              <div>email</div>
              <input type="useremail" onChange={this.handleSignUpValue('useremail')}></input>
              <div>{this.state.isAvailedEmail}</div>
            </label>
          </li>
          <li>
            <label htmlFor="password">
              <div>password</div>
              <input type="password" onChange={this.handleSignUpValue('password')}></input>
              <div>{this.state.isAvailedPassword}</div>
            </label>
          </li>
          <li>
            <label htmlFor="password check" onChange={this.handleSignUpValue('passwordCheck')}>
              <div>password 확인</div>
              <input type="password"></input>
              <div>{this.state.isAvailedPasswordCheck}</div>
            </label>
          </li>
          <li>
            <label htmlFor="Github ID" onChange={this.handleSignUpValue('githubId')}>
              <div>Github ID (for identification)</div>
              <input></input>
            </label>
          </li>
        </ul>
        <div>
          <button
            onClick={(e) => {
              e.preventDefault();
              {
                this.handleSignUpButton();
              }
            }}
          >
            Submit
          </button>
        </div>
      </div>
    );
  }
}

// export default withRouter(SignUp);
export default SignUp;

※ 이 과정에서 배운 것 2: 

1) 이전에 server에서 응답으로 주는 데이터를 client에서는 어떻게 다루는지 배운 것을 복습했다. 

동영상 정보를 요청하고 응답받는 Youtube API 이용

const searchYouTube = ({ key, query, max = 5 }, callback) => {
  fetch(
    `https://www.googleapis.com/youtube/v3/search?part=snippet&key=${key}&q=${query}&maxResult=${max}&type=video&videoEmbeddable=true`,
    {
      method: 'GET',
    },
  )
    .then((resp) => resp.json())
    .then(({ items }) => {
      callback(items)
    })
}
onSearchSumit(term) {
    var options = {
      key: YOUTUBE_API_KEY,
      query: term,
    }

    searchYouTube(options, (videos) =>
      this.setState({
        videos: videos,
        selectedVideo: videos[0],
      }),
    )
  }

 

2)  React에서 이벤트 다루기의 bind에 대해서 다시 공부 

velog.io/@kyusung/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B5%90%EA%B3%BC%EC%84%9C-React%EC%97%90%EC%84%9C-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%8B%A4%EB%A3%A8%EA%B8%B0

 

리액트 교과서 - React에서 이벤트 다루기

React에서 이벤트 다루기 React에서 DOM 이벤트 다루기 function vs. Arrow Function > 예제 코드 this가 가지는 값 function 스코프 내에서 this는 해당 function을 호출한 객체를 this로 한다. 화살표 함수 스코프 내

velog.io

3) 응답이 오는 형태에 대해서 확실히 확인!

그러니까 response.data에 내가 원하는 데이터가 담겨서 응답으로 온다!