게시판 - 회원가입

소스코드

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

이 게시물의 소스코드는 게시판 만들기 / 게시판 - Front End 개발에서 이어집니다.

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

git reset --hard
git pull
git reset --hard 4adcb08
git reset --soft 9d1396d
npm install
atom .

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

git clone https://github.com/a-mean-blogger/board.git
cd board
git reset --hard 4adcb08
git reset --soft 9d1396d
npm install
atom .

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


사이트에 회원가입 기능을 만들어 봅시다!

회원가입 기능은 왠지 복잡할 것 같지만, 사실 post model이나 주소록 만들기 예제의 contact model과 같이 회원(user) model을 만들고, CRUD 중 create, 즉 회원 데이터를 생성해 DB에 저장하는 것이 회원가입 기능입니다. 다만 user model에는 특별히 비밀번호가 추가되는데, 이를 통해 로그인시에 username과 password가 일치하는 user가 DB에 있는지를 확인하게 됩니다. 로그인 기능은 이후의 강의에서 다루도록 하고, 이번 강의에서는 user CRUD를 만들어 봅시다.

contact, post에 이어 이번이 3번째로 CRUD 기능을 만드는데요, 이번엔 back end(model, route) 코드와 front end(ejs) 코드를 한 강의에 진행하겠습니다.

폴더구조

코드 - js

user 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!');
    }
    else if(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 : requiretrue 대신 배열([ ... ])이 들어갔습니다. 첫번째는 true/false 값이고, 두번째는 에러메세지입니다. 그냥 true/false을 넣을 경우 기본 에러메세지가 나오고, 위와 같이 배열을 사용해서 에러메세지 내용을 원하는 대로 변경할 수 있습니다.

password에는 select:false가 추가되었습니다. select:false로 설정하면 DB에서 해당 모델을 읽어 올때 해당 항목값을 읽어오지 않습니다. 비밀번호는 중요하기 때문에 DB에서 값을 읽어오지 않게 설정했습니다. 물론 이 값이 필요한 경우도 있는데, 이럴 때는 해당 값을 읽어오도록 특별히 설정을 해주어야 합니다. 이 설정은 아래 route코드에서 설명합니다.

2. DB에 저장되는 값 이외의 항목이 필요할 땐 virtual 항목으로 만듭니다. 즉 passwordConfirmation, originalPassword, currentPassword, newPassword는 회원가입, 회원정보 수정을 위해 필요한 항목이지만, DB에 저장할 필요는 없는 값들입니다. 이처럼 DB에 저장될 필요는 없지만, model에서 사용하고 싶은 항목들은 virtual로 만듭니다.

3. password를 DB에 생성, 수정하기 전에 값이 유효(valid)한지 확인(validate)을 하는 코드를 작성합니다. 

3-1. validation callback 함수 속에서 this는 user model입니다. 헷갈리지 않도록 user 변수에 넣었습니다.

3-2. model.isNew 항목은 해당 모델이 생성되는 경우에는 true, 아니면 false의 값을 가집니다. 이 항목을 이용해서 현재 password validation이 '회원가입' 단계인지, 아니면 '회원 정보 수정'단계인지를 알 수 있습니다.

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

// 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/'+user.username);
      });
  });
});

// destroy
router.delete('/:username', function(req, res){
  User.deleteOne({username:req.params.username}, function(err){
    if(err) return res.json(err);
    res.redirect('/users');
  });
});

module.exports = router;

나머지는 지금까지 만들어 왔던 route들과 기본 형태는 같습니다. 다만 id대신에 username을 route에 사용하였는데요, 그냥 id말고 다른 항목도 route에 사용할 수도 있다는 것을 보여주기 위함입니다.

index와 update만 자세히 살펴봅시다. 나머지는 username을 id대신 사용하는 것 말고는 특별한 것이 없습니다.

1. 지금까지의 index route과는 다르게, 찾은 값을 정렬하는 기능이 추가되었습니다. sort함수가 추가되었는데요, 이 함수에는 {username:1} 이 들어가서 username을 기준으로 오름차순(asc)으로 정렬하고 있습니다.(-1을 넣으면 내림차순(desc)이 됩니다.)

sort 함수를 사용하기 위해 구조가 좀 바뀌었는데요, sort함수가 있을 때와 없을 때를 비교해서 살펴봅시다.

sort가 없을때 (callback 함수가 find 함수에 인자로 들어감)

User.find({}, function(err, users){
  ...
});

sort가 있을때 (callback 함수가 exec함수에 인자로 들어감)

User.find({})
  .sort({username:1})
  .exec(function(err, users){
    ...
  });

sort말고도 다양한 함수들이 끼어들 수 있는데요, 이러한 경우에 위와 같이 exec 함수를 사용합니다.

2-1. 이번에는 findOneAndUpdate함수대신에 findOne함수로 값을 찾은 후에 값을 수정하고 user.save함수로 값을 저장합니다. 단순히 값을 바꾸는 것이 아니라 user.password를 조건에 맞게 바꿔주어야 하기 때문이죠.

2-2. select함수를 이용하면 DB에서 어떤 항목을 선택할지, 안할지를 정할 수 있습니다. user schema에서 password의 select을 false로 설정했으니 DB에 password가 있더라도 기본적으로 password를 읽어오지 않게 되는데, select('password')를 통해서 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

...

// Routes
app.use('/', require('./routes/home'));
app.use('/posts', require('./routes/posts'));
app.use('/users', require('./routes/users')); // 1

...

1. 마지막으로 users route를 추가해 줍니다.

코드 - ejs

view들은 post와 비교해서 특별한 것이 전혀 없습니다.

<!-- views/partials/nav.ejs -->

...
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      ...
      <ul class="navbar-nav ml-auto"> <!-- 1 -->
        <li class="nav-item"><a href="/users" class="nav-link">Users</a></li>
        <li class="nav-item"><a href="/users/new" class="nav-link">Sign Up</a></li>
      </ul>
    </div>
...

ml-auto class를 이용하면 우측정렬된 메뉴를 만들 수 있습니다.

나머지 ejs파일들은 특별한 내용은 없으므고 그냥 쭉 살펴봅시다.

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

<!DOCTYPE html>
<html>
  <head>
    <%- include('../partials/head') %>
  </head>
  <body>
    <%- include('../partials/nav') %>

    <div class="container mb-3">

      <h3 class="mb-3">Users</h3>

      <ul class="list-group">
        <% if(users == null || users.length == 0){ %>
          <li class="list-group-item"> There is no user yet.</div>
        <% } %>
        <% users.forEach(function(user) { %>
          <li class="list-group-item">
            <a href="/users/<%= user.username %>"><%= user.username %></a>
          </li>
        <% }) %>
      </ul>

    </div>
  </body>
</html>

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

<!DOCTYPE html>
<html>
  <head>
    <%- include('../partials/head') %>
  </head>
  <body>
    <%- include('../partials/nav') %>

    <div class="container mb-3">

      <h3 class="contentBoxTop mb-3">New User</h3>

      <form action="/users" method="post">

        <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="" class="form-control">
          </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="" class="form-control">
          </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="" class="form-control">
          </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">
          </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">
          </div>
        </div>
        <p>
          <small>*Required</small>
        </p>

        <div class="form-group">
          <button type="submit" class="btn btn-primary">Submit</button>
        </div>
      </form>

    </div>
  </body>
</html>

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

<!DOCTYPE html>
<html>
  <head>
    <%- include('../partials/head') %>
  </head>
  <body>
    <%- include('../partials/nav') %>

    <div class="container mb-3">

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

      <form action="/users/<%= user.username %>?_method=put" method="post">

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

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

        <div class="buttons">
          <a class="btn btn-primary" href="/users/<%= user.username %>">Back</a>
          <button type="submit" class="btn btn-primary">Submit</button>
        </div>

      </form>

    </div>
  </body>
</html>

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

<!DOCTYPE html>
<html>
  <head>
    <%- include('../partials/head') %>
  </head>
  <body>
    <%- include('../partials/nav') %>

    <div class="container mb-3">

      <h3 class="contentBoxTop"><%= user.username %></h3>

      <form class="user-form" action="/users" method="post">
        <fieldset disabled>
          <div class="form-group row">
            <label for="name" class="col-sm-3 col-form-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 row">
            <label for="email" class="col-sm-3 col-form-label">Email</label>
            <div class="col-sm-9">
              <input class="form-control" type="text" id="email" name="email" value="<%= user.email %>">
            </div>
          </div>
        </fieldset>
      </form>

      <div>
        <a class="btn btn-primary" href="/users">Back</a>
        <a class="btn btn-primary" href="/users/<%= user.username %>/edit">Edit</a>
        <form action="/users/<%= user.username %>?_method=delete" method="post" class="d-inline">
          <a class="btn btn-primary" href="javascript:void(0)" onclick="confirm('Do you want to delete this?')?this.parentElement.submit():null;">Delete</a>
        </form>
      </div>

    </div>
  </body>
</html>

코드 - css

/* public/css/master.css */

...

.user-form {
  width: 400px;
}

실행결과

Sign Up 메뉴를 누르면 회원가입 form이 뜹니다.

회원가입이 끝나거나, 메뉴의 Users를 누르면 index가 나옵니다.

show입니다.

edit입니다. 회원가입할때는 password가 필수지만, 정보 수정시의 new password는 필수가 아닙니다. 다만 current password가 DB에 저장된 값과 일치해야합니다.

마치며..

긴 강의 읽으시느라 수고하셨습니다. 이번 포스팅은 코드가 좀 많긴한데, 비슷한 내용을 자꾸 반복하면서 계속 글을 나눌 수 없어서 한번에 해봤습니다.

이번 강의를 보면서 비밀번호를 그대로 DB에 저장하는 보안상의 엄청나게 중요한 문제를 발견하신 분들이 있으실텐데, 다음 포스팅에서는 이에 대해 알아보겠습니다.

댓글

댓글쓰기

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

UP