게시판 - Back End 개발

소스코드

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

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

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

git reset --hard
git pull
git reset --hard 79ba957
git reset --soft 1cd61b1
npm install
atom .

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

git clone https://github.com/a-mean-blogger/board.git
cd board
git reset --hard 79ba957
git reset --soft 1cd61b1
npm install
atom .

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


이제 게시판 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, required:true},
  createdAt:{type:Date, default:Date.now}, // 2
  updatedAt:{type:Date},
});

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

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

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

// 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와 형태가 거의 같습니다. 앞으로도 CRUD 기능을 만들게 된다면 항상 거의 이 형태를 사용하게 됩니다.

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. post를 수정하는 경우 수정된 날짜를 updateAt에 기록합니다.

// index.js

...

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

...

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

실행결과

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

마치며...

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

댓글

박우림 2020.03.20
좋은 글 정말 감사합니다!! 여쭙고싶은게 있습니다!ㅎㅎ
index.js에서 라우터 설정을 보면 // Routes app.use('/', require('./routes/home')); app.use('/posts', require('./routes/posts'));
와 같이 "/", "/posts" 두개의 경로만 지정해줬는데  url에서 /about의 경로로 들어가면 views/home/about.ejs가 열리는 이유가 잘 이해되질 않네요 ㅜ
routes/home.js에서 /about의 경로를 설정해주긴했으나, 메인 js 파일인 index.js에서 /about 경로를 설정하지 않았는데도 이동되는게 납득이 안됩니다 ㅜ
I
Ian H 2020.03.20
@박우림,
app.use(express.static(__dirname+'/public'));를 통해 /public폴더가 static 폴더로 설정되었기 때문입니다.  https://www.a-mean-blog.com/ko/blog/Node-JS-첫걸음/Hello-World/Static-폴더-추가하기 글을 다시 한번 읽어주세요. 감사합니다^^
박우림 2020.03.25
@Ian H,
친절한 설명 감사드립니다.ㅎㅎ 말씀하신 게시글을 읽어보고 왔는데, 죄송하지만 저의 궁금증은 해소되질 않았습니다 ㅜ 한 시간 정도 혼자 고민해봤는데도 납득이 안되네요. 번거롭게 해드려 죄송합니다.
static 폴더로 지정하였기에 "/css" 경로로 들어갈 경우 "/public/css"로 들어가지는것은 이해가 잘 됩니다. 근데 그러하다면, "/"의 경로로 들어갈 때 "/public"의 경로와 "./routes/home"의 경로 두 개가 충돌해야 맞지 않나 싶기도합니다. 억지로 받아들여서 "/public"은 서버에서 호출했을 때의 경로, "./routes/home"은 클라이언트에서 호출했을 때의 경로라고 생각해보기도 했습니다.
이렇게 받아들였을 때 "/"로 링크 이동을 하면 "./routes/home"으로 이동하여 home.js 파일에서 "/"의 경로를 탐색해 "views/home/welcome"으로 최종 이동하는 것까지 이해됐습니다. 근데 문제는 "/about"으로 링크 이동을 하면 왜 "./routes/home"으로 이동되는지가 이해가 안됩니다. 애초에 "/"가 입력될 때 "./routes/home"으로 이동하도록 설정하였기에 "./routes/home"으로 이동한 후에 home.js 파일에서 "views/home/about"으로 최종 이동하는 것이라고 한다면, 추가로 궁금한 점이 생깁니다.
만약 index.js에서 app.use('/posts', require('./routes/posts')); app.use('/users', require('./routes/users')); 와 같은 소스를 추가하지 않는다면, "/posts"나 "/users"로 링크 이동을 하면 "./routes/home"으로 이동한 후에 home.js 파일에서 "/posts"와 "/users"의 경로를 추가 탐색하게 되는건가요? 이럴 경우 home.js에 "/posts"와 "/users"의 경로가 명시되어 있지 않으니 에러가 뜨게 되겠구요. 
제발 제가 생각한게 맞았으면 좋겠네요😢 다시 한번 좋은 강의 감사드리고, 정말 많이 배우고 있습니다. 감사합니다!
I
Ian H 2020.03.25
@박우림,
앗. 이전댓글에서 제가 잘못 설명드렸네요. about 페이지 내용이 static에 가깝다 보니 전 about 페이지가 public 폴더에 있는줄 알고 about 페이지가 보이는 이유가 static 설정 때문이라고 답글드렸어요. 죄송합니다!
정정합니다. about페이지는 static 설정이랑은 상관없구요 다시 설명드릴게요.
app.use('/', require('./routes/home')); 부분때문에 '/'으로 시작하는 route은 일단 무조건 ./route/home.js 파일로 전달이 됩니다. 두번째 댓글에서 생각하신 부분이 맞습니다. 위 코드에서 /users를 입력한다면 home.js에서 users route을 찾게 됩니다. 
home.js파일을 보면 '/'과 '/about' route에 대한 설정이 있죠 그렇기 때문에 /about은 home.js의 /about 라우트로 연결이 되는 겁니다.
몇가지 추가로 정리해 드릴게요. 1. ["/public"은 서버에서 호출했을 때의 경로, "./routes/home"은 클라이언트에서 호출했을 때의 경로] 라고 가정하신 부분은 잘못되었습니다. route을 통한 모든 경로탐색은 클라이언트가 서버로 request를 한 경우에 해당합니다. 2가지로 나누어 지지 않습니다.
2. request가 전달되면 index.js의 코드를 위에어 아래로 순서대로 진행합니다. 아래는 index.js에서 route에 관련된 코드부분입니다.
// Other settings ... app.use(express.static(__dirname+'/public')); ...
// Routes app.use('/', require('./routes/home')); app.use('/posts', require('./routes/posts'));
즉 어떠한 url을 받더라도 우선 static에 해당 url에 맞는 파일이 있는지를 먼저 살피고, 그다음에 route이 '/'로 시작한다면 home.js에 해당 url에 대응하는 route이 있는지 살핀 후 route이 '/posts'로 시작한다면 posts.js에서 route을 찾습니다.
그러므로 동일한 route이 한번 이상 설정된 경우 위의 흐름에 따라 가장 먼저 발견되는 route만 실행됩니다! 그러므로 route을 설정할때는 순서를 잘 생각하여 코드를 작성해야 합니다.
이해가 잘 되셨는지 모르겠어요 ㅠㅠ 이해 안되시면 또 댓글 남겨주세요!
a
ams 2020.07.15
실무에서 작업하실때도 백엔드 -> 프론트 엔드 순으로 하는게 효율적인가요?
I
Ian H 2020.07.15
@ams,
위 강의 내용의 코드 정도는 내용이 간단하기 때문에 백엔드를 완전히 만든 후 프론트엔드를 만들지만,  실무에서는 백엔드와 프론트엔드를 번갈아가며 동시에 개발하게 됩니다. 백엔드에서 한가지를 기능을 만든후 그 기능을 수행하는 프론트엔드를 만들고 테스트, 그리고 다시 다음 기능으로 넘어가죠.
가정교사 2020.10.05
감사합니다. 정말 도움이 많이 되고 있습니다.^^
I
Ian H 2020.10.06
@가정교사,
반갑습니다^^ 도움이 되었다니 기쁩니다!
J
J J 2020.10.08
백엔드 게시판 짜는 도중에 Create 와 Read 는 되는데 Update와 Delete가 안되는 경우는  경우의 수를 뭐로 보시는가요?
J
J J 2020.10.08
404 때문에 이틀간 고통받고 있어서 말이죠.
I
Ian H 2020.10.08
@J J,
혹시 method-override 를 사용하지 않으시고 HTML의 form에서 PUT, DELETE을 바로 보내시나요? (https://www.a-mean-blog.com/ko/blog/Node-JS-첫걸음/주소록-만들기/주소록-Show-Edit-Update-Destroy 를 참고하세요)
J
J J 2020.10.14
mongodb 와 ejs로 작업하고 있는데  전부다 router.update, router.delete 지원안해서  /post/delete/<%=post.id%>  하고 /post/update/<%=post.id%> 이 2개의 폼을 전부다 주소로만 구분하고 전부 post로 싸줘서 해결했네요.  요새 router.update, router.delete 를 ejs에서는 잘 인식 못하나 봐요.  일주일 넘게 삽질ㅠㅠ
J
J J 2020.10.14
method_override 했는데 잘 안되서 속썩이다가 console.log찍어도 죄다 post로만 넘어오니 관두고 gg 쳤습니다.
I
Ian H 2020.10.14
@J J,
Method override 안 되는 경우는 처음인데요, 혹시 문제가 발생하는 코드를 github에 올려주시면 제가 한번 확인해 보겠습니다!
박형주 2020.10.10
좋은 강의 제공해 주셔서 감사합니다. 따라하다 에러를 만났는데요. 이유를 모르겠네요. res.json(err)를 통해 크롬 화면에 나온 에러는 다음과 같습니다. 
{ errors: { body: { name: "ValidatorError", message: "Path `body` is required.", properties: { message: "Path `body` is required.", type: "required", path: "body", }, kind: "required", path: "body", }, title: { name: "ValidatorError", message: "Path `title` is required.", properties: { message: "Path `title` is required.", type: "required", path: "title", }, kind: "required", path: "title", }, }, _message: "Post validation failed", message: "Post validation failed: body: Path `body` is required., title: Path `title` is required.", }
models/posts 에서 정의한 모델은 아래와 같습니다. 
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({   title: {     type: String,     required: true,   },   body: {     type: String,     required: true,   },   createdAt: {     type: Date,     default: Date.now,   },   updateAt: {     type: Date,   }, });
module.exports = mongoose.model('Post', postSchema);
초보자라 한땀 한땀 찾으면서 공부하는데, node가 참 어렵네요… 답변 미리 감사합니다.
I
Ian H 2020.10.12
@박형주,
안녕하세요, html form의 body와 title이 서버로 전달이 안되고 있는 것으로 보입니다. 전체 코드를 봐야 원인을 알 수 있는데, github에 올리시면 봐드릴게요^^
w
whale 2020.10.11
처음부터 쭉 이해하면서 따라하고 있는데 지금 구조를 REST API로 정리해보려 하는데 단순히 GET은 show POST는 new라고 해야 하나요? 리다이렉팅 형식으로 되어있어서 헷갈리네요ㅠㅠ
I
Ian H 2020.10.12
@whale,
REST API로 만드실 때는 https://www.a-mean-blog.com/ko/blog/Node-JS-첫걸음/주소록-만들기/CRUD와-7-Standard-Actions 에서 form을 보여주는 new와 edit만 지우시고 나머지 create, update, index, show, destroy 만 API로 만드시면 됩니다!
w
whale 2020.10.12
@Ian H,
답변 감사드립니다!! 주소록 만들기에 이해가 잘되게 정리가 엄청 잘되어 있었네요 감사합니다!!!
I
Ian H 2020.10.12
@whale,
질문있으시면 언제든지 남겨주세요^^ 감사합니다
어윽 2020.12.07
안녕하세요. 선생님. 정말 좋은 강의 감사드립니다. 한 가지 문제점이 있어서 질문 드립니다. 삭제 버튼을 눌렀을 때, (Do you want to delete this?에서도 확인을 누른 후) 리다이렉트(?)이 http://localhost:3000/post 으로 가버립니다. (삭제 기능은 잘 작동함) 리다이렉트가 http://localhost:3000/posts으로 가야할 것 같은데 어느 부분을 수정해야 할지 궁금합니다.
I
Ian H 2020.12.07
@어윽,
안녕하세요^^  routes/posts.js 파일의 //destroy 부분에서 res.redirect('/posts');이 /post로 오타가 있는 듯합니다. 확인해보세요^^
어윽 2020.12.08
@Ian H,
감사합니다.
M
Michael Jackson 2021.02.28
var Post = mongoose.model('post', postSchema); module.exports = Post; 
코드의 경우 postSchema를 'post'라는 이름으로 모델(https://mongoosejs.com/docs/guide.html)로 만들어서 export시키면 어디서든 require할시 사용 가능하다는건 이해 하겠는데,   왜 post는 소문자이고 var Post와 require는 대문자인가요  대체 뭐가뭔지 정확히 감은 안잡히네요.  아 그리고 몽고db 기본 링크(https://www.a-mean-blog.com/ko/blog/Node-JS-%EC%B2%AB%EA%B1%B8%EC%9D%8C/%EC%A3%BC%EC%86%8C%EB%A1%9D-%EB%A7%8C%EB%93%A4%EA%B8%B0/mlab-com%EA%B0%80%EC%9E%85-%EB%B0%8F-%EC%98%A8%EB%9D%BC%EC%9D%B8-Mongo-DB-%EC%83%9D%EC%84%B1)는 수록 해주시겠어요?
M
Michael Jackson 2021.02.28
그리고 라우터파일의 끝부부마다 module.exports = router; 이게 있는 이유가 무엇인가요?
I
Ian H 2021.02.28
@Michael Jackson,
Post는 모델이구요, post는 하나의 포스트 인스턴트입니다. 모델은 인스턴스를 찍어내는 붕어빵 틀이고, 인스턴트는 모델에서 만들어진 하나의 데이터이죠. 
module.exports는 module를 만들기 위해 사용되는 코드인데요, https://www.a-mean-blog.com/ko/blog/Node-JS-첫걸음/주소록-만들기/Node-js-Module 에 설명이 있습니다. module.exports에 담겨진 변수를 다른 js파일에서 사용할 수 있도록 하는 역할을 합니다.
몽고 db의 기본링크는 따로 있는게 아니고 게시물의 내용에 따라 db 생성하시면 db 링크가 생성됩니다^^
M
Michael Jackson 2021.02.28
@Ian H,
몽고DB 오피셜 문서 왈 mongoose.model('post',xxx)는 post가 posts로 바뀌어서 저장된다고 하더라고요.   근데 포스트 인스턴트가 post이긴한데 다른 스크립트 파일에서 Post말고 post를 사용하는 부분이 없어서 아직도 모르겠어요.
I
Ian H 2021.03.01
@Michael Jackson,
mongoose.model('post',xxx) 하시면 mongo db에 posts라는 이름의 collection이 생성됩니다.
제가 말한 인스턴트 post는 'post'가 아니라,
routes/posts.js의
// 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});  });  }); 와 같이 route의 콜백 함수의 function(err, post){에 있는 post를 말한거예요.
M
Michael Jackson 2021.02.28
Post.find({}) .sort('-createdAt').exec(function(err, posts){ 
실행순서가 find sort exec로 계속 넘겨진다는건 알겠습니다만... 이런거 부르는 용어를 자바스크립트에서 뭐라 하는지 모르겠습니다.
I
Ian H 2021.02.28
@Michael Jackson,
저도 딱히 뭐라고 부르는지는 몰랐는데 찾아보니 이름이 있긴 하네요.
Method chaining 이라고 하는 듯해요. 출처: https://medium.com/backticks-tildes/understanding-method-chaining-in-javascript-647a9004bd4f
댓글쓰기

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

UP