게시판 - User Error 처리

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

이 게시물의 소스코드는 게시판 만들기 / 게시판 - 계정 비밀번호 암호화(bcrypt)에서 이어집니다.

board.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.

git reset --hard
git pull
git reset --hard 54fc085
git reset --soft 66e787e
npm install
atom .

board.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 board.git 을 clone 합니다.

git clone https://github.com/a-mean-blogger/board.git
cd board
git reset --hard 54fc085
git reset --soft 66e787e
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/board/tree/54fc085a3b966dd570abef2aef0089707471ac31


이번 포스팅에서는 User 생성/수정에서 일어날 수 있는 error들을 처리해 봅시다.
현재는 username이 중복된다든지, 비밀번호가 일치하지 않는다든지 하는 경우에 에러JSON을 그대로 보여주는데, 에러가 있는 경우 해당 form 으로 돌아가 아래와 같이 에러를 표시하게 해 봅시다.

생성 과정(create)에서 에러가 있는 경우 생성 페이지(new)로, 수정 과정(update)에서 에러가 있는 경우 수정 페이지(edit)로 route간의 이동이 일어나는데, 이때 route간의 정보 전달을 위해 flash를 사용합니다.

flash는 변수처럼 이름을 정하고 값(문자열, 숫자, 배열, 객체 등등 어떠한 형태의 값이라도 사용 가능)을 저장할 수 있는데, 한번 생성 되면 사용될 때까지 서버에 저장이 되어 있다가 한번 사용되면 사라지는 형태의 data입니다.

이 포스팅에서는 connect-flash package를 사용해서 flash를 만듭니다.

또한 User의 username, password, name, email 항목들에 regex를 사용하여 특별한 제약조건을 추가해 봅시다.
(email에는 email형식의 값만 받게 하고, 비밀번호는 영문자와 숫자를 혼용할 것 등등)

폴더구조

주황색은 변경된 파일, 회색은 변화가 없는 파일입니다.

Package 설치

express-session와 connect-flash package를 설치해 줍니다.
express-session은 connect-flash를 실행하기 위해 필요한 package입니다.

$ npm install --save express-session connect-flash

코드 - js

// models/User.js

var mongoose = require("mongoose");
var bcrypt = require("bcrypt-nodejs");

// schema //1
var userSchema = mongoose.Schema({
 username:{
  type:String,
  required:[true,"Username is required!"],
  match:[/^.{4,12}$/,"Should be 4-12 characters!"],
  trim:true,
  unique:true
 },
 password:{
  type:String,
  required:[true,"Password is required!"],
  select:false
 },
 name:{
  type:String,
  required:[true,"Name is required!"],
  match:[/^.{4,12}$/,"Should be 4-12 characters!"],
  trim:true
 },
 email:{
  type:String,
  match:[/^[a-zA-Z0-9._%+-][email protected][a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/,"Should be a vaild email address!"],
  trim:true
 }
},{
 toObject:{virtuals:true}
});

// virtuals ...

// password validation // 2
var passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/;
var passwordRegexErrorMessage = "Should be minimum 8 characters of alphabet and number combination!";
userSchema.path("password").validate(function(v) {
  var user = this;

  // create user
  if(user.isNew){
    if(!user.passwordConfirmation){
      user.invalidate("passwordConfirmation", "Password Confirmation is required!");
    }
    if(!passwordRegex.test(user.password)){
      user.invalidate("password", passwordRegexErrorMessage);
    } else if(user.password !== user.passwordConfirmation) {
      user.invalidate("passwordConfirmation", "Password Confirmation does not matched!");
    }
  }

  // update user
  if(!user.isNew){
    if(!user.currentPassword){
      user.invalidate("currentPassword", "Current Password is required!");
    }
    if(user.currentPassword && !bcrypt.compareSync(user.currentPassword, user.originalPassword)){
      user.invalidate("currentPassword", "Current Password is invalid!");
    }
    if(user.newPassword && !passwordRegex.test(user.newPassword)){
      user.invalidate("newPassword", passwordRegexErrorMessage);
    } else if(user.newPassword !== user.passwordConfirmation) {
      user.invalidate("passwordConfirmation", "Password Confirmation does not matched!");
    }
  }
});

// hash password ...
// model methods ...
// model & export ...

...은 이전과 비교해서 변화가 없는 부분입니다.
User schema와 Password validation 부분이 바뀌었는데, 따로 따로 살펴봅시다.

// schema // 1
var userSchema = mongoose.Schema({
 username:{
  type:String,
  required:[true,"Username is required!"],
  match:[/^.{4,12}$/,"Should be 4-12 characters!"], // 1-2
  trim:true, // 1-1
  unique:true
 },
 password:{
  type:String,
  required:[true,"Password is required!"],
  select:false
 },
 name:{
  type:String,
  required:[true,"Name is required!"],
  match:[/^.{4,12}$/,"Should be 4-12 characters!"], // 1-2
  trim:true // 1-1
 },
 email:{
  type:String,
  match:[/^[a-zA-Z0-9._%+-][email protected][a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/,"Should be a vaild email address!"], // 1-3
  trim:true // 1-1
 }
},{
 toObject:{virtuals:true}
});

이전에 비해 상당히 달라진 모습이지만 전체적인 구조가 바뀌었을 뿐 실제로 바뀐건 match랑 trim밖에 없습니다.

1-1. trim은 문자열 앞뒤에 빈칸이 있는 경우 빈칸을 제거해 주는 옵션입니다. 어렵지 않죠.

1-2, 1-3. match는 regex(Regular Expression, 정규표현식)를 사용해서 문자열을 검사하는 내용입니다.
정규표현식은 문자열에 특정한 규칙에 맞는 문자열이 있는지 알아보는 표현식인데요, 이거 하나만으로도 책 한권을 쓸 수 있는 내용이니까 따로 공부를 하셔야합니다.
schema에서 match: [/정규표현식/,"에러메세지"]를 사용하면, 해당 표현식에 맞지 않는 값이 오는 경우 에러메세지를 내보냅니다.

1-2의 regex(/^.{4,12}$/)를 해석해 보면,
/^.{4,12}$/ : regex는 / /안에 작성합니다.
/^.{4,12}$/ : ^는 문자열의 시작을 나타냅니다.
/^.{4,12}$/ : .는 어떠한 문자열이라도 상관없음을 나타냅니다.
/^.{4,12}$/ : {숫자1,숫자2}숫자1 이상, 숫자2 이하의 길이 나타냅니다.
/^.{4,12}$/ : $는 문자열의 끝을 나타냅니다.
^$가 regex의 시작과 끝에 동시에 있으면 전체 문자열이 조건에 맞아야 성공입니다.
.{4,12}는 어떠한 문자라도 좋지만 4개 이상 12개 이하여야 한다는 뜻입니다.
즉 전체 길이가 4자리 이상 12자리 이하 길이라면 어떠한 문자라도 regex를 통과한다는 의미가 됩니다.

1-3의 /^[a-zA-Z0-9._%+-][email protected][a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/는 문자열이 이메일의 형식이 맞는지 아닌지를 확인하는 식인데, 인터넷에서 regex를 공부해 보시고 직접 해독해 보시기 바랍니다!

// password validation // 2
var passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/; // 2-1
var passwordRegexErrorMessage = "Should be minimum 8 characters of alphabet and number combination!"; // 2-2
userSchema.path("password").validate(function(v) {
  var user = this;

  // create user
  if(user.isNew){
    if(!user.passwordConfirmation){
      user.invalidate("passwordConfirmation", "Password Confirmation is required!");
    }
    if(!passwordRegex.test(user.password)){ // 2-3
      user.invalidate("password", passwordRegexErrorMessage); // 2-4
    } else if(user.password !== user.passwordConfirmation) {
      user.invalidate("passwordConfirmation", "Password Confirmation does not matched!");
    }
  }

  // update user
  if(!user.isNew){
    if(!user.currentPassword){
      user.invalidate("currentPassword", "Current Password is required!");
    }
    if(user.currentPassword && !bcrypt.compareSync(user.currentPassword, user.originalPassword)){
      user.invalidate("currentPassword", "Current Password is invalid!");
    }
    if(user.newPassword && !passwordRegex.test(user.newPassword)){ // 2-3
      user.invalidate("newPassword", passwordRegexErrorMessage); // 2-4
    } else if(user.newPassword !== user.passwordConfirmation) {
      user.invalidate("passwordConfirmation", "Password Confirmation does not matched!");
    }
  }
});

2-1. 8-16자리 문자열 중에 숫자랑 영문자가 반드시 하나 이상 존재해야 한다는 뜻의 regex입니다.

2-2. 에러메세지가 반복되므로 변수로 선언하였습니다.

2-3. 정규표현식.test(문자열) 함수는 문자열정규표현식을 통과하는 부분이 있다면 true를, 그렇지 않다면 false를 return합니다.

2-4. 2-3에서 false가 return되는 경우 2-2에서 선언한 문자열로 model.invalidate을 호출합니다.

// index.js

var express    = require("express");
var mongoose   = require("mongoose");
var bodyParser  = require("body-parser");
var methodOverride = require("method-override");
var flash     = require("connect-flash"); // 1
var session    = require("express-session"); // 1
var app = express();

// DB setting ...

// Other settings
app.set("view engine", "ejs");
app.use(express.static(__dirname+"/public"));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:true}));
app.use(methodOverride("_method"));
app.use(flash()); // 2
app.use(session({secret:"MySecret", resave:true, saveUninitialized:true})); // 3

// Routes ...
// Port setting ...

1. 새로 설치된 package로 부터 flash, session을 선언하였습니다.

2. flash를 초기화 합니다. 이제부터 req.flash라는 함수를 사용할 수 있습니다.
req.flash(문자열, 저장할_값) 의 형태로 저장할_값(숫자, 문자열, 오브젝트등 어떠한 값이라도 가능)을 해당 문자열에 저장합니다.
이때 flash는 배열로 저장되기 때문에 같은 문자열을 중복해서 사용하면 순서대로 배열로 저장이 됩니다.
req.flash(문자열) 인 경우 해당 문자열에 저장된 값들을 배열로 불러옵니다. 저장된 값이 없다면 빈 배열([])을 return합니다.

3. session은 서버에서 접속자를 구분시키는 역할을 합니다. user1과 user2가 웹사이트를 보고 있는 경우 해당 user들을 구분하여 서버에서 필요한 값 들(예를 들어 로그인 상태 정보 등등)을 따로 관리하게 됩니다. flash에 저장되는 값 역시 user1이 생성한 flash는 user1에게, user2가 생성한 flash는 user2에게 보여져야 하기 때문에 session이 필요합니다. {secret:"MySecret", resave:true, saveUninitialized:true}은 옵션부분으로, secret은 hash를 생성하는데 사용되는 값으로 비밀번호 정도로 생각하면 됩니다. 아무값이나 넣어주고 해커가 알 수 없게 합시다. 환경변수에 저장하면 더욱 안전하겠죠.resave와 saveUninitialized은 현재 강의에서 중요한 내용이 아니므로 설명은 생략합니다. 꼭 알고싶으신 분들은 https://github.com/expressjs/session#options 를 참고합시다.

// routes/users.js

var express = require("express");
var router = express.Router();
var User  = require("../models/User");

// Index ...

// New // 1
router.get("/new", function(req, res){
 var user = req.flash("user")[0] || {};
 var errors = req.flash("errors")[0] || {};
 res.render("users/new", { user:user, errors:errors });
});

// create
router.post("/", function(req, res){
 User.create(req.body, function(err, user){
  if(err){
   req.flash("user", req.body);
   req.flash("errors", parseError(err));
   return res.redirect("/users/new");
  }
  res.redirect("/users");
 });
});

// show ...

// edit
router.get("/:username/edit", function(req, res){
 var user = req.flash("user")[0];
 var errors = req.flash("errors")[0] || {};
 if(!user){
  User.findOne({username:req.params.username}, function(err, user){
   if(err) return res.json(err);
   res.render("users/edit", { username:req.params.username, user:user, errors:errors });
  });
 } else {
  res.render("users/edit", { username:req.params.username, user:user, errors:errors });
 }
});

// update
router.put("/:username",function(req, res, next){
 User.findOne({username:req.params.username})
 .select({password:1})
 .exec(function(err, user){
  if(err) return res.json(err);

  // update user object ...

  // save updated user
  user.save(function(err, user){
   if(err){
    req.flash("user", req.body);
    req.flash("errors", parseError(err));
    return res.redirect("/users/"+req.params.username+"/edit");
   }
   res.redirect("/users/"+user.username);  });
 });
});

module.exports = router;

// Functions
function parseError(errors){
 var parsed = {};
 if(errors.name == 'ValidationError'){
  for(var name in errors.errors){
   var validationError = errors.errors[name];
   parsed[name] = { message:validationError.message };
  }
 } else if(errors.code == "11000" && errors.errmsg.indexOf("username") > 0) {
  parsed.username = { message:"This username already exists!" };
 } else {
  parsed.unhandled = JSON.stringify(errors);
 }
 return parsed;
}

route별로 살펴봅시다

// New
router.get("/new", function(req, res){
 var user = req.flash("user")[0] || {};
 var errors = req.flash("errors")[0] || {};
 res.render("users/new", { user:user, errors:errors });
});

user 생성시에 에러가 있는 경우 new페이지에 에러와 기존에 입력했던 값들을 보여주게 되는데, 이 값들은 create route에서 생성된 flash로부터 받아옵니다. flash는 array가 오게 되는데 이 프로그램에서는 하나 이상의 값이 저장되는 경우가 없고, 있더라도 오류이므로 무조건 [0]의 값을 읽어 오게 했습니다. 값이 없다면(처음 new페이지에 들어온 경우)에는 || {}를 사용해서 빈 오브젝트를 넣어 user/new페이지를 생성합니다.

// create
router.post("/", function(req, res){
 User.create(req.body, function(err, user){
  if(err){
   req.flash("user", req.body);
   req.flash("errors", parseError(err));
   return res.redirect("/users/new");
  }
  res.redirect("/users");
 });
});

user 생성시에 오류가 있다면 user, error flash를 만들고 new페이지로 redirect합니다.

user 생성시에 발생할 수 있는 오류는 2가지로 첫번째는 User model의 userSchema에 설정해둔 validation을 통과하지 못한 경우와, mongoDB에서 오류를 내는 경우입니다. 이때 발생하는 error 객체의 형식이 상이하므로parseError라는 함수를 따로 만들어서 err을 분석하고 일정한 형식으로 만들게 됩니다.

// edit
router.get("/:username/edit", function(req, res){
 var user = req.flash("user")[0];
 var errors = req.flash("errors")[0] || {};
 if(!user){
  User.findOne({username:req.params.username}, function(err, user){
   if(err) return res.json(err);
   res.render("users/edit", { username:req.params.username, user:user, errors:errors });
  });
 } else {
  res.render("users/edit", { username:req.params.username, user:user, errors:errors });
 }
});

edit은 처음 접속하는 경우에는 DB에서 값을 찾아 form에 기본값을 생성하고, update에서 오류가 발생해 돌아오는 경우에는 기존에 입력했던 값으로 form에 값들을 생성해야 합니다.

이를 위해 user에는 || {} 를 사용하지 않았으며, user flash값이 있으면 오류가 있는 경우, user flash 값이 없으면 처음 들어온 경우로 가정하고 진행합니다.

이제부터 render시에 username을 따로 보내주는데, 이전에는 user.username이 항상 해당 user의 username이였지만 이젠 user flash에서 값을 받는 경우 username이 달라 질 수도 있기 때문에 주소에서 찾은 username을 따로 보내주게됩니다.

// Functions
function parseError(errors){
 var parsed = {};
 if(errors.name == 'ValidationError'){
  for(var name in errors.errors){
   var validationError = errors.errors[name];
   parsed[name] = { message:validationError.message };
  }
 } else if(errors.code == "11000" && errors.errmsg.indexOf("username") > 0) {
  parsed.username = { message:"This username already exists!" };
 } else {
  parsed.unhandled = JSON.stringify(errors);
 }
 return parsed;
}

mongoose에서 내는 에러와 mongoDB에서 내는 에러의 형태가 다르기 때문에 이 함수를 통해 에러의 형태를
{ 항목이름: { message: "에러메세지" } 로 통일시켜주는 함수입니다.
if 에서 mongoose의 model validation error를, else if 에서 mongoDB에서 username이 중복되는 error를, else 에서 그 외 error들을 처리합니다.
함수 시작부분에 console.log("errors: ", errors")를 추가해 주면 원래 에러의 형태를 console 에서 볼 수 있습니다.

코드 - ejs

<!-- 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/<%= username %>">Back</a>
   </div>

   <form class="user-form form-horizontal" action="/users/<%= username %>?_method=put" method="post">
    <div class="contentBox">
     <h3 class="contentBoxTop">Edit User</h3>
     <fieldset>
      <div class="form-group <%= (errors.currentPassword)?'has-error':'' %>">
       <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="">
        <% if(errors.currentPassword){ %>
         <span class="help-block"><%= errors.currentPassword.message %></span>
        <% } %>
       </div>
      </div>
      <hr></hr>
      <div class="form-group <%= (errors.username)?'has-error':'' %>">
       <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 %>">
        <% if(errors.username){ %>
         <span class="help-block"><%= errors.username.message %></span>
        <% } %>
       </div>
      </div>
      <div class="form-group <%= (errors.name)?'has-error':'' %>">
       <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 %>">
        <% if(errors.name){ %>
         <span class="help-block"><%= errors.name.message %></span>
        <% } %>
       </div>
      </div>
      <div class="form-group <%= (errors.email)?'has-error':'' %>">
       <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 %>">
        <% if(errors.email){ %>
         <span class="help-block"><%= errors.email.message %></span>
        <% } %>
       </div>
      </div>
      <div class="form-group <%= (errors.newPassword)?'has-error':'' %>">
       <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="">
        <% if(errors.newPassword){ %>
         <span class="help-block"><%= errors.newPassword.message %></span>
        <% } %>
       </div>
      </div>
      <div class="form-group <%= (errors.passwordConfirmation)?'has-error':'' %>">
       <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="">
        <% if(errors.passwordConfirmation){ %>
         <span class="help-block"><%= errors.passwordConfirmation.message %></span>
        <% } %>
       </div>
      </div>
      <p>
       <small>*Required</small>
      </p>
     </fieldset>
    </div>
    <div class="buttons">
     <button type="submit" class="btn btn-default">Submit</button>
    </div>
    <% if(errors.unhandled){ %>
     <div class="alert alert-danger">
      <%= errors.unhandled %>
     </div>
    <% } %>
   </form>

  </div> <!-- container end -->
 </body>
</html>

1. user.username 대신 router에서 받은 username을 사용합니다.

2. 각 항목의 form-group class가 있는 div에 <%= (errors.항목이름)?'has-error':'' %> 가 추가되었습니다. 에러가 있다면 bootstraphas-error class를 사용합니다.

3. 각 항목의input밑에 <% if(errors.항목이름){ %> 부분으로 에러메세지를 보여주는 부분이 추가되었습니다.

4. errors.unhandled는 따로 표시합니다.

<!-- 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 <%= (errors.username)?'has-error':'' %>">
       <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 %>">
        <% if(errors.username){ %>
         <span class="help-block"><%= errors.username.message %></span>
        <% } %>
       </div>
      </div>
      <div class="form-group <%= (errors.name)?'has-error':'' %>">
       <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 %>">
        <% if(errors.name){ %>
         <span class="help-block"><%= errors.name.message %></span>
        <% } %>
       </div>
      </div>
      <div class="form-group <%= (errors.email)?'has-error':'' %>">
       <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 %>">
        <% if(errors.email){ %>
         <span class="help-block"><%= errors.email.message %></span>
        <% } %>
       </div>
      </div>
      <div class="form-group <%= (errors.password)?'has-error':'' %>">
       <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="">
        <% if(errors.password){ %>
         <span class="help-block"><%= errors.password.message %></span>
        <% } %>
       </div>
      </div>
      <div class="form-group <%= (errors.passwordConfirmation)?'has-error':'' %>">
       <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="">
        <% if(errors.passwordConfirmation){ %>
         <span class="help-block"><%= errors.passwordConfirmation.message %></span>
        <% } %>
       </div>
      </div>
      <p>
       <small>*Required</small>
      </p>
     </fieldset>
    </div>
    <div class="buttons">
     <button type="submit" class="btn btn-default">Submit</button>
    </div>
    <% if(errors.unhandled){ %>
     <div class="alert alert-danger">
      <%= errors.unhandled %>
     </div>
    <% } %>
   </form>

  </div> <!-- container end -->
 </body>
</html>

1. edit.ejs와 마찬가지로 각 항목의 form-group class가 있는 div에 <%= (errors.항목이름)?'has-error':'' %> 가 추가되었습니다.

2. 이제 new.ejs에도 value에 user 객체의 값을 기본값으로 들어갑니다.

3. edit.ejs와 마찬가지로 각 항목의input밑에 <% if(errors.항목이름){ %> 부분으로 에러메세지를 보여주는 부분이 추가되었습니다. 

4. errors.unhandled는 따로 표시합니다.

마치며

생각보다 포스팅이 길어졌네요. regex를 처음 보신 분들은 좀 당황스러우셨을 수도 있는데 배우고 나면 상당히 유용하게 쓰이므로 반드시 익히시길 바랍니다.
이 글에서 post model의 에러 처리는 하지 않았는데요, 이건 직접 한번 만들어 보세요.
다음은 드디어 로그인입니다.

댓글

J
Junil Ko 2016.12.02
사용자 비밀번호 업데이트가 불가능합니다. 원인은 user.password를 변경할 패스워드로 할당하면서 두 가지 문제가 생기는데 하나는 비교 대상이 아닌 점과 패스워드가 평문으로 저장된 점입니다. 따라서 간단히 이 문제를 피할 방법으로 authenticate 메서드를 아래와 같이 수정해봤는데 좀 더 좋은 방법이 있으시면 의견 부탁드립니다.
userSchema.methods.authenticate = function(password, originalPassword) {     var user = this;
    if (originalPassword) {         return bcrypt.compareSync(password, originalPassword);         } else {         return bcrypt.compareSync(password, user.password);     } }
I
Ian H 2016.12.09
@Junil Ko,
MEN-게시판/게시판-계정-비밀번호-암호화-bcrypt에 잘못된 부분이 있었는데요, 해당 강좌에서 수정하였습니다. user schema에 붙이는 method는 user model의 값을 사용해야 의미가 있는데 두개의 값을 입력받아서 두개의 값을 bcrypt.compareSync함수로 비교하면 굳이 해당 함수를 user schema에 붙여야 할 필요가 없죠.  userSchema.methods.authenticate는 나중에 login할 때 사용되는 method이고 userSchema.methods.authenticate가 호출된 부분을 bcrypt.compareSync로 바꾸어 주었습니다.  오류를 찾아주셔서 감사해요^^
S
SungMin Kim 2017.07.12
username을 수정할때  정상 수정 됬을 경우 res.redirect 시 파라메터에 오타가 아닌가 싶습니다. bbbb에서 aaaa로 정상변경되었을 경우에도 /users/bbbb로 redirect 되어 오류가 나서 req.params.username이 아닌 req.body.username으로 redirect 로 변경하니 정상적으로 show화면이 호출되었습니다.
I
Ian H 2017.07.12
@SungMin Kim,
아.. 저의 실수입니다. user.username으로 수정하였습니다. 제보 감사합니다!
S
SunWoong Lee 2017.09.10
username 중복 확인을 하는 코드 errors.code == "11000" && errors.errmsg.indexOf("username") > 0  부분에 대해서 조금 자세하게 설명해주실 수 있을까요? ㅠㅠ
I
Ian H 2017.09.11
@SunWoong Lee,
http://www.a-mean-blog.com/ko/blog/Node-JS-첫걸음/게시판-만들기/게시판-회원가입 에서 user schema의 username을 unique:true로 만들었던것 기억나시나요.  이때문에 username이 중복되면 mongoDB에서 errors.code가 11000인 error를 return합니다. mongoose가 아닌 mongoDB에서 내는 error라서 그 형태가 다릅니다.  그 부분을 처리하기 위한 코드에요.
parseError 함수 첫줄에 console.log("errors: ", errors)를 넣으시고 username을 중복시킨다든가(mongoDB error), username을 입력하지 않는다든가(mongoose validation error) 등 에러를 유도하면 error object의 형태를 볼 수 있습니다.
S
SunWoong Lee 2017.09.12
indexOf는 존재하는지에 대해서 검사하신거죠? 그러니까 정리하자면, 중복되면 11000 code가 뜨는 것이고, errmsg중에 username이라는 키워드가 있는지 확인함으로써, username이 중복되었는지 확인하는 코드겠네요! 다만 unique:true인 value값이 2개 이상이라면 어디에서 중복이 일어났는지도 다시 확인하는 작업이 필요하겠네요...?!
I
Ian H 2017.09.22
@SunWoong Lee,
그렇죠 사실 errors.errmsg를 loop를 돌면서 키워드들을 수집한 후 현재 schema와 비교해서 어디가 중복됬는지 자동으로 알려주는 함수를 만들 수도 있지만 코드가 너무 복잡해지는 것을 막으려다 보니 이렇게 되었네요. 이부분에 이렇게 관심을 가져주실 줄 알았다면 더 고민을 했을텐데 ㅠㅠ
박수진 2017.10.28
안녕하세요 해당 강의를 진행하다가 유효성 검사에서 막혔는데요 소스는 기존 소스와 별반 다를 것 없어보이는데 유저에러처리소스 추가 이후에 유저생성 유저수정시  [nodemon] app crashed - waiting for file changes before starting... 콘솔창에 이런 에러가나고 유효성검사가 아예뜨질않아요 ..ㅜㅜ 시간 괜찮으시면 제 소스한번 살펴봐주실 수 있으신가요? https://github.com/Hajimara/mean
I
Ian H 2017.10.28
@박수진,
우와... 소스코드를 보니 설명까지 넣어가며 만드신걸 보니 제가 다 뿌듯하네요^^
에러메세지는 처음부터 읽는 것이 중요합니다.
ReferenceError: parseError is not defined     at .../mean/routes/users.js:30:27 라는 부분이 있죠? /routes/users.js의 30번째 줄 27번째 위치에서 parseError를 못찾는 에러가 발생했네요. users.js의 코드를 보니 밑에 함수이름이 ParseError라고 첫글자가 대문자로 되어 있구요.
이걸 해결하면 다른 에러가 또 나올텐데.. 그건 한번 직접 해결해 보세요^^
박수진 2017.10.28
@Ian H,
빠른 피드백 정말 감사드립니다! 말해주신 에러와 다른 에러 해결하고 작동도 잘됩니다! 덕분에 오류가나면 어느쪽을 살펴봐야하는지 배웠네요 노드와 몽고로 이렇게 자세히 포스팅해주시는 분이 많이 없어서  올려주시는 강좌 감사히 잘보고있습니다!
박수진 2017.10.28
아 그리고 제가 회원가입이 계속 비밀번호 때문에 막혀서 본게있는데요 암호 정규식에 var passwordRegex = /^(?=.*[A-Za-z])(?=.*d)[A-Za-zd]{8,16}$/; // 2-1 이부분이 [A-Za-zd].{8,16} 점하나 추가하면 회원가입이 제대로 돌아가요 혹시 안되시는분들 계시면 정규식 이걸로 수정해보세요
I
Ian H 2017.10.30
@박수진,
아.. 강의 본문에 오타가 있는데 8-16자리 영어+숫자 텍스트의 정규식은  /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/입니다. 강의 본문 수정하였습니다. 알려주셔서 감사해요^^
H
Hyunsung Kim 2017.11.29
models/User.js 파일에서 첫 코드는 // update user 중 두번째 if문의 !user.authenticate(user.current....) 으로 되어 있고 세부 내용 설명 중에 나오는 코드는 그 부분이 !bcrypt.compareSync(....)으로 되어 있네요
I
Ian H 2017.11.30
@Hyunsung Kim,
지적하신 부분을 수정하였습니다. 감사합니다^^
H
Hyunsung Kim 2017.11.30
@Ian H,
어이쿠.... 지적이라뇨 ㅜㅜ 단지 오타일뿐입니다. 늘 감사합니다!
T
Tyler Park 2017.12.09
안녕하세요. 초보지만 강좌 열심히 따라보면서 배우고 있습니다. 덕분에 배우고 깨닫는 바가 많아 감사드립니다. 한창 진행 중에 요번 단계에서 막히는 부분이 하나 있는데요. 비밀번호 확인 부분에서 Password Confirmation is required! 가 계속 뜨길래 invalidate 하는 부분 앞단에 console.log(user.passwordConfirmation); 로 확인을 해봤더니. undefined 라는 결과가 나옵니다. 제가 뭔가를 실수 한 걸까요? 위에서부터 빠짐 없이 다 진행한 것 같은데...
I
Ian H 2017.12.11
@Tyler Park,
안녕하세요 반갑습니다. 정확히 어디부터 passwordConfirmation이 없어졌는지를 찾아야겠죠. route가 시작될 때 console.log(req.body)로 req.body부터 확인해 보시고 계속 따라가면 찾을 수 있을 것 같습니다. 혹시라도 정 못찾으시면 소스코드를 github에 올려주세요 확인해 보겠습니다^^
H
Hyunsung Kim 2017.12.17
User.js 에서 userSchema 생성시에 name의 match 값을 위와 동일하게 [/^.{4.12}$/, "..."]로 했는데 왜 계속 required 메세지가 나오는 걸까요? ㅜㅜ 오타도 없고 이름은 제 실명 그대로 세글자(한글) 적었는데
I
Ian H 2017.12.18
@Hyunsung Kim,
"Name is required!" 메세지가 나오시나요? "Should be 4-12 characters!"가 아니라?
참고로 javascript에서 한글은 한 글자의 길이가 1이기 때문에 한글 세글자를 넣으면 4-12 길이 조건에 맞지 않게 됩니다. 
H
Hyunsung Kim 2017.12.29
@Ian H,
아.. 전 바보인가봐요... 예전에 공부할 때 한글 1글자를 영문 2글자로 봤어서 착각한 것 같네요 2-12로 바꾸고 하니 잘 되는군요...
I
Ian H 2018.01.03
@Hyunsung Kim,
ASCII에서 영문은 1 bite, 한글은 2 bites이죠 ㅋㅋ C언어 라이브러리중 문자열 길이를 bite 크기로 구하는 함수들에서는 실제로 그렇게 작동하기도 했어요
H
Hyunsung Kim 2018.01.09
@Ian H,
그러게요... ㅜㅜ 바보짓 했네요 새해 복 많이 받으세요!!! 왕창!!!! 와장창!!!!
I
Ian H 2018.01.09
@Hyunsung Kim,
현성님도 새해 복 많이 받으세요^^
쮸리맨 2018.02.15
express-session deprecated undefined resave option; provide resave option index.js:26:9 express-session deprecated undefined saveUninitialized option; provide saveUninitialized option index.js:26:9 (node:8232) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): TypeError: Parameter "url" mus t be a string, not undefined
서버 실행시 이런에러가뜨는데 원인을모르겠습니다 ..ㅠ
I
Ian H 2018.02.15
@쮸리맨,
제가 방금 이 강좌의 코드를 다운받아서 실행해봤는데요,  express-session deprecated undefined resave option; provide resave option index.js:26:9 express-session deprecated undefined saveUninitialized option; provide saveUninitialized option index.js:26:9 이 에러는 express-session이 버전업되면서 나오는 에러로 구글에 express-session deprecated undefined resave option 치면 해결법이 금방 나옵니다. 참고로 위 두개는 에러는 처리하지 않아도 위 강좌에서는 아무 이상 없습니다.
(node:8232) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): TypeError: Parameter "url" mus t be a string, not undefined 이 에러는 제 코드에서는 나오지 않는데요, 혹시 직접 작성하신 코드에서 표시되는 에러라면 소스코드를 github에 올려주시면 제가 한번 확인해 보겠습니다.
n
nodemon 2018.05.03
username 중복방지코드중에 특정이름(undefined)은 중복가능하도록 예외처리하려합니다 parseError 부분 수정하는거같은데 어느코드가 추가될까요... } else if(errors.code == "11000" && errors.errmsg.indexOf("username") > 0) { 질문이 많네요 죄송합니다 ㅠㅠ
I
Ian H 2018.05.03
@nodemon,
perseError는 이미 생성된 error를 특정한 형식의 객체로 바꾸는 역학을 하는 변수입니다.
우선 username의 중복을 허용하려면/models/User.js 의 userSchema를 생성하는 부분에서 username의 unique:true 부분을 지우면 중복이 허용됩니다.
그 다음 username을 생성할 때마다 DB에서 직접 해당 username이 있는지를 먼저 확인한 후에 허락을 하는 수 밖에 없겠네요
n
nodemon 2018.05.04
@Ian H,
아아~~~ 이해했습니다 감사합니다!!! ^^
이병만 2018.08.19
변수의 타입을 var에서 const로, function을 arrow function으로 수정하는 과정에서 created와 updated의 시간이 게시판에 출력이 되지 않는데 혹시 다음과 같은 과정을 진행해서 그런 것인가요? ES6문법에 대해서 알아보다가 const로 수정하는 중이었습니다.
I
Ian H 2018.08.20
@이병만,
const와 arrow function은 무조건 바꾸는 것이 아니라, 필요한 부분에만 변경을 하고, var와 일반 function도 필요한 자리에는 그대로 두어야 합니다. 작성하신 코드를 github에 올리시면 제가 한번 확인해 보겠습니다!
이병만 2018.08.21
이병만 2018.08.21
감사합니다~~
I
Ian H 2018.08.21
@이병만,
Post.js에서 postSchema의 virtual 부분을 화살표 함수로 바꾸셨는데, 여기에서 this를 사용하고 있으므로 이부분은 화살표 함수로 바꾸시면 안됩니다.
화살표함수와 일반함수는 해당함수의 scope가 다릅니다. 용도에 따라 일반함수를 써야 하는 곳도 있고 화살표 함수를 써야 하는 곳이 있는 것이지 무조건 화살표 함수를 써야 하는 것은 아닙니다.
const도 상수인 경우에만 사용하고 상수가 아닌 곳에는 여전히 var를 사용해야 합니다.
이병만 2018.08.19
제가 에디터를 VScode를 사용중입니다.  mongo랑  연결하는 포스팅 보고 했는데 잘 실행되다가 갑자기 다음과 같은 에러가 발생하는데 검색을 해봐도 해결방법을  찾지 못했습니다.  DB ERROR :  { MongoError: Authentication failed.     at Function.MongoError.create (C:\Users\ssey0\React-study\post\man_server\node_modules\mongodb-core\lib\error.js:31:11)     at C:\Users\ssey0\React-study\post\man_server\node_modules\mongodb-core\lib\connection\pool.js:497:72     at authenticateStragglers (C:\Users\ssey0\React-study\post\man_server\node_modules\mongodb-core\lib\connection\pool.js:443:16)     at Connection.messageHandler (C:\Users\ssey0\React-study\post\man_server\node_modules\mongodb-core\lib\connection\pool.js:477:5)     at Socket.<anonymous> (C:\Users\ssey0\React-study\post\man_server\node_modules\mongodb-core\lib\connection\connection.js:333:22)     at emitOne (events.js:116:13)     at Socket.emit (events.js:211:7)     at addChunk (_stream_readable.js:263:12)     at readableAddChunk (_stream_readable.js:250:11)     at Socket.Readable.push (_stream_readable.js:208:10)     at TCP.onread (net.js:597:20)   name: 'MongoError',   message: 'Authentication failed.',   ok: 0,   errmsg: 'Authentication failed.',   code: 18,   codeName: 'AuthenticationFailed',   operationTime: Timestamp { _bsontype: 'Timestamp', low_: 1, high_: 1534702042 },   '$clusterTime':    { clusterTime: Timestamp { _bsontype: 'Timestamp', low_: 1, high_: 1534702042 },      signature: { hash: [Object], keyId: [Object] } } }
그런데 VScode 터미널 말고 기본 터미널에서 실행시키면 되는데 그 이유를 잘 모르겠어서 질문드립니다.
I
Ian H 2018.08.20
@이병만,
MongoError: Authentication failed. 라고 뜨는 것은 DB 접속할 때 username 혹은 password가 맞지 않아서 뜨는 에러입니다. DB 접속하는 부분에서 접속주소를 출력하여(예를들면 console.log("process.env.MONGO_DB: ", process.env.MONGO_DB); ) VScode 터미널과 기본 터미널에서 같은 값이 출력되는 지를 확인해 보세요.
이병만 2018.08.21
Validator failed for path `password` with value `11111111` 회원가입을 하면 이러한 에러메세지가 뜨는데 잘 작동할 때도 있고 그렇지 않을때도 있는데 파악을 잘 못하겠습니다.
I
Ian H 2018.08.21
@이병만,
이 부분도 마찬가지입니다. User.js에서  userSchema.path("password").validate(function(v){ ... 를 userSchema.path("password").validate((v) => { 로, 화살표 함수로 바꾸셨는데, 다음라인에 this를 사용하고 있으므로 화살표 함수로 사용하시면 안됩니다.
Schema에서 path().validate의 this는 전달받은 전체 document(위 예제에서는 입력받은 user 오브젝트)를 가리키게 됩니다. mongoose를 만든 사람이 그렇게 작동하도록 만들었습니다.
참고문헌: https://mongoosejs.com/docs/validation.html#update-validators-and-this
여기에 화살표 함수를 사용하면 scope가 변경되서 this가 가리키는 값이 바뀌게 되죠.
참고로 이 부분을 화살표 함수로 바꾸면 '항상' 작동하지 않습니다. 아마 이걸 바꾸기 직전엔 잘 작동하여서 확률적으로 작동한다고 생각하신것 같은데, 위 코드가 변경되면 일관적으로 작동하지 않습니다.
이병만 2018.08.22
@Ian H,
ES6 문법에 대해서 공부를 하다가 Arrow 함수에 대해서 알게 되어서 모든 함수를 바꿔도 되는지 알았는데 잘못된 생각이었나보네요 조언 감사드립니다~~.
G
GS 2018.09.12
안녕하세요 users.js에서 update 부분에 password를 읽기위해 가져오는 .select 부분이 .select("password")에서 .select({password:1})로 변경된거 같은데 어떤 차이가 있나요?
I
Ian H 2018.09.12
@GS,
안녕하세요. 
결과에는 차이가 없고 그냥 작성하는 방법이 두가지 입니다. 특별한 이유없이 두가지 방법이 사용되었네요. 혼란을 드려서 죄송합니다 ㅠ
select("password") : password 항목을 가져옴 select("-password") : password 항목을 숨김 select({password: 1}) : password 항목을 가져옴 select({password: 0}) : password 항목을 숨김
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP