게시판 - Back End 개발

소스코드

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

이 게시물의 소스코드는 게시판 만들기 / 게시판 - 프로젝트 생성 및 navbar에서 이어집니다.

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

git reset --hard
git pull
git reset --hard 2151499
git reset --soft 3f61f06
npm install
atom .

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

git clone https://github.com/a-mean-blogger/board.git
cd board
git reset --hard 2151499
git reset --soft 3f61f06
npm install
atom .

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


이제 게시판 CRUD를 만들텐데 두 파트로 나누어서 이번 글에서 back end 코드를 작성하고, 다음 글에서 front end 코드를 작성하겠습니다.

게시판이라고 해서 주소록 만들기와 크게 다르지 않습니다. 게시판도 결국 데이터를 생성하고, 보여주고, 수정하고, 지우는 CRUD 기능이니까요.

이번 글에서는 Post(게시물) model을 만들고 back end CRUD를 구현해 봅시다.

폴더구조

주황색은 변경된 파일, 녹색은 새로 생성된 파일, 회색은 변화가 없는 파일입니다.

코드

// models/Post.js

var mongoose = require("mongoose");

// schema
var postSchema = mongoose.Schema({ // 1
  title:{type:String, required:true},
  body:{type:String},
  createdAt:{type:Date, default:Date.now}, // 2
  updatedAt:{type:Date},
},{
  toObject:{virtuals:true} // 4
});

// virtuals // 3
postSchema.virtual("createdDate")
.get(function(){
  return getDate(this.createdAt);
});

postSchema.virtual("createdTime")
.get(function(){
  return getTime(this.createdAt);
});

postSchema.virtual("updatedDate")
.get(function(){
  return getDate(this.updatedAt);
});

postSchema.virtual("updatedTime")
.get(function(){
  return getTime(this.updatedAt);
});

// model & export
var Post = mongoose.model("post", postSchema);
module.exports = Post;

// functions
function getDate(dateObj){
  if(dateObj instanceof Date)
    return dateObj.getFullYear() + "-" + get2digits(dateObj.getMonth()+1)+ "-" + get2digits(dateObj.getDate());
}

function getTime(dateObj){
  if(dateObj instanceof Date)
    return get2digits(dateObj.getHours()) + ":" + get2digits(dateObj.getMinutes())+ ":" + get2digits(dateObj.getSeconds());
}

function get2digits(num){
  return ("0" + num).slice(-2);
}

1. Post의 schema는 title, body, createdAt, updatedAt으로 구성이 되어 있습니다.

2. default 항목으로 기본 값을 지정할 수 있습니다. 함수명을 넣으면 해당 함수의 return이 기본값이 됩니다.
(Date.now는 현재시간을 리턴하는 함수입니다)

3. 여기에 postSchema.virtual함수를 이용해서 createdDate, createdTime, updatedDate, updatedTime의 virtuals(가상 항목들)을 설정해 주었습니다. virtuals은 실제 DB에 저장되진 않지만 model에서는 db에 있는 다른 항목들과 동일하게 사용할 수 있는데, get, set함수를 설정해서 어떻게 해당 virtual 값을 설정하고 불러올지를 정할 수 있습니다. createdAt, updatedAt은 Data 타입으로 설정되어 있는데 javascript은 Data 타입에 formatting 기능(시간을 어떠한 형식으로 보여줄지 정하는 것, 예를 들어 2017-01-02로 할지, 01-02-2017로 할지 등등)을 따로 설정해 주어야 하기 때문에 이와 같은 방식을 택했습니다.

4. virtual들을 object에서 보여주는 mongoose schema의 option입니다.

// routes/posts.js

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

// Index 
router.get("/", function(req, res){
  Post.find({})                  // 1
  .sort("-createdAt")            // 1
  .exec(function(err, posts){    // 1
    if(err) return res.json(err);
    res.render("posts/index", {posts:posts});
  });
});

// New
router.get("/new", function(req, res){
  res.render("posts/new");
});

// create
router.post("/", function(req, res){
  Post.create(req.body, function(err, post){
    if(err) return res.json(err);
    res.redirect("/posts");
  });
});

// show
router.get("/:id", function(req, res){
  Post.findOne({_id:req.params.id}, function(err, post){
    if(err) return res.json(err);
    res.render("posts/show", {post:post});
  });
});

// edit
router.get("/:id/edit", function(req, res){
  Post.findOne({_id:req.params.id}, function(err, post){
    if(err) return res.json(err);
    res.render("posts/edit", {post:post});
  });
});

// update
router.put("/:id", function(req, res){
  req.body.updatedAt = Date.now(); // 2
  Post.findOneAndUpdate({_id:req.params.id}, req.body, function(err, post){
    if(err) return res.json(err);
    res.redirect("/posts/"+req.params.id);
  });
});

// destroy
router.delete("/:id", function(req, res){
  Post.deleteOne({_id:req.params.id}, function(err){
    if(err) return res.json(err);
    res.redirect("/posts");
  });
});

module.exports = router;

route은 1, 2번을 제외하면 주소록 만들기의 contacts route와 형태가 같습니다.(변수명인 contact가 post로 바뀌었죠)

1. 나중에 생성된 data가 위로 오도록 정렬합니다. find와 function 사이에 sort함수가 들어간 형태인데요,
원래 모양은

Post.find({}, function(err, posts){ ... })

이였는데

  Post.find({})
  .sort("-createdAt")
  .exec(function(err, posts){ ... })

이렇게 바뀌었죠. 사실 원래모양 역시

  Post.find({})
  .exec(function(err, posts){ ... })

를 줄인 표현인데요, .exec함수 앞에 DB에서 데이터를 어떻게 찾을지, 어떻게 정렬할지 등등을 함수로 표현하고, exec안의 함수에서 해당 data를 받아와서 할일을 정하는 구조입니다.

.sort()함수는 string이나 object를 받아서 데이터 정렬방법을 정의하는데요, 문자열로 표현하는 경우 정렬할 항목명을 문자열로 넣으면 오름차순으로 정렬하고, 내림차순인 경우 -를 앞에 붙여줍니다. 두가지 이상으로 정렬하는 경우 빈칸을 넣고 각각의 항목을 적어주면 됩니다. object를 넣는 경우 {createdAt:1}(오름차순), {createdAt:-1}(내림차순) 이런식으로 넣어주면 됩니다.

2. data의 수정이 있는 경우 수정된 날짜를 업데이트 합니다.

// index.js

// ...
// DB setting ...
// Other settings ...

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

// Port setting ...

1. 마지막으로 posts route를 index.js에 추가해 줍니다.

실행결과

아직 views가 없으니 생성한 route를 실행해 볼 수가 없죠. 실행결과는 다음 포스트에서..!

마치며...

model은 조금 더 복잡해지긴 했지만 route주소록 만들기과 거의 똑같죠?
7 actions로 route를 한번 잘 만들어 놓으면 새로운 route을 만들때 필요한 부분만 수정해서 쓸 수 있습니다.
다음 포스트에서는 이번에 만든 back end를 사용하여 실제 page들을 만들어 보겠습니다.

댓글

K
Kyoungtaek Oh 2016.10.20
강의를 잘 보고 있습니다. index.js 에서 app.use로 라우팅 하는 라인이 빠져 있어서 아마 다음 Front 부분을 완성해도 페이지가 안보이는 것같습니다. 저는 app.use('/posts', require('./routes/posts')); 를 추가하였습니다. 그리고 뒤에 회원가입할때도 요고 한줄이 빠져있네요. 저처럼 초보는 한줄 차이로 좌절을 맛보기 때문에... ㅎㅎㅎ 수고하세요.
I
Ian H 2016.10.21
@Kyoungtaek Oh,
아이고 제 실수 때문에 고생하셨겠어요 ㅠ 죄송합니다. 두 포스팅 모두 수정하였습니다. 감사합니다!
U
Ukdong Son 2016.10.31
안녕하세요. 자세한 설명으로 node에 대한 이해도가 엄청 올라 갔어요~ 완전 감사합니다~^^
궁금증이 생겨 질문을 하려는데 여기다 해도 될까요?
Mongodb에 document가 많아지면 페이지 구성을 어떻게 해야 하나요? 건수가 많아지니 문제가 생겨서요.
한페이지에 20건정도만 표시하고 나머지는 페이지로 표시되며 페이지를 누르면 다음 document를 읽어오게 하고싶어요...^^; 개발을 너무 오랜만에 하니 감이 안잡히네요... ㅜㅜ
지금 개발은 back-end는 node restfull module 간단히 구성해두고 front-end 는 anguler2(ionic2)으로 구성하고 있는데요... restful get method를 주면 모든 데이터만 나와요... 데이터가 만으니 읽어오는 시간도 너무 걸리고 있어서요...
I
Ian H 2016.10.31
@Ukdong Son,
반갑습니다^^ Mongodb의 skip, limit을 활용하시면 페이지를 구성하실 수 있습니다.  여기서도 나중에 다룰 예정입니다~
U
Ukdong Son 2016.10.31
@Ian H,
감사합니다~ 우선 찾아 보겠습니다~ 강좌 기다려 보겠습니다~~~^^
K
Kyowon Im 2017.02.13
좋은 강좌 감사합니다.
z
zzolain 2017.07.13
안녕하세요. 강의 잘 보고 있습니다. 음.. 제 경우 게시판 글 작성 시간에 문제가 있어서 질문 드려요. Local에서 확인할 경우 작성 시간이 현재 시간에 맞춰 확인이 되는데, 이를 heroku에 업로드해 확인을 하면 작성 시간이 12시간 정도 차이나는 시간으로 표시가 됩니다. (DB는 mlab.com MONGO DB 사용 중입니다.) 원인이 무엇일까요..? 서버 시간 기준으로 불러오는 원리인가요?
z
zzolain 2017.07.13
음.. 잘은 모르겠지만 heroku 서버에 업로드를 해 놓으니 UTC 기준으로 getHours가 작동 하나 보군요.. function getTime(dateObj) {   if (dateObj instanceof Date)   return get2digits(dateObj.getHours()) 부분을  return get2digits(dateObj.getHours()+9) 로 바꿈으로써 임시 조치는 하였는데 혹시 더 좋은 방법이 있을까요?
I
Ian H 2017.07.13
@zzolain,
맞습니다. 서버 기준으로 시간이 표시됩니다.
dateObj.getHours()+9를하면 20시는 29시가 되버리기 때문에 좋지 않구요,
var offset = 9;
function getDate(dateObj){  if(dateObj instanceof Date){   var date = new Date(dateObj.getTime()+(3600000*offset));   return date.getFullYear() + "-" + get2digits(date.getMonth()+1)+ "-" + get2digits(date.getDate());  } }
function getTime(dateObj){  if(dateObj instanceof Date){   var date = new Date(dateObj.getTime()+(3600000*offset));   return get2digits(date.getHours()) + ":" + get2digits(date.getMinutes())+ ":" + get2digits(date.getSeconds());  } }
이렇게 고치시면 되겠네요. 물론 offset은 heroku의 환경변수로 설정해서 불러오면 더욱 좋구요^^
z
zzolain 2017.07.14
@Ian H,
앗 답변 감사드립니다~ 아이구.. 29시가 될거라는 생각을 못했네요..!! 아 getHours 메소드에서 getTime메소드로 변경하셨는데, 혹시 ms 단위로 데이터를 가져오기 위함이신가요? 그후 3600000*offset값을 추가해서 다시 getHours메소드를 이용해 24시 형태로 계산할 수 있도록? 입문자라 질문이 많습니다. 죄송합니다. ㅠ.ㅠ
I
Ian H 2017.07.15
@zzolain,
네 맞습니다^^ 질문 하시라고 댓글 기능을 만든 거니까, 질문 많이 하셔도 됩니다. 그래야 빨리 배우시죠~
박수진 2017.09.30
안녕하세요 강의보고 민스택 공부하고있는 학생입니다 소스코드 중 toObject:{virtuals:true} 이부분에 대해  설명을 봐도 무슨 말인지 잘 이해가 가지 않는데요 ㅠㅠ 좀더 풀어서 설명 해주실 수 있나요?  function get2digits(num){  return ("0" + num).slice(-2); } 그리고 문자0을 num과 더해주는거랑 slice를 써서-2 하는 이유가 궁금합니다 다른분들보다 이해도가 많이 낮습니다 ㅠㅠ
I
Ian H 2017.10.02
@박수진,
안녕하세요 반갑습니다.
toObject항목은 현재 schema(위 글에서는 postSchema)를 데이터로서 사용하게 될 때 사용될 옵션들을 넣는 부분입니다. virtual은 실제 db상에는 없는 항목들이잖아요? toObject:{virtuals:true}를 해 주지 않으면 view에서 virtual 항목들을 사용하려 하면 없는 항목으로 간주됩니다.
get2digits는 그냥 숫자의 형태를 마춰주기 위한 함수로, 시간을 항상 2자리로 표시하기 위해 만든 함수입니다. 2자리인 숫자를 넣으면 그냥 그대로 나오고 한자리인 숫자를 넣으면 앞에 0이 붙어서 나오게 됩니다.
또 궁금하신 점 있으면 질문해 주세요^^
박수진 2017.10.03
@Ian H,
답변 너무 감사드립니다! 블로그 글이 공부에 너무 도움이 많이 되고있습니다 추석연휴 잘 보내시기 바랍니다!
김민준 (alswns) 2017.11.05
강의 잘 보았습니다! 감사합니다! 많은 도움이 되었습니다!
I
Ian H 2017.11.07
@김민준 (alswns),
감사합니다^^
박재성 2018.05.04
안녕하세요 한가지 여쭤볼게 있어서요~ show 할때 전체 글 목록도 보여지게 하고싶은데요 그럴땐 어떻게 사용하면되나요 ??
I
Ian H 2018.05.04
@박재성,
안녕하세요! show.ejs 파일에 index.ejs의 코드 중 글 목록을 보이게 하는 코드를 복사해서 넣으면 되겠습니다!
박재성 2018.05.11
@Ian H,
라우터 posts.js에서 show.ejs를 부를때 findOne해서 해당 글 하나만 가져가잖아요 ? 그런데 index.ejs에서 목록을 뿌려주는건 find로 모두 다 들고가는데 결국엔 show.ejs로 갈때 findeOne 한거랑 find한거랑 두개 다 가져가야하는데 이럴땐 어떻게 해야하는지가 궁금합니다!
I
Ian H 2018.05.14
@박재성,
async 라이브러리를 사용하면 여러개의 async 함수를 처리할 수 있습니다. async 라이브러리 링크: https://github.com/caolan/async
stackoverflow에서 유사한 질문을 찾았는데요, https://stackoverflow.com/questions/6180896/how-to-return-mongoose-results-from-the-find-method
질문자는 user랑 article을 찾아서 한 페이지에 표시하고 싶어 합니다. 답변을 보면, 
async.parallel([    function(cb){       users.find({}, cb);    },    function(cb){       articles.find({}, cb);    } ], function(results){    // results contains both users and articles });
이런식으로, 배열에 할일들을 넣고, 마지막 function에 모든 작업이 끝난 경우 할일을 작성합니다. 
우리 예제에서는 Post.findOne, Post.find 를 써서 검색된 값을 변수(var post; var posts;)로 저장해 뒀다가, 마지막 function에     res.render("posts/show", {post:post, posts:posts}); 를 넣으면 되겠습니다!
-
-1 2019.01.27
exec 메소드로 데이터를 가져오려면,,, 정규식 reg 가 필요한걸로 알고 있는데요....정규식은 생략 가능한가요??
I
Ian H 2019.01.28
@-1,
exec메소드는 callback함수를 인자로 가집니다. 정규식이 들어가는 예제를 알려주실 수 있으신가요?
J
JS Park 2019.01.30
안녕하세요~ 블로그 잘 보고 있습니다. 게시글과 전혀 상관없는 질문이라 답변 해주실지는 모르겠으나.. 현재 운영중인 블로그 db 뭐 쓰시는지, tier 및 요금은 어떤거 쓰시는지 알 수 있을까요..?ㅎㅎㅎ
I
Ian H 2019.01.30
@JS Park,
안녕하세요 이메일 주소를 알려주시거나, [email protected]로 이메일 보내주시면 답변드릴게요^^
J
JS Park 2019.01.31
아 네 감사합니다! 메일 보내드렸습니다~ 
I
Ian H 2019.02.04
@JS Park,
답변드렸어요 답변이 늦어서 죄송합니다!
댓글쓰기

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

UP