기존
[클라이언트]
// 회원가입 완료
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를 입력할시 비교가 이루어지지 않는 것 점을 확인했다.
코드는 다음과 같다.
// 비밀번호 확인
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들은 따로 lib/api에 모아 관리