[리팩토링] 햄릿 증후군을 위한 술자리 결정 웹 서비스 #1 회원가입

세나정·2023년 5월 19일
0
post-thumbnail

회원가입

회원가입 중복 ID 확인 및 메인화면 이동

기존

[클라이언트]
 // 회원가입 완료
  const submitHandler = (e) => {
    e.preventDefault();
    const body = {
      id: id,
      // pw: btoa(pw),
      name: name,
      pw: pw,
    };
    {
      idchk
        ? axios.post("api/signup", body).then((res) => {
            {
              res.data == "Exist" ? alert("이미 존재하는 아이디이오니 다른 아이디를 사용하여 주세요.") : navigate("/");
            }
          })
        : alert("반드시 ID 중복 확인을 해주세요.");
    }
  };


[서버]
// 회원가입 정보 기입
app.post("/api/Signup", function (req, res) {
  db.collection("login").findOne({ 아이디: req.body.id }, function (err, result) {
    if (err) return console.log(err);
    if (result) {
      res.json("Exist");
    } else {
      // pw는 소금 + 해시값 동시 생성 및 DB 적재
      bcrypt.hash(req.body.pw, saltRounds, (err, hash) => {
        
        db.collection("login").insertOne({ 아이디: req.body.id, 패스워드: hash, 닉네임: req.body.name }, function (err, result) {
          if (err) return console.log(err);
          res.redirect("/");
        });
      });
    }
  });
});

기존 코드를 보면 client에서도 navigate("/")를 통한 메인 화면 이동처리,
서버 쪽에서도 redirect("/") 통한 메인 이동처리 두 가지 모두 적어놓은 걸 볼 수 있다.

이번에 고안한 방법은
서버 - 새 URL과 함께 302 Found 상태 코드를 보내어 리디렉션을 수행하는 코드를 통해 server에서만 동작하는 코드로 수정하였다.

그렇게 하기 위해 3항 연산자부분을

    {
      idchk
        ? axios.post("api/signup", body).then((res) => {
            {
              res.data === "Exist" ? alert("이미 존재하는 아이디이오니 다른 아이디를 사용하여 주세요.") : (window.location.href = res.data.redirectUrl);
            }
          })
        : alert("반드시 ID 중복 확인을 해주세요.");
    }

다음과 같이 res로 받아온 redirectUrl을 window.location 함수를 통해 이동시키게 하였고, 다음과 같은 정보를 저장하기 위해서 서버에선 json형태의 데이터를 보내주었다.

  db.collection("login").insertOne({ 아이디: req.body.id, 패스워드: hash, 닉네임: req.body.name }, function (err, result) {
    if (err) return console.log(err);
    // res.redirect("/");
    res.json({ redirectUrl: "/" });
  });

중복 확인 버튼

<div class="d-grid d-md-flex justify-content-md-end">
  <button
    class="btn mt-2 gap-2 col-md-4 press_btn"
    onClick={(e) => {
      e.preventDefault();
      CHECK_ID();
      setIdchk(true);
    }}
    disabled={!id}
  >
    중복확인
  </button>
</div>

disabled={!id}

기존에는 공백상태(false) 여도 중복확인이 가능해서, 아무것도 입력한 상태가 아니더라도 중복 확인 버튼을 누를 수 있었고, 그 값을 데이터베이스에서 findOne 하고 있었지만 disabled값에 !id값을 넣엊워서, id값이 비어있을 때는 동작하지 않도록 수정하였다.

회원가입 완료 버튼

<div class="d-grid gap-2 col-md-11 mx-auto">
  <button onSubmit={submitHandler} class="btn btn-lg press_btn mt-5 gap-2 " type="submit" disabled={(ispwconfirm && !pw) || !id}>
    회원가입 완료
  </button>
</div>

disabled에 조건을 추가하여 비밀번호가 비밀번호 확인과 일치하지 않거나 pw가 비어있을 때거나 id값을 입력하지 않아 id값이 비어있을 때는 회원가입 완료 버튼을 제거하여 버튼을 클릭할 수 없도록 바꾸었다.

issue #1

하지만 여기에서, 비동기처리로 인해 pw와 passwordConfirm의 처리에서의 문제점을 확인 ex. pw와 passwordConfirm이 맞았는데 그 후 pw를 수정하면 실시간 동기처리가 되지 않는 것, 혹은 passwordConfirm을 먼저 입력하고 pw를 입력할시 비교가 이루어지지 않는 것 점을 확인했다.

코드는 다음과 같다.

해결 과정 #1

  // 비밀번호 확인
  const pwConfirm = (e) => {
    const passwordConfirm = e.target.value;

    if (pw === passwordConfirm) {
      document.getElementById("alert").setAttribute("class", "mt-4 alert alert-success alert-dismissible fade show");
      setPwmessage("비밀번호가 일치합니다. 😊 회원가입 버튼을 눌러주세요.");
      setIspwconfirm(false);
    } else {
      document.getElementById("alert").setAttribute("class", "mt-4 alert alert-danger alert-dismissible fade show");
      setPwmessage("비밀번호가 일치하지 않습니다. 😢");
      setIspwconfirm(true);
    }
  };

이는 이벤트 핸들러가 실행되는 동안 React가 상태 및 UI를 업데이트하고 렌더링하기 때문에 발생하는데. 따라서 이벤트 핸들러 함수가 이전 상태 값을 사용하는 경우, 최신 상태가 반영되지 않을 수 있기 때문이다.

이 문제를 해결하기 위해 React는 이벤트 객체를 풀어서 사용할 수 있도록 persist() 메서드를 활용
(persist() 메서드를 사용하면 이벤트 객체가 유지되며 비동기 처리 중에도 접근할 수 있음)

하지만 persist를 활용하더라도 pwchk값을 실시간 변경하는 함수가 없기 때문에 잘 안 되지 않을까? 하고 state를 넣어줬지만..

애초에 비동기적으로 동작하는 useState가 따라올터가 없었다.

const pwConfirm = (e) => {
e.persist();
const passwordConfirm = e.target.value; // 입력된 값

setPwchk(passwordConfirm);

그래서 생각해낸 방법이 useEffect방법이다.

  useEffect(() => {
  if (pw === pwchk && pw) {
    document.getElementById("alert").setAttribute("class", "mt-4 alert alert-success alert-dismissible fade show");
    setPwmessage("비밀번호가 일치합니다. 😊 회원가입 버튼을 눌러주세요.");
    setIspwconfirm(false);
  } else if (pw && pwchk) {
    document.getElementById("alert").setAttribute("class", "mt-4 alert alert-danger alert-dismissible fade show");
    setPwmessage("비밀번호가 일치하지 않습니다. 😢");
    setIspwconfirm(true);
  }
}, [pw, pwchk]);

두 값이 변경 될 때마다 비교를 수행하고, 한 가지 더로 각 값이 존재할 때만 비교를 수행해서 더 깔끔한 로직이 되었다.

마지막으로 회원가입 완료 버튼의 활성화도

<div class="d-grid gap-2 col-md-11 mx-auto">
{/* password가 일치하지 않거나, 중복 검사를 하지 않았거나, 모든 input이 비어있다면 회원 가입 완료 불가 */}
<button onSubmit={submitHandler} class="btn btn-lg press_btn mt-5 gap-2" type="submit" disabled={!ispwconfirm || !idchk || !pwchk || !id || !pw}>
  회원가입 완료
</button>
</div>

모든 iuput과 회원가입 완료 조건을 달성하지 않았을 땐 로그인 할 수 없도록 변경하였다.

비밀번호 확인 엔터 입력 확인 핸들러

조급하고 바쁜 사용자의 편의를 위해 비밀번호 확인에서 엔터키를 눌렀을 때 바로 회원가입이 완료되는데, 아직 중복 확인을 하지 않았다면 아이디 중복 검사를 실행해줌

  // 비밀번호 확인 엔터 입력 확인 핸들러
// 중복확인을 했을 때 이동, 하지 않았으면 중복확인
const handleKeyDown = (e) => {
  if (e.key === "Enter" && idDupchk) {
    submitHandler(e);
  }
};

전체 코드

/* eslint-disable */
import React, { useState, useEffect } from "react";
import axios from "axios";

function Signup() {
// 상태 변수 선언
const [id, setId] = useState(""); // 아이디
const [pw, setPw] = useState(""); // 비밀번호
const [name, setName] = useState(""); // 닉네임
const [pwmessage, setPwmessage] = useState(""); // 비밀번호 일치 메세지
const [pwchk, setPwchk] = useState(""); // 비밀번호 확인
const [ispwconfirm, setIspwconfirm] = useState(false); // 비밀번호 확인 일치 여부
const [idDupchk, setIdDupchk] = useState(false); // 아이디 중복여부 확인

// 아이디 입력 핸들러
const idHandler = (e) => {
  e.preventDefault();
  setId(e.target.value);
};

// 비밀번호 입력 핸들러
const pwHandler = (e) => {
  e.preventDefault();
  setPw(e.target.value);
};

// 비밀번호 확인 입력 핸들러
const pwCheckHandler = (e) => {
  e.persist();
  e.preventDefault();
  setPwchk(e.target.value);
};

// 비밀번호 확인 엔터 입력 확인 핸들러
// 중복확인을 했을 때 이동, 하지 않았으면 중복확인
const handleKeyDown = (e) => {
  if (e.key === "Enter" && idDupchk) {
    submitHandler(e);
  }
};

// 닉네임 입력 핸들러
const nameHandler = (e) => {
  e.preventDefault();
  setName(e.target.value);
};

// 아이디 중복확인
const userIdDupchk = async () => {
  const body = {
    id: id,
  };
  try {
    const res = await axios.post("api/signup/checkID", body);

    console.log("검사여부 : " + res.data);

    if (res.data === "Exist") {
      alert("이미 존재하는 아이디입니다.");
    } else {
      alert("아이디가 사용이 가능합니다.");
    }
  } catch (err) {
    console.log(err);
  }
};

// 회원 가입 제출
const submitHandler = (e) => {
  e.preventDefault();
  const body = {
    id: id,
    name: name,
    pw: pw,
  };
  // 서버로 데이터를 보낼 때 이미 존재하는 아이디라면 회원가입 불가
  axios
    .post("api/signup", body)
    .then((res) => {
      if (res.data === "Exist") {
        alert("이미 존재하는 아이디이오니 다른 아이디를 사용하여 주세요.");
      } else {
        window.location.href = res.data.redirectUrl;
      }
    })
    .catch((err) => {
      console.log(err);
    });
};

// 비밀번호 일치 여부 확인 (useEffect 동기처리)
useEffect(() => {
  // 비밀번호가 비밀번호 확인과 일치하고 pw가 공백 아닐 때 동작
  if (pw === pwchk && pw) {
    document.getElementById("alert").setAttribute("class", "mt-4 alert alert-success alert-dismissible fade show");
    setPwmessage("비밀번호가 일치합니다. 😊 회원가입 버튼을 눌러주세요.");
    setIspwconfirm(true);

    // 비밀번호 확인과 비밀번호가 둘 다 공백이 아닐 때 동작
  } else if (pw && pwchk) {
    document.getElementById("alert").setAttribute("class", "mt-4 alert alert-danger alert-dismissible fade show");
    setPwmessage("비밀번호가 일치하지 않습니다. 😢");
    setIspwconfirm(false);
  }
}, [pw, pwchk]);

return (
  <>
    <div class="container col-8 mx-auto my-4 bg-white rounded rounded-8 shadow-lg">
      <div class="row p-5">
        <div class="col-lg-8 col-12 mx-auto bg-white">
          <div class="p-2">
            <div class="border rounded m-3 p-3">
              <a href="/">
                <h3>
                  <i class="bi bi-arrow-left arrow"></i>
                </h3>
              </a>
              <h3 class="mb-2 text-center pt-2">회원가입</h3>

              <form onSubmit={submitHandler}>
                {/* 아이디 입력란 */}
                <label class="p-3 font-500">ID</label>
                <input
                  type="text"
                  class="form-control form-control-lg mb-3 rounded-pill"
                  placeholder="사용할 아이디를 입력하세요."
                  value={id}
                  onChange={(e) => {
                    idHandler(e);
                  }}
                />

                <div class="d-grid d-md-flex justify-content-md-end">
                  <button
                    class="btn mt-2 gap-2 col-md-4 press_btn"
                    onClick={(e) => {
                      e.preventDefault();
                      userIdDupchk();
                      setIdDupchk(true);
                    }}
                    disabled={!id}
                  >
                    중복확인
                  </button>
                </div>

                {/* 닉네임 입력란 */}
                <label class="p-3 font-500">Username</label>
                <input
                  type="text"
                  class="form-control form-control-lg mb-3 rounded-pill"
                  placeholder="닉네임을 입력하세요. (추후 변경가능합니다.)"
                  value={name}
                  onChange={(e) => {
                    nameHandler(e);
                  }}
                />

                {/* 비밀번호 입력란 */}
                <label class="p-3 font-500">Password</label>
                <input
                  type="password"
                  class="form-control form-control-lg rounded-pill"
                  placeholder="사용할 비밀번호를 입력해 주세요."
                  value={pw}
                  onChange={(e) => pwHandler(e)}
                  onClick={(e) => {
                    e.preventDefault();
                  }}
                />

                {/* 비밀번호 확인 입력란 */}
                <input
                  type="password"
                  class="form-control form-control-lg mt-3 rounded-pill"
                  placeholder="비밀번호를 한 번 더 입력하세요."
                  onChange={(e) => {
                    pwCheckHandler(e);
                  }}
                  onKeyDown={handleKeyDown}
                  disabled={!pw}
                />

                {/* 비밀번호와 비밀번호 확인 일치 여부 메세지 */}
                <div id="alert">
                  <h6 id="errormessage">{pwmessage}</h6>
                </div>

                {/* 회원가입 완료 버튼 */}
                <div class="d-grid gap-2 col-md-11 mx-auto">
                  {/* password가 일치하지 않거나, 중복 검사를 하지 않았거나, 모든 input이 비어있다면 회원 가입 완료 불가 */}
                  <button onSubmit={submitHandler} class="btn btn-lg press_btn mt-5 gap-2" type="submit" disabled={!ispwconfirm || !idDupchk || !pwchk || !id || !pw}>
                    회원가입 완료
                  </button>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  </>
);
}

export default Signup;

API 분할

회원가입 (유저)를 다루는 API들은 따로 lib/api에 모아 관리

profile
압도적인 인풋을 넣는다면 불가능한 것은 없다. 🔥

0개의 댓글