구축한 express 서버에서 데이터 CRUD를 구현하기 위해 필요한 내용들과 구현 내용을 정리해보겠다!
1. methodOverride
REST API가 무엇인지 간단한 설명 포스트를 작성했었다.
RESTful API
🌱 REST란 무엇일까? REpresentational State Transfer의 약자로, "architectural style for distributed hypermedia systems"라고 한다. 말이 어렵다. 클라이언트와 서버가 resource를 가지고 어떻게 CRUD operat..
buchu-doodle.tistory.com
이 글에서 REST가 무엇인지, 그리고 URI 설계를 어떻게 하면 좋을지 깔끔하게 정리해놨으니 보는 것을 추천한다!
REST API 제대로 알고 사용하기 : NHN Cloud Meetup
REST API 제대로 알고 사용하기
meetup.toast.com
REST란 다시 한 번 간단하게 정리하자면 리소스를 교환하기 위한 verb와 uri에 관한 규약이다. URI에는 자원의 리소스만 가독성이 좋은 형태로 남겨두고, get/post/put/delete의 verb를 통해 클라이언트와 서버가 통신할 수 있도록 한다.
그러나 기본 HTML에서는 주로 클라이언트가 서버로부터 응답을 받는 get, 서버에게 데이터를 보내는 post 이외의 verb는 지원하지 않는다. 물론 post를 가지고 put(수정)과 delete(삭제)를 구현할 수는 있지만 이는 RESTful하지 않다. 자원에 대한 행위를 나눠놓은 의미가 없기 때문이다. HTML에서 put, delete를 지원하기 위해 사용하는 모듈이 method override이다.
method-override
Override HTTP verbs. Latest version: 3.0.0, last published: 4 years ago. Start using method-override in your project by running `npm i method-override`. There are 1367 other projects in the npm registry using method-override.
www.npmjs.com
프로젝트 루트 디렉토리에 npm install 해주자.
$ npm install method-override
게시글에 rest 구현을 위한 구체적인 개념(URL과 verb 구현)이 정리되어 있으니 여기서 반복하진 않겠다.
유저의 정보 수정을 위한 HTML form을 구성할 때 form에 붙은 attribute는 다음과 같이 설정한다. action값에 _method=PUT이라는 쿼리스트링을 하나 붙이는 것이다.
<form
action="/user/<%=currentUser._id%>/edit?_method=PUT"
method="post"
class="needs-validation"
novalidate
>
_method query 스트링은 app.js의 이 부분이 받고
// app.js
const methodOverride = require("method-override");
app.use(methodOverride("_method"));
user routes의 router.put이 verb에 맞는 controller 미들웨어를 호출한다. 위의 form이 submit되면 아래 코드들이 실행되는 것이다.
// routes/user.js
router.put(
"/:id/edit",
isLoggedIn,
userController.validateEditUser,
userController.editUser
);
2. multer
이전 포스트에서 app.use(express.urlencoded({extended:true}))설명을 하며 body parser에 관해 다룬적이 있다.
아래 블로그에 HTML의 MIME타입중 하나인 multipart/form-data가 어떤 것인지, 등장 배경이 무엇인지, 간단한 코드의 예제까지 있으니 확인하는 것을 추천한다.
HTTP multipart/form-data 란?
프로젝트를 진행하면서 프론트 -> 백엔드로 이미지를 전송하는 경우가 있었다.오늘은 HTTP, multipart, multipart/form-data 세 가지 키워드에 대해 알아보고, 그 중에서 중요한 개념중에 하나인 multipart/for
velog.io
역시 한마디로 정리하자면, post method로 들어오는 사용자의 여러가지 content type을 한꺼번에 처리하기 위한 인코딩 타입이라고 보면 된다.
body-parser은 application/x-www-form-urlencoded 타입을 req.body에 오브젝트 형식으로 넣어준다고 했지만, 만약 데이터 인코딩 타입이 multipart/form-data라면 client에게 받은 폼 데이터를 파싱하지 못한다. (req.body를 콘솔에 찍어보면 빈 object가 찍힌다) 프로젝트에선 유저로부터 form을 통해 음식 정보 입력을 받을 때 enctype="multipart/form-data"를 입력하여 폼 데이터 인코딩 타입을 바꿨다. 현재 프로젝트 완성본에서는 파일 업로드를 따로 하지 않으므로 필요 없지만 공부차원에서 + 나중에 사진이 필요해질 날이 올지도 모르기 때문에..
<form
action="/food/new"
method="post"
enctype="multipart/form-data"
class="needs-validation"
novalidate
>
req.body에 유저의 폼 데이터가 찍히지 않으면 어떻게 해야하냐? multer라는 npm 모듈을 사용하면 된다.
multer
Middleware for handling `multipart/form-data`.. Latest version: 1.4.5-lts.1, last published: 2 months ago. Start using multer in your project by running `npm i multer`. There are 3344 other projects in the npm registry using multer.
www.npmjs.com
express의 body-parser은 유저의 application/x-www-form-urlencoded데이터(text로 이뤄짐)를 req.body에 key-value 형태로 저장한다. multer는 multipart/form-data를 req.body와 req.files(혹은 req.file)에 나눠 저장한다.
HTML에서 file input은 다음과 같은 태그로 받는다.
<input type="file" name="example-file" />
form 태그의 action이 "/", method가 post라고 할 때, example-file이라는 name을 갖는 file을 받기 위한 javascript 코드는 아래와 같다. multer의 공식문서 코드를 가져왔다.
const express = require('express')
const multer = require('multer')
const upload = multer({ dest: 'uploads/' })
app.post('/', upload.single('example-file'), function (req, res, next) {
// req.file is the `avatar` file
// req.body will hold the text fields, if there were any
})
multipart/form-data를 나눠 저장한다는 것이 이런 의미이다. multer option의 dest를 uploads/로 두었으므로 유저가 input으로 넣은 파일 입력값은 프로젝트 루트 디렉토리의 upload 폴더에 저장된다. 폼 데이터가 서버에 도착한 후 uploa.single() 미들웨어를 거치면 req.file에는 유저가 업로드한 파일의 fieldname, originalname, path, size 등 file에 관한 정보가 들어있는 object가 들어있고 req.body에는 기존 urlencoded 방식과 같이 name-value값이 key-value 형태로 존재한다.
추가로, example-file을 받는 input tag에서 여러개의 파일을 받고싶다면 multiple 옵션을 추가한 뒤 upload.single 대신 upload.array('example-file')로 받는다. 그럼 오브젝트들이 req.files에 array 형태로 존재하게 된다.
multer 객체에 옵션으로 줄 수 있는 요소는 dest(파일을 저장할 디렉토리), fileFilter(accept할 파일의 종류), limits(accept할 파일의 개수) 정도이고 사실 실제로 사용하는 것은 dest정도가 전부이다. 그러나 input file을 조금 더 섬세하게 다루거나 이미지나 영상 등 크기가 큰 input을 다른 외부 서버로 옮기는 작업 등을 하기 위해 storage라는 객체 자체를 multer의 storage 옵션으로 줄 수 있다. (보통 서드파티 라이브러리에서 제공한 storage를 이용한다) 자세한 사항은 공식문서를 참고하자.. 사실 프로젝트에서 실제로 file 관련한걸 다루지 않아서 프로젝트 코드 예시를 따로 들 수가 없어 설명에 한계가 있다.
대신 multer을 이용한 multiplart/form-data 파일 업로드에 참고하면 매우매우 유용할 블로그 링크를 하나 첨부한다.(구글링만세)
단편강좌: Node.JS - Multer로 파일 업로드 - A MEAN Blog
Node.JS Express 서버로 파일을 업로드 하는 방법을 알아봅시다. Multer라는 package를 사용합니다. 이 강의는 Node.JS/Express로 기본적인 웹사이트를 만들 수 있는 분들을 대상으로 합니다. (최소 MEAN Stack
www.a-mean-blog.com
3. methods (static, instance) + virtuals
mongoose 모델에는 특정 기능을 수행하는 method를 추가할 수 있다. java에서 instance/static method와 마찬가지로 모델 자체에 instance / stataic method를 붙일 수 있다.
mongoose schema에서 static method란, 해당 모델 자체에 적용되는 기능을 가진 함수를 말한다. food 모델 자체에 근데 static method를 쓰는 예시를 많이 보진 못했다. 아무래도 스키마 자체 query에 대부분의 기능이 들어가있다보니..
반대로 instance method란 무조건! 특정 모델 객체를 하나 생성한 후에 이용할 수 있는 함수를 말한다. instance variable을 이용해서 기능을 구현한 method의 경우 대부분 instance method이다.
내가 food 모델의 field값을 음식의 이름 / 탄수화물 / 단백질 / 지방의 양 으로 설정했다고 가정해보자. 이 정보를 토대로 총 칼로리양을 모델에 저장해야한다.(사실 칼로리 필드를 따로 추가하는게 와방이긴 하지만 예시니까!)
// foodmodel.js
const mongoose = require("mongoose");
const { Schema } = mongoose;
const foodSchema = new Schema({ name: String, carbs: Number, protein: Number, fat: Number});
방법은 두가지가 있다. 하나는 virtual type을 추가하는 것이고, 하나는 instance method를 이용하는 것이다.
virtual type은 따로 parameter을 받을 필요 없이, 기존 모델의 필드값을 가지고 구해지는 무언가가 있을 때 유용하다.
foodSchema.virtual("calories").get(function () {
return (this.carbs + this.protein) * 4 + this.fat * 9;
});
이제 새로운 food 객체를 선언하고 .calories를 확인하면 해당 음식의 칼로리를 확인할 수 있다. 프로젝트에서 유저의 생년월일을 바탕으로 나이를 구하는 과정, 키/나이/몸무게를 토대로 BMI와 기초대사량을 구하는 과정 등에서 virtual type을 이용했다.
instance method는 짧게 설명했듯 생성한 model instance와 관련한 method를
만약 저 위의 탄단지 양이 100g을 기준으로 한 양이라고 생각해보자. 그런데 나는 200g을 먹고싶다. 200g의 탄단지 양과 칼로리를 알고싶다 한다면? 원하는 양을 입력받아 그를 토대로 영양소 계산을 해주는 Method를 작성하면 된다.
foodSchema.methods.getNuts = function (amount) {
let [carbs, protein, fat] = [0, 0, 0, 0];
carbs = Math.round(this.carbs * amount) / 100;
protein = Math.round(this.protein * amount) / 100;
fat = Math.round(this.fat * amount) / 100;
return { carbs, protein, fat };
};
이제 food 객체를 하나 생성하고 food.getNuts(200)을 하면 원하는 탄단지 양이 반환된다.
이런식으로.. mongoose model에 원하는 method나 type을 추가할 수 있다. 더 필요한 내용이 있으면 또한! 공식 문서를 참고하자.
Mongoose v6.4.6: API docs
mongoosejs.com
4. 필요한 내용 구현
CRUD 내용 자체는 지겹도록 했으니 페이징과 양 수정에 관한 내용만 다루겠다!
일단 음식의 양 수정을 위해 food show page에 유저로부터 원하는 음식 양을 받는 form을 작성했다.
<form action="/food/<%=food._id%>/anotherserving" method="post">
<div class="d-flex">
<div class="input-group input-group-sm">
<input
type="text"
class="form-control"
id="amount"
name="amount"
value="<%=food.amount%>"
aria-describedby="amountperservs"
/>
<span class="input-group-text" id="amountperservs">
<%=food.serving%>
</span>
<button type="submit" class="btn btn-outline-secondary btn-sm">
변경
</button>
</div>
</div>
</form>
food.serving은 g,회,개 중 하나이다. g은 100g기준, 회와 개는 1회와 1개 기준이다.
amount 값으로 유저가 원하는 값을 받을 수 있도록 했다.
// routes/foods.js
const foodController = require("../controllers/food");
router.get("/:id", catchAsync(foodController.showFood));
router.post(
"/:id/anotherserving",
catchAsync(foodController.getAnotherServing)
);
저 폼을 받는 라우터 코드다.
// controllers/food.js
module.exports.getAnotherServing = async (req, res, next) => {
const food = await Food.findById(req.params.id);
const n_nuts = food.getNuts(req.body.amount);
food.calories = n_nuts.calories;
food.carbs = n_nuts.carbs;
food.sugar = n_nuts.sugar;
food.protein = n_nuts.protein;
food.fat = n_nuts.fat;
food.saturated = n_nuts.saturated;
food.amount = req.body.amount;
req.session.anotherfood = { ...food };
res.redirect(`/food/${food._id}`);
};
라우터 코드를 구현하기 위한 controller 코드는 위와 같다. 세션의 anotherfood 필드를 추가하여 food의 수정된 객체를 spread해서 넣어줬다. 앗! 근데 food 객체의 값을 저렇게 막 바꿔도 되는거냐고? 당연하다. food.save()를 하지 않는이상 DB에 저장된 food 값은 바뀌지 않는다. 근데 지금 생각해보니 또 저 코드들은 destructuring으로 가독성을 높일 수 있을 것 같다. 그건 나중에 하기로 하고...
세션에 넣은 수정된 food값을 show page에 반영하기 위해 컨트롤러의 showFood 미들웨어는 아래와 같이 작성되었다.
module.exports.showFood = async (req, res, next) => {
if (req.session.anotherfood) {
const food = { ...req.session.anotherfood._doc };
delete req.session.anotherfood;
res.render("foods/show", { food });
} else {
const food = await Food.findById(req.params.id);
res.render("foods/show", { food });
}
};
model instance 객체를 spread로 넣으면 ._doc을 통해 내부의 실질적인 instance 값이 받아진다. find로 찾아진 mongoose 객체는 단순한 object가 아니라 .save(), virtuals, getter/setter 등의 기능을 지원하는 mongoose document 형태이기 때문이다. 모델 객채의 값 자체를 받아오기 위해선 위의 코드처럼 ._doc으로 따로 빼오거나 find 후에 객체의 값만을 빼오는 추가 조치가 필요하다.
https://velog.io/@modolee/mongodb-document-to-javascript-object
MongoDB - Document를 JavaScript Object로 변환하기
MongoDB + Mongoose를 이용해 API 개발 시 Mongoose Document를 Plain Old JavaScript Object로 변환하는 방법을 소개합니다.
velog.io
위 포스트를 참조해서 다음엔 toObject와 lean method를 이용하도록 하자.
그리고 페이징 구현이다!

이런식으로 음식의 목록을 보여주고, 앞뒤에 표시할 추가 음식목록이 있으면 화살표를 활성화하는 기능을 구현했다.
이를 위해 page값을 query string으로 받고, Mongoose의 limit, skip기능을 사용했다.
// controllers/food.js
module.exports.renderFood = async (req, res, next) => {
const page = req.query.page ? parseInt(req.query.page) : 1;
const foods = await Food.find({})
.skip(15 * (page - 1))
.limit(16);
let end = false;
if (foods.length < 16) {
end = true;
} else {
foods.pop();
}
res.render("foods/index", { foods, page, foodname: null, end });
};
module.exports.searchedFood = async (req, res, next) => {
const page = req.query.page ? parseInt(req.query.page) : 1;
const { foodname } = req.params;
let end = false;
const foods = await Food.find({
name: { $regex: foodname, $options: "i" },
})
.skip(15 * (page - 1))
.limit(16);
if (foods.length < 16) {
end = true;
} else {
foods.pop();
}
res.render("foods/index", { foods, page, foodname, end });
};
코드를 보면 알겠지만 .skip(number)은 find로 찾아진 결과물들 중 number만큼 앞에서 자르겠다는 뜻이고, .limit(number)은 찾은 결과중 number 개수만큼의 결과만 가져오겠다는 의미이다.
food index의 한 페이지에 15개의 음식 정보를 렌더링하기로 했다. 페이지 번호에 따라 .skip method를 이용하여 앞에서부터 15개씩 삭제하면서 페이징을 구성했는데, 만약 해당 페이지에 표시할 음식이 15개 이하라면 다음 페이지로 가는 버튼을 비활성화 시켜야 한다. 이 정보를 전달하기 위해 end라는 boolean값을 ejs파일에 전달했다. end값을 결정하는 기준은 .llimit(16)으로 해당 페이지에 보여줄 15개의 음식값과 다음 페이지의 존재 여부를 결정할 마지막 1개의 음식이다.
따로 mongoose+express 환경에서 페이징을 조금 더 가독성 높고 스마트하게 구현할 수 있는 것 같은데.. 페이징 구현이 메인이 아니라 일단 어떻게든 돌아가게 되는 형태로 구현했다.
searchedFood는 사용자가 음식사전의 검색란에 무언가를 검색했을 때의 처리이다. 그냥 $ regex로 검색한 food string이 존재하는 음식 전체를 불러왔다.
search 쿼리의 동작은 mongoDB의 공식문서를 참고하자.
https://www.mongodb.com/docs/manual/text-search/
Text Search — MongoDB Manual
Docs Home → MongoDB ManualMongoDB offers a full-text search solution, MongoDB Atlas Search, for data hosted on MongoDB Atlas. A legacy text search capability is available for users self-managing MongoDB deployments.For MongoDB Atlas users, MongoDB's Atla
www.mongodb.com
일단 $ regex를 사용해서 검색을 구현했는데, .find({$ text : { $ search : foodname } })를 통해 더 효율적으로 검색 코드를 짤 수 있을 것 같다. 이것도 나중에 수정하자...
다음과 이전 페이지로 가기 위한 화살표 버튼 html코드는 아래와 같다. (views/index.ejs)
<nav>
<ul class="pagination">
<li class="page-item <%=(page===1)?"disabled":""%>">
<a
class="page-link"
href="/food<%=(foodname)?"/search/"+foodname:""%>?page=<%=page-1%>"
aria-label="Previous"
>
<span aria-hidden="true">«</span>
</a>
</li>
<li class="page-item <%=(end)?"disabled":""%>">
<a
class="page-link"
href="/food<%=(foodname)?"/search/"+foodname:""%>?page=<%=page+1%>"
aria-label="Next"
>
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
무언가 검색했을 때만 foodname이 특정 값으로 설정됐다(아니면 null). 그에 따라 다음/이전 페이지 버튼을 눌렀을 때 href값이 다르고, 불려지는 미들웨어도 다르게 했다.
페이지가 1인 경우를 제외하면 이전 페이지는 모두 활성화시켰고, end가 false일 경우에만 다음 페이지 버튼을 활성화시켰다.
기본적인 food CRUD에 관한 내용은 생략하겠다. 그냥 form에서 name-value 키를 req.body로 받아와 new Food({...req.body})해서 .save(), findById, findByIdAndUpdate 등등을 실행한 것 뿐이니까.
다음 포스트는 식단관리의 꽃! 음식과 유저를 잇는 식단 모델을 다뤄보겠다!
구축한 express 서버에서 데이터 CRUD를 구현하기 위해 필요한 내용들과 구현 내용을 정리해보겠다!
1. methodOverride
REST API가 무엇인지 간단한 설명 포스트를 작성했었다.
RESTful API
🌱 REST란 무엇일까? REpresentational State Transfer의 약자로, "architectural style for distributed hypermedia systems"라고 한다. 말이 어렵다. 클라이언트와 서버가 resource를 가지고 어떻게 CRUD operat..
buchu-doodle.tistory.com
이 글에서 REST가 무엇인지, 그리고 URI 설계를 어떻게 하면 좋을지 깔끔하게 정리해놨으니 보는 것을 추천한다!
REST API 제대로 알고 사용하기 : NHN Cloud Meetup
REST API 제대로 알고 사용하기
meetup.toast.com
REST란 다시 한 번 간단하게 정리하자면 리소스를 교환하기 위한 verb와 uri에 관한 규약이다. URI에는 자원의 리소스만 가독성이 좋은 형태로 남겨두고, get/post/put/delete의 verb를 통해 클라이언트와 서버가 통신할 수 있도록 한다.
그러나 기본 HTML에서는 주로 클라이언트가 서버로부터 응답을 받는 get, 서버에게 데이터를 보내는 post 이외의 verb는 지원하지 않는다. 물론 post를 가지고 put(수정)과 delete(삭제)를 구현할 수는 있지만 이는 RESTful하지 않다. 자원에 대한 행위를 나눠놓은 의미가 없기 때문이다. HTML에서 put, delete를 지원하기 위해 사용하는 모듈이 method override이다.
method-override
Override HTTP verbs. Latest version: 3.0.0, last published: 4 years ago. Start using method-override in your project by running `npm i method-override`. There are 1367 other projects in the npm registry using method-override.
www.npmjs.com
프로젝트 루트 디렉토리에 npm install 해주자.
$ npm install method-override
게시글에 rest 구현을 위한 구체적인 개념(URL과 verb 구현)이 정리되어 있으니 여기서 반복하진 않겠다.
유저의 정보 수정을 위한 HTML form을 구성할 때 form에 붙은 attribute는 다음과 같이 설정한다. action값에 _method=PUT이라는 쿼리스트링을 하나 붙이는 것이다.
<form
action="/user/<%=currentUser._id%>/edit?_method=PUT"
method="post"
class="needs-validation"
novalidate
>
_method query 스트링은 app.js의 이 부분이 받고
// app.js
const methodOverride = require("method-override");
app.use(methodOverride("_method"));
user routes의 router.put이 verb에 맞는 controller 미들웨어를 호출한다. 위의 form이 submit되면 아래 코드들이 실행되는 것이다.
// routes/user.js
router.put(
"/:id/edit",
isLoggedIn,
userController.validateEditUser,
userController.editUser
);
2. multer
이전 포스트에서 app.use(express.urlencoded({extended:true}))설명을 하며 body parser에 관해 다룬적이 있다.
아래 블로그에 HTML의 MIME타입중 하나인 multipart/form-data가 어떤 것인지, 등장 배경이 무엇인지, 간단한 코드의 예제까지 있으니 확인하는 것을 추천한다.
HTTP multipart/form-data 란?
프로젝트를 진행하면서 프론트 -> 백엔드로 이미지를 전송하는 경우가 있었다.오늘은 HTTP, multipart, multipart/form-data 세 가지 키워드에 대해 알아보고, 그 중에서 중요한 개념중에 하나인 multipart/for
velog.io
역시 한마디로 정리하자면, post method로 들어오는 사용자의 여러가지 content type을 한꺼번에 처리하기 위한 인코딩 타입이라고 보면 된다.
body-parser은 application/x-www-form-urlencoded 타입을 req.body에 오브젝트 형식으로 넣어준다고 했지만, 만약 데이터 인코딩 타입이 multipart/form-data라면 client에게 받은 폼 데이터를 파싱하지 못한다. (req.body를 콘솔에 찍어보면 빈 object가 찍힌다) 프로젝트에선 유저로부터 form을 통해 음식 정보 입력을 받을 때 enctype="multipart/form-data"를 입력하여 폼 데이터 인코딩 타입을 바꿨다. 현재 프로젝트 완성본에서는 파일 업로드를 따로 하지 않으므로 필요 없지만 공부차원에서 + 나중에 사진이 필요해질 날이 올지도 모르기 때문에..
<form
action="/food/new"
method="post"
enctype="multipart/form-data"
class="needs-validation"
novalidate
>
req.body에 유저의 폼 데이터가 찍히지 않으면 어떻게 해야하냐? multer라는 npm 모듈을 사용하면 된다.
multer
Middleware for handling `multipart/form-data`.. Latest version: 1.4.5-lts.1, last published: 2 months ago. Start using multer in your project by running `npm i multer`. There are 3344 other projects in the npm registry using multer.
www.npmjs.com
express의 body-parser은 유저의 application/x-www-form-urlencoded데이터(text로 이뤄짐)를 req.body에 key-value 형태로 저장한다. multer는 multipart/form-data를 req.body와 req.files(혹은 req.file)에 나눠 저장한다.
HTML에서 file input은 다음과 같은 태그로 받는다.
<input type="file" name="example-file" />
form 태그의 action이 "/", method가 post라고 할 때, example-file이라는 name을 갖는 file을 받기 위한 javascript 코드는 아래와 같다. multer의 공식문서 코드를 가져왔다.
const express = require('express')
const multer = require('multer')
const upload = multer({ dest: 'uploads/' })
app.post('/', upload.single('example-file'), function (req, res, next) {
// req.file is the `avatar` file
// req.body will hold the text fields, if there were any
})
multipart/form-data를 나눠 저장한다는 것이 이런 의미이다. multer option의 dest를 uploads/로 두었으므로 유저가 input으로 넣은 파일 입력값은 프로젝트 루트 디렉토리의 upload 폴더에 저장된다. 폼 데이터가 서버에 도착한 후 uploa.single() 미들웨어를 거치면 req.file에는 유저가 업로드한 파일의 fieldname, originalname, path, size 등 file에 관한 정보가 들어있는 object가 들어있고 req.body에는 기존 urlencoded 방식과 같이 name-value값이 key-value 형태로 존재한다.
추가로, example-file을 받는 input tag에서 여러개의 파일을 받고싶다면 multiple 옵션을 추가한 뒤 upload.single 대신 upload.array('example-file')로 받는다. 그럼 오브젝트들이 req.files에 array 형태로 존재하게 된다.
multer 객체에 옵션으로 줄 수 있는 요소는 dest(파일을 저장할 디렉토리), fileFilter(accept할 파일의 종류), limits(accept할 파일의 개수) 정도이고 사실 실제로 사용하는 것은 dest정도가 전부이다. 그러나 input file을 조금 더 섬세하게 다루거나 이미지나 영상 등 크기가 큰 input을 다른 외부 서버로 옮기는 작업 등을 하기 위해 storage라는 객체 자체를 multer의 storage 옵션으로 줄 수 있다. (보통 서드파티 라이브러리에서 제공한 storage를 이용한다) 자세한 사항은 공식문서를 참고하자.. 사실 프로젝트에서 실제로 file 관련한걸 다루지 않아서 프로젝트 코드 예시를 따로 들 수가 없어 설명에 한계가 있다.
대신 multer을 이용한 multiplart/form-data 파일 업로드에 참고하면 매우매우 유용할 블로그 링크를 하나 첨부한다.(구글링만세)
단편강좌: Node.JS - Multer로 파일 업로드 - A MEAN Blog
Node.JS Express 서버로 파일을 업로드 하는 방법을 알아봅시다. Multer라는 package를 사용합니다. 이 강의는 Node.JS/Express로 기본적인 웹사이트를 만들 수 있는 분들을 대상으로 합니다. (최소 MEAN Stack
www.a-mean-blog.com
3. methods (static, instance) + virtuals
mongoose 모델에는 특정 기능을 수행하는 method를 추가할 수 있다. java에서 instance/static method와 마찬가지로 모델 자체에 instance / stataic method를 붙일 수 있다.
mongoose schema에서 static method란, 해당 모델 자체에 적용되는 기능을 가진 함수를 말한다. food 모델 자체에 근데 static method를 쓰는 예시를 많이 보진 못했다. 아무래도 스키마 자체 query에 대부분의 기능이 들어가있다보니..
반대로 instance method란 무조건! 특정 모델 객체를 하나 생성한 후에 이용할 수 있는 함수를 말한다. instance variable을 이용해서 기능을 구현한 method의 경우 대부분 instance method이다.
내가 food 모델의 field값을 음식의 이름 / 탄수화물 / 단백질 / 지방의 양 으로 설정했다고 가정해보자. 이 정보를 토대로 총 칼로리양을 모델에 저장해야한다.(사실 칼로리 필드를 따로 추가하는게 와방이긴 하지만 예시니까!)
// foodmodel.js
const mongoose = require("mongoose");
const { Schema } = mongoose;
const foodSchema = new Schema({ name: String, carbs: Number, protein: Number, fat: Number});
방법은 두가지가 있다. 하나는 virtual type을 추가하는 것이고, 하나는 instance method를 이용하는 것이다.
virtual type은 따로 parameter을 받을 필요 없이, 기존 모델의 필드값을 가지고 구해지는 무언가가 있을 때 유용하다.
foodSchema.virtual("calories").get(function () {
return (this.carbs + this.protein) * 4 + this.fat * 9;
});
이제 새로운 food 객체를 선언하고 .calories를 확인하면 해당 음식의 칼로리를 확인할 수 있다. 프로젝트에서 유저의 생년월일을 바탕으로 나이를 구하는 과정, 키/나이/몸무게를 토대로 BMI와 기초대사량을 구하는 과정 등에서 virtual type을 이용했다.
instance method는 짧게 설명했듯 생성한 model instance와 관련한 method를
만약 저 위의 탄단지 양이 100g을 기준으로 한 양이라고 생각해보자. 그런데 나는 200g을 먹고싶다. 200g의 탄단지 양과 칼로리를 알고싶다 한다면? 원하는 양을 입력받아 그를 토대로 영양소 계산을 해주는 Method를 작성하면 된다.
foodSchema.methods.getNuts = function (amount) {
let [carbs, protein, fat] = [0, 0, 0, 0];
carbs = Math.round(this.carbs * amount) / 100;
protein = Math.round(this.protein * amount) / 100;
fat = Math.round(this.fat * amount) / 100;
return { carbs, protein, fat };
};
이제 food 객체를 하나 생성하고 food.getNuts(200)을 하면 원하는 탄단지 양이 반환된다.
이런식으로.. mongoose model에 원하는 method나 type을 추가할 수 있다. 더 필요한 내용이 있으면 또한! 공식 문서를 참고하자.
Mongoose v6.4.6: API docs
mongoosejs.com
4. 필요한 내용 구현
CRUD 내용 자체는 지겹도록 했으니 페이징과 양 수정에 관한 내용만 다루겠다!
일단 음식의 양 수정을 위해 food show page에 유저로부터 원하는 음식 양을 받는 form을 작성했다.
<form action="/food/<%=food._id%>/anotherserving" method="post">
<div class="d-flex">
<div class="input-group input-group-sm">
<input
type="text"
class="form-control"
id="amount"
name="amount"
value="<%=food.amount%>"
aria-describedby="amountperservs"
/>
<span class="input-group-text" id="amountperservs">
<%=food.serving%>
</span>
<button type="submit" class="btn btn-outline-secondary btn-sm">
변경
</button>
</div>
</div>
</form>
food.serving은 g,회,개 중 하나이다. g은 100g기준, 회와 개는 1회와 1개 기준이다.
amount 값으로 유저가 원하는 값을 받을 수 있도록 했다.
// routes/foods.js
const foodController = require("../controllers/food");
router.get("/:id", catchAsync(foodController.showFood));
router.post(
"/:id/anotherserving",
catchAsync(foodController.getAnotherServing)
);
저 폼을 받는 라우터 코드다.
// controllers/food.js
module.exports.getAnotherServing = async (req, res, next) => {
const food = await Food.findById(req.params.id);
const n_nuts = food.getNuts(req.body.amount);
food.calories = n_nuts.calories;
food.carbs = n_nuts.carbs;
food.sugar = n_nuts.sugar;
food.protein = n_nuts.protein;
food.fat = n_nuts.fat;
food.saturated = n_nuts.saturated;
food.amount = req.body.amount;
req.session.anotherfood = { ...food };
res.redirect(`/food/${food._id}`);
};
라우터 코드를 구현하기 위한 controller 코드는 위와 같다. 세션의 anotherfood 필드를 추가하여 food의 수정된 객체를 spread해서 넣어줬다. 앗! 근데 food 객체의 값을 저렇게 막 바꿔도 되는거냐고? 당연하다. food.save()를 하지 않는이상 DB에 저장된 food 값은 바뀌지 않는다. 근데 지금 생각해보니 또 저 코드들은 destructuring으로 가독성을 높일 수 있을 것 같다. 그건 나중에 하기로 하고...
세션에 넣은 수정된 food값을 show page에 반영하기 위해 컨트롤러의 showFood 미들웨어는 아래와 같이 작성되었다.
module.exports.showFood = async (req, res, next) => {
if (req.session.anotherfood) {
const food = { ...req.session.anotherfood._doc };
delete req.session.anotherfood;
res.render("foods/show", { food });
} else {
const food = await Food.findById(req.params.id);
res.render("foods/show", { food });
}
};
model instance 객체를 spread로 넣으면 ._doc을 통해 내부의 실질적인 instance 값이 받아진다. find로 찾아진 mongoose 객체는 단순한 object가 아니라 .save(), virtuals, getter/setter 등의 기능을 지원하는 mongoose document 형태이기 때문이다. 모델 객채의 값 자체를 받아오기 위해선 위의 코드처럼 ._doc으로 따로 빼오거나 find 후에 객체의 값만을 빼오는 추가 조치가 필요하다.
https://velog.io/@modolee/mongodb-document-to-javascript-object
MongoDB - Document를 JavaScript Object로 변환하기
MongoDB + Mongoose를 이용해 API 개발 시 Mongoose Document를 Plain Old JavaScript Object로 변환하는 방법을 소개합니다.
velog.io
위 포스트를 참조해서 다음엔 toObject와 lean method를 이용하도록 하자.
그리고 페이징 구현이다!

이런식으로 음식의 목록을 보여주고, 앞뒤에 표시할 추가 음식목록이 있으면 화살표를 활성화하는 기능을 구현했다.
이를 위해 page값을 query string으로 받고, Mongoose의 limit, skip기능을 사용했다.
// controllers/food.js
module.exports.renderFood = async (req, res, next) => {
const page = req.query.page ? parseInt(req.query.page) : 1;
const foods = await Food.find({})
.skip(15 * (page - 1))
.limit(16);
let end = false;
if (foods.length < 16) {
end = true;
} else {
foods.pop();
}
res.render("foods/index", { foods, page, foodname: null, end });
};
module.exports.searchedFood = async (req, res, next) => {
const page = req.query.page ? parseInt(req.query.page) : 1;
const { foodname } = req.params;
let end = false;
const foods = await Food.find({
name: { $regex: foodname, $options: "i" },
})
.skip(15 * (page - 1))
.limit(16);
if (foods.length < 16) {
end = true;
} else {
foods.pop();
}
res.render("foods/index", { foods, page, foodname, end });
};
코드를 보면 알겠지만 .skip(number)은 find로 찾아진 결과물들 중 number만큼 앞에서 자르겠다는 뜻이고, .limit(number)은 찾은 결과중 number 개수만큼의 결과만 가져오겠다는 의미이다.
food index의 한 페이지에 15개의 음식 정보를 렌더링하기로 했다. 페이지 번호에 따라 .skip method를 이용하여 앞에서부터 15개씩 삭제하면서 페이징을 구성했는데, 만약 해당 페이지에 표시할 음식이 15개 이하라면 다음 페이지로 가는 버튼을 비활성화 시켜야 한다. 이 정보를 전달하기 위해 end라는 boolean값을 ejs파일에 전달했다. end값을 결정하는 기준은 .llimit(16)으로 해당 페이지에 보여줄 15개의 음식값과 다음 페이지의 존재 여부를 결정할 마지막 1개의 음식이다.
따로 mongoose+express 환경에서 페이징을 조금 더 가독성 높고 스마트하게 구현할 수 있는 것 같은데.. 페이징 구현이 메인이 아니라 일단 어떻게든 돌아가게 되는 형태로 구현했다.
searchedFood는 사용자가 음식사전의 검색란에 무언가를 검색했을 때의 처리이다. 그냥 $ regex로 검색한 food string이 존재하는 음식 전체를 불러왔다.
search 쿼리의 동작은 mongoDB의 공식문서를 참고하자.
https://www.mongodb.com/docs/manual/text-search/
Text Search — MongoDB Manual
Docs Home → MongoDB ManualMongoDB offers a full-text search solution, MongoDB Atlas Search, for data hosted on MongoDB Atlas. A legacy text search capability is available for users self-managing MongoDB deployments.For MongoDB Atlas users, MongoDB's Atla
www.mongodb.com
일단 $ regex를 사용해서 검색을 구현했는데, .find({$ text : { $ search : foodname } })를 통해 더 효율적으로 검색 코드를 짤 수 있을 것 같다. 이것도 나중에 수정하자...
다음과 이전 페이지로 가기 위한 화살표 버튼 html코드는 아래와 같다. (views/index.ejs)
<nav>
<ul class="pagination">
<li class="page-item <%=(page===1)?"disabled":""%>">
<a
class="page-link"
href="/food<%=(foodname)?"/search/"+foodname:""%>?page=<%=page-1%>"
aria-label="Previous"
>
<span aria-hidden="true">«</span>
</a>
</li>
<li class="page-item <%=(end)?"disabled":""%>">
<a
class="page-link"
href="/food<%=(foodname)?"/search/"+foodname:""%>?page=<%=page+1%>"
aria-label="Next"
>
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
무언가 검색했을 때만 foodname이 특정 값으로 설정됐다(아니면 null). 그에 따라 다음/이전 페이지 버튼을 눌렀을 때 href값이 다르고, 불려지는 미들웨어도 다르게 했다.
페이지가 1인 경우를 제외하면 이전 페이지는 모두 활성화시켰고, end가 false일 경우에만 다음 페이지 버튼을 활성화시켰다.
기본적인 food CRUD에 관한 내용은 생략하겠다. 그냥 form에서 name-value 키를 req.body로 받아와 new Food({...req.body})해서 .save(), findById, findByIdAndUpdate 등등을 실행한 것 뿐이니까.
다음 포스트는 식단관리의 꽃! 음식과 유저를 잇는 식단 모델을 다뤄보겠다!