게시판 - User Error 처리

소스코드

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

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

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

git reset --hard
git pull
git reset --hard 59e9202
git reset --soft 259189d
npm install
atom .

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

git clone https://github.com/a-mean-blogger/board.git
cd board
git reset --hard 59e9202
git reset --soft 259189d
npm install
atom .

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


이번 포스팅에서는 User 생성/수정에서 일어날 수 있는 error들을 처리해 봅시다.

현재는 username이 중복된다든지, 비밀번호가 일치하지 않는다든지 하는 경우에 웹사이트가 에러을 JSON 그대로 보여주고 사이트가 뻗는데, 좋은 사용자 경험(User Experience)가 아닙니다. 에러가 있는 경우 아래와 같이 사이트에 에러들을 보기 좋게 표시해 줍시다.

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

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

이 강의에서는 connect-flash package를 사용해서 flash를 만듭니다.

추가로 regex(Regular Expression, 정규표현식)를 사용하여 User의 username, password, name, email 항목들에게 특정한 형식의 값만 저장 할 수 있게 해봅시다(email에는 email형식의 값만 받게 하고, 비밀번호는 영문자와 숫자를 혼용할 것 등등). regex는 문자열이 특정한 형식을 가지고 있는지 아닌지를 판단하기 위해 사용됩니다.

폴더구조

Package 설치

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

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

코드 - js

// models/User.js

...

// 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}
});

...

// 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!');
    }
    else if(!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!');
    }
  }
});

...

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, 정규표현식)이 들어가서 값이 regex에 부합하는지 않으면 에러메세지를 내게 됩니다.
정규표현식은 문자열에 특정한 규칙에 맞는 문자열이 있는지 알아보는 표현식인데요, 이거 하나만으로도 책 한권을 쓸 수 있는 내용이니까 따로 공부를 하셔야합니다.

1-2는 username을 위한 regex인데요, regex(/^.{4,12}$/)를 해석해 보면,
/^.{4,12}$/ : regex는 / /안에 작성합니다. 즉 / /를 통해 이게 regex임을 알수 있습니다.
/^.{4,12}$/ : ^문자열의 시작 위치를 나타냅니다.
/^.{4,12}$/ : .어떠한 문자열이라도 상관없음을 나타냅니다.
/^.{4,12}$/ : {숫자1,숫자2}숫자1 이상, 숫자2 이하의 길이 나타냅니다.
/^.{4,12}$/ : $문자열의 끝 위치를 나타냅니다.
해석하면  "문자열의 시작 위치에 길이 4 이상 12 이하인 문자열이 있고, 바로 다음이 문자열의 끝이여야 함". 즉 전체 길이가 4이상 12자리 이하의 문자열이라면 이 regex를 통과할 수 있습니다.

1-3의 이메일의 형식을 위한 regex인데요, /^[a-zA-Z0-9._%+-][email protected][a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/는 이 regex는 인터넷에서 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!');
    }
    else if(!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를 반환합니다.

2-4. 2-3에서 false가 반환되는 경우 2-2에서 선언한 문자열로 model.invalidate함수를 호출합니다.

// index.js

...
var methodOverride = require('method-override');
var flash = require('connect-flash'); // 1
var session = require('express-session'); // 1
var app = express();

...

// Other settings
...
app.use(methodOverride('_method'));
app.use(flash()); // 2
app.use(session({secret:'MySecret', resave:true, saveUninitialized:true})); //3

...

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은session을 hash화하는데 사용되는 값으로 비밀번호 정도로 생각하면 됩니다. 아무값이나 넣어주고 해커가 알 수 없게 합시다. 환경변수에 저장하면 더욱 안전하겠죠. resave와 saveUninitialized은 현재 강의에서 중요한 내용이 아니므로 설명은 생략합니다. 꼭 알고싶으신 분들은 https://github.com/expressjs/session#options 를 참고합시다.

// routes/users.js

...

// 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 });
});

// 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');
  });
});

...

// 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')
    .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;
      for(var p in req.body){
        user[p] = req.body[p];
      }

      // 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 -->

...

      <h3 class="mb-3">Edit User</h3>

      <form action="/users/<%= username %>?_method=put" method="post"> <!-- 1 -->

        <div class="form-group row">
          <label for="currentPassword" class="col-sm-3 col-form-label">Current Password*</label>
          <div class="col-sm-9 col-sm-offset-3">
            <input type="password" id="currentPassword" name="currentPassword" value="" class="form-control <%= (errors.currentPassword)?'is-invalid':'' %>"> <!-- 2 -->
            <% if(errors.currentPassword){ %> <!-- 3 -->
              <span class="invalid-feedback"><%= errors.currentPassword.message %></span>
            <% } %>
          </div>
        </div>

        <hr></hr>

        <div class="form-group row">
          <label for="username" class="col-sm-3 col-form-label">Username*</label>
          <div class="col-sm-9">
            <input type="text" id="username" name="username" value="<%= user.username %>" class="form-control <%= (errors.username)?'is-invalid':'' %>"> <!-- 2 -->
            <% if(errors.username){ %> <!-- 3 -->
              <span class="invalid-feedback"><%= errors.username.message %></span>
            <% } %>
          </div>
        </div>

        <div class="form-group row">
          <label for="name" class="col-sm-3 col-form-label">Name*</label>
          <div class="col-sm-9">
            <input type="text" id="name" name="name" value="<%= user.name %>" class="form-control <%= (errors.name)?'is-invalid':'' %>"> <!-- 2 -->
            <% if(errors.name){ %> <!-- 3 -->
              <span class="invalid-feedback"><%= errors.name.message %></span>
            <% } %>
          </div>
        </div>

        <div class="form-group row">
          <label for="email" class="col-sm-3 col-form-label">Email</label>
          <div class="col-sm-9">
            <input type="text" id="email" name="email" value="<%= user.email %>" class="form-control <%= (errors.email)?'is-invalid':'' %>"> <!-- 2 -->
            <% if(errors.email){ %> <!-- 3 -->
              <span class="invalid-feedback"><%= errors.email.message %></span>
            <% } %>
          </div>
        </div>

        <div class="form-group row">
          <label for="newPassword" class="col-sm-3 col-form-label">New Password</label>
          <div class="col-sm-9 col-sm-offset-3">
            <input type="password" id="newPassword" name="newPassword" value="" class="form-control <%= (errors.newPassword)?'is-invalid':'' %>"> <!-- 2 -->
            <% if(errors.newPassword){ %> <!-- 3 -->
              <span class="invalid-feedback"><%= errors.newPassword.message %></span>
            <% } %>
          </div>
        </div>

        <div class="form-group row">
          <label for="passwordConfirmation" class="col-sm-3 col-form-label">Password Confirmation</label>
          <div class="col-sm-9 col-sm-offset-3">
            <input type="password" id="passwordConfirmation" name="passwordConfirmation" value="" class="form-control <%= (errors.passwordConfirmation)?'is-invalid':'' %>"> <!-- 2 -->
            <% if(errors.passwordConfirmation){ %> <!-- 3 -->
              <span class="invalid-feedback"><%= errors.passwordConfirmation.message %></span>
            <% } %>
          </div>
        </div>

        <p>
          <small>*Required</small>
        </p>

        <% if(errors.unhandled){ %> <!-- 4 -->
          <div class="alert alert-danger">
            <%= errors.unhandled %>
          </div>
        <% } %>

...

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

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

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

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

<!-- views/users/new.ejs -->

...

        <div class="form-group row">
          <label for="username" class="col-sm-3 col-form-label">Username*</label>
          <div class="col-sm-9">
            <input type="text" id="username" name="username" value="<%= user.username %>" class="form-control <%= (errors.username)?'is-invalid':'' %>"> <!-- 1, 2 -->
            <% if(errors.username){ %> <!-- 3 -->
              <span class="invalid-feedback"><%= errors.username.message %></span>
            <% } %>
          </div>
        </div>
        <div class="form-group row">
          <label for="name" class="col-sm-3 col-form-label">Name*</label>
          <div class="col-sm-9">
            <input type="text" id="name" name="name" value="<%= user.name %>" class="form-control <%= (errors.name)?'is-invalid':'' %>"> <!-- 1, 2 -->
            <% if(errors.name){ %> <!-- 3 -->
              <span class="invalid-feedback"><%= errors.name.message %></span>
            <% } %>
          </div>
        </div>
        <div class="form-group row">
          <label for="email" class="col-sm-3 col-form-label">Email</label>
          <div class="col-sm-9">
            <input type="text" id="email" name="email" value="<%= user.email %>" class="form-control <%= (errors.email)?'is-invalid':'' %>"> <!-- 1, 2 -->
            <% if(errors.email){ %>
              <span class="invalid-feedback"><%= errors.email.message %></span>
            <% } %>
          </div>
        </div>
        <div class="form-group row">
          <label for="password" class="col-sm-3 col-form-label">Password*</label>
          <div class="col-sm-9">
            <input type="password" id="password" name="password" value="" class="form-control <%= (errors.password)?'is-invalid':'' %>"> <!-- 1, 2 -->
            <% if(errors.password){ %> <!-- 3 -->
              <span class="invalid-feedback"><%= errors.password.message %></span>
            <% } %>
          </div>
        </div>
        <div class="form-group row">
          <label for="passwordConfirmation" class="col-sm-3 col-form-label">Password Confirmation*</label>
          <div class="col-sm-9 col-sm-offset-3">
            <input type="password" id="passwordConfirmation" name="passwordConfirmation" value="" class="form-control <%= (errors.passwordConfirmation)?'is-invalid':'' %>"> <!-- 1, 2 -->
            <% if(errors.passwordConfirmation){ %> <!-- 3 -->
              <span class="invalid-feedback"><%= errors.passwordConfirmation.message %></span>
            <% } %>
          </div>
        </div>
        <p>
          <small>*Required</small>
        </p>

        <% if(errors.unhandled){ %> <!-- 4 -->
          <div class="alert alert-danger">
            <%= errors.unhandled %>
          </div>
        <% } %>

...

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

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

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

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

마치며

생각보다 포스팅이 길어졌네요. regex를 처음 보신 분들은 좀 당황스러우셨을 수도 있는데 배우고 나면 상당히 유용하게 쓰이므로 반드시 익히시길 바랍니다.

다음은 드디어 로그인입니다.

댓글

이현종 2020.05.02
항상 감사하면서 공부하고있습니다. 공부하다가 궁금한점이생겼는데 route edit부분에서 'edit은 처음 접속하는 경우에는 DB에서 값을 찾아 form에 기본값을 생성하고, update에서 오류가 발생해 돌아오는 경우에는 기존에 입력했던 값으로 form에 값들을 생성해야 합니다.
이를 위해 user에는 || {} 를 사용하지 않았으며, user flash값이 있으면 오류가 있는 경우, user flash 값이 없으면 처음 들어온 경우로 가정하고 진행합니다.' 라고 설명해주셨는데 user의 flash에 || {}을 포함해서 해도 큰 문제를 느끼지 못하고 || {}가 없는점에 대해 이해가 잘되지 않습니다.. 혹시 부가적인 설명이 가능할까요??
이현종 2020.05.02
계속 하다보니까 문제점을 찾았습니다!! 다음 login 파트에서의 [get]/login 에서의   var username = req.flash('username')[0]  var errors = req.flash('errors')[0] || {} 이 부분에서 errors flash부분의 || {}을 제거해주면 form에서의 username부분에서 에러가 뜨는데 왜그럴까요..ㅠ
I
Ian H 2020.05.04
@이현종,
var errors = req.flash('errors')[0] || {} 는 req.flash('errors')[0]의 값이 존재한다면 해당 값을 errors에 대입하고, 만약 값이 존재하지 않으면 {}를 대입하는 코드입니다.
뒤에 || {}를 빼버리면 req.flash('errors')[0]의 값이 존재하지 않으면 undefined이 들어가겠죠. view 코드를 보면 errors.title과 같이 errors의 하위 항목에 접근하는 코드가 있는데 이때 errors가 {}라면 errors.title은 undefined가 되어 값이 없다고 판별이 되지만, errors 자체가 undefined라면 errors.title은 접근할 수 없기 때문에 에러가 납니다!
만약 이해가 안되시면 다시 질문해주세요^^
이현종 2020.05.08
@Ian H,
항상 친절한 답변 감사합니다!! 덕분에 도움이 많이 됩니다ㅎㅎ
u
utoru80 2020.08.03
 user flash값이 있으면 오류가 있는 경우, user flash 값이 없으면 처음 들어온 경우로 가정하고 진행합니다. 이부분이요.  굳이 위 두가지 분기점으로 구분하는이유가 단지 처음에 들어온경우이면 if(err) return res.json(err); 를 추가하기 위한것인가요??  몽고디비에서 데이터가져온후 if 절과 else절의 차이가  if(err) return res.json(err);  밖에 없어요. update에서 오류로 들어온경우는 이미 몽고디비에 꼭 데이터값이 있으니까  if(err) return res.json(err);  적을 필요 없는거죠??
I
Ian H 2020.08.04
@utoru80,
다르게 설명하자면,  - 최초로 edit에 오는 경우 -> user변수에 값이 없음 -> DB에서 user를 받아와서 view를 생성함(form를 user의 DB 값들로 채움) - update에서 에러가 생겨 edit으로 돌아오는 경우 -> user변수의 값을 flash에서 받아옴 -> user를 사용해서 view의 값을 생성함(form를 update에서 입력됐던 값으로 채움)
else의 경우에는 몽고DB에서 값을 가져오지 않습니다. update에 전달된 form의 값이 user가 되어 flash에 들어갑니다.(물론 update에 전달되기 전에, edit의 view에 생성된 값들은 DB에서 옵니다. 하지만 edit 페이지에서 사용자가가 이 값들을 변경할 수 있기 때문에 이 값들이 DB에서 온다고 할 수는 없습니다.)
u
utoru80 2020.08.11
@Ian H,
아  유저가 유저페이지에서 에디트 할때 처음들어온거면 db에서 값을 view에 뿌려주고.  유저에디트를 유저가 편집 하다가  update 하다가 에러플래시 걸려서 다시 이라우터로 오면..  수정하다 에러걸린 폼값을 다시 view에 뿌려주는거군요.. 결국 분기점의 다른점은  req.params.username 이 어디에서 들어왔나에 따라서 다른거네요.  잘이해한건지 모르겠네요.  나중에 보면 또잊어먹을까바 댓글로 남겨봅니다. 감사합니다. 
I
Ian H 2020.08.11
@utoru80,
정확하게 이해하셨어요^^
u
utoru80 2020.08.10
//create user  else if(user.password !== user.passwordConfirmation) {  //이부분이요. user.passwordConfirmation는 폼에서 받아온값이라 그냥 값이고 해시가 안되있는값인데
user.password  이거는 user모델에있는 해시화된 값이 잖아요. model.create 나 model.save 실행시 save 이벤트전 userSchema.pre함수로 해시화 시켜서 저장한 변수값이 user.password 잖아요.
bcrypt.compareSync 함수로 비교하는것도아니고...
user.password !== user.passwordConfirmation 는 비교 불가니까  ! 이니까까 무조건 true 잖아요.
u
utoru80 2020.08.10
결론은... user.password !== user.passwordConfirmation  이게아니고 user.password !== bcrypt.hashSync(user.passwordConfirmation) 요렇게 둘다 해시값으로 맞처놓고 비교해야하는거 아닌가요? 질문 읽어주셔서 감사합니다..
I
Ian H 2020.08.10
@utoru80,
create user시 user는 db에서 오는 값이 아니라 form에서 입력받은 값이므로 user.password에는 사용자가 입력한 값(해시가 아닌 값)이 들어있습니다^^
https://www.a-mean-blog.com/ko/blog/단편강좌/_/node-js-디버깅-방법 강의를 참고해서 해당 부분에 어떤값이 있는지 한번 직접 확인해보세요
u
utoru80 2020.08.25
// 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 }); ---->이부분이요                                         이 응답페이지요.   관련 라우터가 아무리바도  users/:username/edit 이거 밖에 없는데요.                                        users/edit', { username:req.params.username, user:user, errors:errors } 이런식으로 쓰면                                       users/:username/edit 요런식으로 변경되서 get url 을 요청하나요????
    });   }   else {     res.render('users/edit', { username:req.params.username, user:user, errors:errors });   } });
I
Ian H 2020.08.25
@utoru80,
res.render('users/edit', {...})는 view template으로부터 html 코드를 만드는(render) 함수입니다. 
현재 view engine이 ejs로 설정되어 있고, view들의 default folder가 'views'이므로 'users/edit'인 view template은 views/user/edit.ejs로 선택됩니다. 
선택된 view template에 전달받은 오브젝트({ username:req.params.username, user:user, errors:errors })를 넣어서 html 코드를 render하게 되고, 이 html 코드가 웹사이트에 표시되게 됩니다.
u
utoru80 2020.08.26
@Ian H,
감사합니다. 이렇게 ejs로 뿌려주고 ejs 에서   <form action="/users/<%= username %>   이런식으로 요청해서  다시 라우터에서router.get('/:username/edit' 이런식으로 받는군요. 
I
Ian H 2020.08.26
@utoru80,
네 맞습니다 👍
댓글쓰기

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

UP