이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.
이 게시물의 소스코드는 게시판 만들기(2016) / 게시판 - 스타일 수정에서 이어집니다.
board.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.
board.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 board.git 을 clone 합니다.
- Github에서 소스코드 보기: https://github.com/a-mean-blogger/board/tree/282faaf4b2717b2bc47918c1f0727e5e2a83f533
/* * 이 강의는 2020년 버전으로 update되었습니다. -> 2020년 버전 보기 */
회원가입을 만들어 봅시다!
사실 회원가입은 주소록, 게시물과 다르게 특별한 건 아니고, user model의 CRUD 중 create이 바로 회원가입입니다.
게시판 만들기 강의에서는 user CRUD 중에서 생성, 열람, 수정만 구현하고 삭제는 만들지 않습니다.
나중에 user(이용자)가 post(게시물)를 생성하게 될텐데, post가 생성된 이후에 user를 삭제하게 되면 user와 post간의 연결이 끊어지게 됩니다. 이때 이걸 해결하는 방법이 여러가지가 있는데.. 그건 이 강좌에서 다루지 않을 예정이기 때문에, user의 삭제 기능은 없습니다.
주소록, 게시물에 이어 이번이 3번째 비슷한 CRUD인데요, user는 password가 필요하고 회원가입, 정보 수정시에 인증이 필요하므로 model이 좀 더 복잡하게 됩니다. (route은 여전히 크게 달라지지 않습니다.)
이번에 만들 회원가입 페이지의 디자인은 아래와 같습니다.
이번엔 front end와 back end를 한번에 만들겠습니다.
주황색은 변경된 파일, 녹색은 새로 생성된 파일, 회색은 변화가 없는 파일입니다.
model부터 살펴봅시다.
// models/User.js var mongoose = require("mongoose"); // schema // 1 var userSchema = mongoose.Schema({ username:{type:String, required:[true,"Username is required!"], unique:true}, password:{type:String, required:[true,"Password is required!"], select:false}, name:{type:String, required:[true,"Name is required!"]}, email:{type:String} },{ toObject:{virtuals:true} }); // virtuals // 2 userSchema.virtual("passwordConfirmation") .get(function(){ return this._passwordConfirmation; }) .set(function(value){ this._passwordConfirmation=value; }); userSchema.virtual("originalPassword") .get(function(){ return this._originalPassword; }) .set(function(value){ this._originalPassword=value; }); userSchema.virtual("currentPassword") .get(function(){ return this._currentPassword; }) .set(function(value){ this._currentPassword=value; }); userSchema.virtual("newPassword") .get(function(){ return this._newPassword; }) .set(function(value){ this._newPassword=value; }); // password validation // 3 userSchema.path("password").validate(function(v) { var user = this; // 3-1 // create user // 3-3 if(user.isNew){ // 3-2 if(!user.passwordConfirmation){ user.invalidate("passwordConfirmation", "Password Confirmation is required!"); } if(user.password !== user.passwordConfirmation) { user.invalidate("passwordConfirmation", "Password Confirmation does not matched!"); } } // update user // 3-4 if(!user.isNew){ if(!user.currentPassword){ user.invalidate("currentPassword", "Current Password is required!"); } if(user.currentPassword && user.currentPassword != user.originalPassword){ user.invalidate("currentPassword", "Current Password is invalid!"); } if(user.newPassword !== user.passwordConfirmation) { user.invalidate("passwordConfirmation", "Password Confirmation does not matched!"); } } }); // model & export var User = mongoose.model("user",userSchema); module.exports = User;
1. schema : require 에 true 대신 배열이 들어갔습니다. 첫번째는 true/false 값이고, 두번째는 에러메세지입니다. 그냥 true/false을 넣을 경우 기본 에러메세지가 나오고, 배열을 사용해서 custom(사용자정의) 에러메세지를 만들 수 있습니다.
password에는 select:false가 추가되었습니다. 기본설정은 자동으로 select:true인데, schema항목을 DB에서 읽어옵니다. select:false로 설정하면 DB에서 값을 읽어 올때 해당 값을 읽어오라고 하는 경우에만 값을 읽어오게 됩니다. 비밀번호는 중요하기 때문에 기본적으로 DB에서 값을 읽어오지 않게 설정했습니다. 값을 읽어오는 방법은 아래 route부분을 설명할때 나옵니다.
2. DB에 저장되는 값은 password인데, 회원가입, 정보 수정시에는 위 값들이 필요합니다. DB에 저장되지 않아도 되는 정보들은 virtual로 만들어 줍니다.
3. DB에 정보를 생성, 수정하기 전에 mongoose가 값이 유효(valid)한지 확인(validate)을 하게 되는데 password항목에 custom(사용자정의) validation 함수를 지정할 수 있습니다. virtual들은 직접 validation이 안되기 때문에(DB에 값을 저장하지 않으니까 어찌보면 당연합니다) password에서 값을 확인하도록 했습니다.
3-1. validation callback 함수 속에서 this는 user model입니다.
3-2. model.isNew 항목이 true이면 새로 생긴 model(DB에 한번도 기록되지 않았던 model) 즉, 새로 생성되는 user이며, 값이 false이면 DB에서 읽어 온 model 즉, 회원정보를 수정하는 경우입니다.
3-3. 회원가입의 경우 password confirmation값이 없는 경우, password와 password confirmation값이 다른 경우에 유효하지않음처리(invalidate)를 하게 됩니다. model.invalidate함수를 사용하며, 첫번째는 인자로 항목이름, 두번째 인자로 에러메세지를 받습니다.
3-4. 회원정보 수정의 경우 current password값이 없는 경우, current password값이 original password랑 다른 경우, new password 와 password confirmation값이 다른 경우 invalidate합시다. 회원정보 수정시에는 항상 비밀번호를 수정하는 것은 아니기 때문에 new password와 password confirmation값이 없어도 에러는 아닙니다.
다음으로 route을 살펴봅시다.
// routes/users.js var express = require("express"); var router = express.Router(); var User = require("../models/User"); // Index // 1 router.get("/", function(req, res){ User.find({}) .sort({username:1}) .exec(function(err, users){ if(err) return res.json(err); res.render("users/index", {users:users}); }); }); // New router.get("/new", function(req, res){ res.render("users/new", {user:{}}); }); // create router.post("/", function(req, res){ User.create(req.body, function(err, user){ if(err) return res.json(err); res.redirect("/users"); }); }); // show router.get("/:username", function(req, res){ User.findOne({username:req.params.username}, function(err, user){ if(err) return res.json(err); res.render("users/show", {user:user}); }); }); // edit router.get("/:username/edit", function(req, res){ User.findOne({username:req.params.username}, function(err, user){ if(err) return res.json(err); res.render("users/edit", {user:user}); }); }); // update // 2 router.put("/:username",function(req, res, next){ User.findOne({username:req.params.username}) // 2-1 .select("password") // 2-2 .exec(function(err, user){ if(err) return res.json(err); // update user object user.originalPassword = user.password; user.password = req.body.newPassword? req.body.newPassword : user.password; // 2-3 for(var p in req.body){ // 2-4 user[p] = req.body[p]; } // save updated user user.save(function(err, user){ if(err) return res.json(err); res.redirect("/users/"+req.params.username); }); }); }); module.exports = router;
7 actions에서 destroy만 빠졌습니다. 나머지는 기본형태와 같고, index와 update만 살펴봅시다.
1. User.find에는 찾을 조건({} = 모든 값)이 들어가고, sort(정렬) 함수를 넣어주기 위해서 callback 함수없이 괄호가 닫혔습니다.
sort 함수에는 {username:1} 이 들어가서 username을 기준으로 오름차순(asc)으로 정렬하고 있습니다.(-1을 넣으면 내림차순(desc)이 됩니다.)
callback 함수가 find 함수 밖으로 나오게 되면, exec(callback_함수)
를 사용합니다.
sort가 없을때 (callback 함수가 find 함수에 인자로 들어감)
User.find({}, function(err, users){ // ... })
sort가 있을때 (callback 함수가 exec 함수에 인자로 들어감)
User.find({}) .sort({username:1}) .exec(function(err, users){ // ... })
sort가 있을때와 없을때 모양을 비교해 보시면 쉽게 이해할 수 있습니다.
sort말고도 다양한 함수들이 끼어들 수 있는데요, 이러한 경우에 exec를 사용합니다.
2-1. 이번에는 findOneAndUpdate대신에 findOne으로 값을 찾은 후에 값을 수정하고 user.save함수로 값을 저장합니다. 단순히 값을 바꾸는 것이 아니라 user.password를 조건에 맞게 바꿔주어야 하기 때문이죠.
2-2. select 함수를 이용하면 DB에서 어떤 항목을 선택할지, 안할지를 정할 수 있습니다. user schema에서 password의 select을 false로 설정했으니 DB에 password가 있더라도 기본적으로 password를 읽어오지 않게 되는데, .select함수로 password 항목을 선택하고 있습니다. 참고로 select함수로 기본적으로 읽어오게 되어 있는 항목을 안 읽어오게 할 수도 있는데 이때는 항목이름 앞에 -를 붙이면 됩니다. 또한 하나의 select함수로 여러 항목을 동시에 정할 수도 있는데, 예를 들어 password를 읽어오고, name을 안 읽어오게 하고 싶다면 .select("password -name")를 입력하면 되겠습니다.
2-3. user의 update는 두가지로 나눌 수 있는데, password를 업데이트 하는 경우와, password를 업데이트 하지 않는 경우에 따라 user.password의 값이 바뀝니다.
2-4. user는 DB에서 읽어온 data이고, req.body가 실제 form으로 입력된 값이므로 각 항목을 덮어 쓰는 부분입니다.
// index.js // ... // DB setting ... // Other settings ... // Routes app.use("/", require("./routes/home")); app.use("/posts", require("./routes/posts")); app.use("/users", require("./routes/users")); // 1 // Port setting ...
1. 마지막으로 users route를 추가해 줍니다.
view들은 post와 비교해서 특별한 것이 전혀 없습니다.
<!-- views/partials/nav.ejs --> <nav class="navbar navbar-default"> <div class="container"> <div class="navbar-header"> <!-- ... --> </div> <div class="collapse navbar-collapse" id="myNavbar"> <ul class="nav navbar-nav"> <!-- ... --> </ul> <ul class="nav navbar-nav navbar-right"> <!-- 1 --> <li><a href="/users">Users</a></li> <li><a href="/users/new">Sign Up</a></li> </ul> </div> </div> </nav>
navbar-right class를 이용하면 우측정렬된 메뉴를 만들 수 있습니다.
<!-- views/users/index.ejs --> <!DOCTYPE html> <html> <head> <% include ../partials/head %> </head> <body> <% include ../partials/nav %> <div class="container user user-index"> <div class="contentBox"> <h3 class="contentBoxTop">Users</h3> <ul class="floats"> <% if(users == null || users.length == 0){ %> <div class="noData" colspan=100> There is no user yet.</div> <% } %> <% users.forEach(function(user) { %> <li> <a href="/users/<%= user.username %>"><%= user.username %></a> </li> <% }) %> </ul> </div> </div> <!-- container end --> </body> </html>
<!-- views/users/new.ejs --> <!DOCTYPE html> <html> <head> <% include ../partials/head %> </head> <body> <% include ../partials/nav %> <div class="container user user-new"> <form class="user-form form-horizontal" action="/users" method="post"> <div class="contentBox"> <h3 class="contentBoxTop">New User</h3> <fieldset> <div class="form-group"> <label for="username" class="col-sm-3 control-label">Username*</label> <div class="col-sm-9"> <input class="form-control" type="text" id="username" name="username" value=""> </div> </div> <div class="form-group"> <label for="name" class="col-sm-3 control-label">Name*</label> <div class="col-sm-9"> <input class="form-control" type="text" id="name" name="name" value=""> </div> </div> <div class="form-group"> <label for="email" class="col-sm-3 control-label">Email</label> <div class="col-sm-9"> <input class="form-control" type="text" id="email" name="email" value=""> </div> </div> <div class="form-group"> <label for="password" class="col-sm-3 control-label">Password*</label> <div class="col-sm-9"> <input class="form-control" type="password" id="password" name="password" value=""> </div> </div> <div class="form-group"> <label for="passwordConfirmation" class="col-sm-12 control-label">Password Confirmation*</label> <div class="col-sm-9 col-sm-offset-3"> <input class="form-control" type="password" id="passwordConfirmation" name="passwordConfirmation" value=""> </div> </div> <p> <small>*Required</small> </p> </fieldset> </div> <div class="buttons"> <button type="submit" class="btn btn-default">Submit</button> </div> </form> </div> <!-- container end --> </body> </html>
<!-- views/users/edit.ejs --> <!DOCTYPE html> <html> <head> <% include ../partials/head %> </head> <body> <% include ../partials/nav %> <div class="container user user-edit"> <div class="buttons"> <a class="btn btn-default" href="/users/<%= user.username %>">Back</a> </div> <form class="user-form form-horizontal" action="/users/<%= user.username %>?_method=put" method="post"> <div class="contentBox"> <h3 class="contentBoxTop">Edit User</h3> <fieldset> <div class="form-group"> <label for="currentPassword" class="col-sm-12 control-label">Current Password*</label> <div class="col-sm-9 col-sm-offset-3"> <input class="form-control" type="password" id="currentPassword" name="currentPassword" value=""> </div> </div> <hr></hr> <div class="form-group"> <label for="username" class="col-sm-3 control-label">Username*</label> <div class="col-sm-9"> <input class="form-control" type="text" id="username" name="username" value="<%= user.username %>"> </div> </div> <div class="form-group"> <label for="name" class="col-sm-3 control-label">Name*</label> <div class="col-sm-9"> <input class="form-control" type="text" id="name" name="name" value="<%= user.name %>"> </div> </div> <div class="form-group"> <label for="email" class="col-sm-3 control-label">Email</label> <div class="col-sm-9"> <input class="form-control" type="text" id="email" name="email" value="<%= user.email %>"> </div> </div> <div class="form-group"> <label for="newPassword" class="col-sm-12 control-label">New Password</label> <div class="col-sm-9 col-sm-offset-3"> <input class="form-control" type="password" id="newPassword" name="newPassword" value=""> </div> </div> <div class="form-group"> <label for="passwordConfirmation" class="col-sm-12 control-label">Password Confirmation</label> <div class="col-sm-9 col-sm-offset-3"> <input class="form-control" type="password" id="passwordConfirmation" name="passwordConfirmation" value=""> </div> </div> <p> <small>*Required</small> </p> </fieldset> </div> <div class="buttons"> <button type="submit" class="btn btn-default">Submit</button> </div> </form> </div> <!-- container end --> </body> </html>
<!-- views/users/show.ejs --> <!DOCTYPE html> <html> <head> <% include ../partials/head %> </head> <body> <% include ../partials/nav %> <div class="container user user-show"> <div class="buttons"> <a class="btn btn-default" href="/users">Back</a> <a class="btn btn-default" href="/users/<%= user.username %>/edit">Edit</a> </div> <form class="user-form form-horizontal" action="/users" method="post"> <div class="contentBox"> <h3 class="contentBoxTop"><%= user.username %></h3> <fieldset disabled> <div class="form-group"> <label for="name" class="col-sm-3">Name</label> <div class="col-sm-9"> <input class="form-control" type="text" id="name" name="name" value="<%= user.name %>"> </div> </div> <div class="form-group"> <label for="email" class="col-sm-3">Email</label> <div class="col-sm-9"> <input class="form-control" type="text" id="email" name="email" value="<%= user.email %>"> </div> </div> </fieldset> </div> </form> </div> <!-- container end --> </body> </html>
/* public/css/master.css */ /* global style ... */ /* home style ... */ /* post style ... */ /* user style */ .user { max-width: 320px; font-family: 'Open Sans', sans-serif; font-size: 12px; } .user-index ul{ margin: 0; padding: 3px 12px; } .user-index ul:after { /* 1 */ content: ""; display: block; clear: both; } .user-index ul li{ display: inline-block; list-style-type: none; float:left; } .user-index ul li a{ display: inline-block; text-decoration:none; margin: 3px; background-color: #eee; padding: 3px 10px; border-radius: 3px; } .user-index ul li a:hover{ background-color: #ccc; } .user-edit hr{ margin-top: 5px; margin-bottom: 11px; }
1. 이부분은 float의 parent를 위한 부분입니다. 이렇게 설정해 놓으면 float이 parent밖으로 안튀어나오게 됩니다. CSS 꿀팁이죠.
Sign Up 메뉴를 누르면 회원가입 form이 뜹니다.
회원가입이 끝나거나, 메뉴의 Users를 누르면 index가 나옵니다.
show입니다.
edit입니다. 회원가입할때는 password가 필수지만, 정보 수정시의 new password는 필수가 아닙니다.
이번 포스팅은 내용이 좀 길긴한데, 자꾸 비슷한 내용을 반복하면서 글을 계속 나눌 수 없어서 한번에 해봤습니다.
긴 강의 읽으시느라 수고하셨습니다. 또한 이번 강의를 보면서 비밀번호를 그대로 저장하는 보안상의 엄청나게 중요한 문제를 발견하신 분들이 있으실텐데, 다음 포스팅에서는 이에 대해 알아보겠습니다.
댓글
이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기