웹 개발 실전 프로젝트 - 3주차
Node JS, Express, MongoDB를 배우고 연동해보자
344. EJS의 조건문
<%= %>
구문에서=
기호를 제거하면 JavaScript 로직을 임베드하되, 결과는 템플릿에 추가하지 않을 수 있다.
<% if(num % 2 === 0) { %>
<h2>That is an even number!</h2>
<% } else { %>
<h2>That is an odd number!</h2>
<% } %>
- 위 코드는 삼항 연산자로 한 줄로도 가능하다.
<h3>That number is: <%= num % 2 === 0 ? 'EVEN' : 'ODD' %></h3>
345. EJS의 루프
- 배열의 요소 렌더링하기
app.get('/cats', (req, res) => {
const cats = ['Blue', 'Rocket', 'Monty', 'Stephanie', 'Winston'];
res.render('cats', { cats });
});
<h1>All The Cats</h1>
<ul>
<% for (let cat of cats) { %>
<li><%= cat %></li>
<% } %>
</ul>
346. 복잡한 서브레딧 데모 더 알아보기
- json 데이터 가져오기
- 데이터가 없을 경우 notfound 페이지 뜨게 처리
const redditData = require('./data.json');
app.get('/r/:subreddit', (req, res) => {
const { subreddit } = req.params;
const data = redditData[subreddit];
if (data) {
res.render('subreddit', { ...data });
} else {
res.render('notfound', { subreddit });
}
});
<h1>Browsing The <%= name %> Subreddit</h1>
<h2><%= description %></h2>
<p><%= subscribers %> Total Subscribers</p>
<hr>
<% for (let post of posts) { %>
<article>
<h3><%= post.title %> - <b><%= post.author %></b></h3>
<% if (post.img) { %>
<img src="<%= post.img %> " alt="">
<% } %>
</article>
<% } %>
347. Express의 정적 Assets 사용하기
- 정적 파일은 CSS나 JavaScript 파일 등을 말한다.
- 요청을 받을 때마다 실행하는
app.use
안에express.static
미들웨어를 사용
- 인수로 제공하고 싶은 에셋 폴더 또는 파일을 전달한다.
- views 디렉토리 설정과 같은 방법으로 public도 설정해준다.
- public/js , public/css 등 public 내의 모든 폴더와 파일에 접근할 수 있다.
app.use(express.static(path.join(__dirname, 'public')));
<link rel="stylesheet" href="/style.css">
348. 부트스트랩과 Express
Compiled CSS and JS
jQuery
349. EJS와 파일 분할
- 파일 분할을 EJS에서는 포함(includes) 이라고 부른다.
- 한 템플릿이 하위 템플릿을 포함하는 구조
- 재사용할 수 있는(reusable) 템플릿 만들기 -
<%- %>
구문 사용 - 이스케이프 되지 않은 값을 템플릿에 출력한다.
- 이스케이프: 콘텐츠를 문자열로 취급하는 것
351. Get 요청과 Post 요청
- get 요청은 정보를 가져와서 화면에 띄운다. 데이터는 쿼리스트링 형태로 보내지고, URL로 정보를 볼 수 있다. 데이터 크기에 한계가 있다. 백엔드에 영향 X
- post 요청은 데이터를 request body에 포함시켜서 서버에 제출한다. 크기나 포맷에 있어 유연성을 갖는다 (JSON 가능). 중요한 데이터를 보낼 때 사용.
352. Express Post 경로 정의하기
app.post
<h1>GET AND POST REQUESTS</h1>
<h2>GET</h2>
<form action="http://localhost:3000/tacos" method="get">
<input type="text" name="meat">
<input type="number" name="qty">
<button>Submit</button>
</form>
<h2>POST</h2>
<form action="http://localhost:3000/tacos" method="post">
<input type="text" name="meat">
<input type="number" name="qty">
<button>Submit</button>
</form>
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.get('/tacos', (req, res) => {
res.send('GET /tacos response');
});
app.post('/tacos', (req, res) => {
res.send('POST /tacos response');
});
app.listen(3000, () => {
console.log('ON PORT 3000!');
});
353. 요청(req) 구문 파싱(분석)하기
- 들어오는 HTTP 요청을 기반으로 한 객체에 접근하는 법 req.query
- post 요청의 경우 req(객체)에 body가 들어 있다.
- 제출된 데이터의 키-값 쌍을 포함한다.
- default 값은 undefined다.
라우트 핸들러 콜백에서 req.body
정보에 접근하기
express.json()
이나express.urlencoded()
와 같은 빌트인 파싱 미들웨어를 사용하면 들어오는 요청의 페이로드를 분석할 수 있다.
- 모든 단일 요청에 대해 일부 코드 및 기능을 실행하는 방법인
app.use
를 사용한다.
- 폼 데이터를 Express가 분석하도록 req.body 파싱하기
const express = require('express');
const app = express();
//To parse form data in POST request body:
app.use(express.urlencoded({ extended: true }));
app.get('/tacos', (req, res) => {
res.send('GET /tacos response');
});
app.post('/tacos', (req, res) => {
console.log(req.body);
res.send('POST /tacos response');
});
app.listen(3000, () => {
console.log('ON PORT 3000!');
});



app.post('/tacos', (req, res) => {
const { meat, qty } = req.body;
res.send(`OK, here are your ${qty} ${meat} tacos`);
});
- JSON을 Express가 분석하도록 req.body 파싱하기
// To parse incoming JSON in POST request body:
app.use(express.json());
354. REST 개요
- REST는 Representational State Transfer을 말한다.
- 분산 하이퍼미디어 시스템의 아키텍처 스타일 또는 패러다임
- 웹 앱을 설계할 때 따르는 일련의 원칙들
- REST가 개념, 가이드라인 표준, 원칙이라면, RESTful은 이 REST 규칙에 따르는 시스템이라고 할 수 있다.
- 리소스(자원)는 RESTful 앱에서 많이 쓰이는 용어로, 하나의 엔티티를 말한다. 메시지, 사용자, 이미지 등이 될 수 있다.
- 무상태성(statelessness), 인터페이스 일관성(uniform interface) 등의 특징
- HTTP에 완전한 CRUD Operation 노출하기
- HTTP를 통한 CRUD 기능과 리소스 이름이 있고, HTTP 동사와 경로가 매칭되는 패턴을 따른다.
355. RESTful 댓글 개요
- RESTful 서버 아키텍처 구현하기
- 데이터베이스와 리소스 역할을 할 배열 만들기
- 리소스(자원) 설정하기 comments
- HTTP 동사
- GET /comments 모든 댓글 리스트
- POST /comments - 새 댓글 추가
- GET /comments/:id - (특정 id값을 가진) 1개의 댓글
- PATCH /comments/:id - 1개의 댓글 수정하기
- DELETE /comments - 댓글 삭제하기
356. RESTful 댓글 Index
- 모든 댓글 렌더링하기
// Our fake database:
let comments = [
{
username: 'Todd',
comment: 'lol that is so funny!',
},
{
username: 'Skyler',
comment: 'I like to go birdwatching with my dog',
},
{
username: 'Sk8erBoi',
comment: 'Plz delete your account, Todd',
},
{
username: 'onlysayswoof',
comment: 'woof woof woof',
},
];
app.get('/comments', (req, res) => {
res.render('comments/index', { comments });
});
<h1>Comments</h1>
<ul>
<% for (let c of comments) { %>
<li><%= c.comment %> - <b><%= c.username %></b></li>
<% } %>
</ul>

357. RESTful 댓글 New
- 댓글 생성 기능을 위해 사용자가 제출 가능한 폼 만들기
- username과 comment 입력
- 라우트 하나는 폼 자체의 역할을 해서 폼을 보여줘야 한다.
- 대개 그 라우트를 new라고 지칭한다.
- new 라우트 추가하기
app.get('/comments/new', (req, res) => {
res.render('comments/new');
});
<h1>Make a new comment</h1>
<form action="/comments" method="post">
<section>
<label for="username">Enter username:</label>
<input type="text" id="username" placeholder="username" name="username">
</section>
<section>
<label for="comment">Comment Text</label>
<br>
<textarea name="comment" id="comment" cols="40" rows="5"></textarea>
</section>
<button>Submit</button>
</form>
- 데이터가 제출될 수 있는 post 라우트 추가하기
app.post('/comments', (req, res) => {
console.log(req.body);
res.send('IT WORKED!');
});
- req.body로 들어온 데이터를 추출하기
app.post('/comments', (req, res) => {
const { username, comment } = req.body;
comments.push({ username, comment })
res.send('IT WORKED!');
});
358. Express 방향 수정
- POST 라우트에서 렌더링 하지 않고 모든 댓글이 있는 인덱스로 리다이렉트 하기
res.redirect
- 상태코드 3으로 시작- Express가 다시 전송하는 상태 코드 기본 값은 302
app.post('/comments', (req, res) => {
const { username, comment } = req.body;
comments.push({ username, comment });
res.redirect('/comments');
});

359. RESTful 댓글 Show
- Show 라우트 또는 디테일 라우트
- 하나의 특정 리소스에 대한 디테일을 주로 확장된 보기 형식으로 보여주는 것
- 어떤 리소스에 대한 고유의 식별자가 있어야 작동한다.
- 중첩 라우팅
- 대개 사용자 이름이나 게시물의 슬러그일 수 있다.
app.get('/comments/:id', (req, res) => {
const { id } = req.params; // 문자열
const comment = comments.find((c) => c.id === parseInt(id));
res.render('comments/show', { comment });
});
360. UUID 패키지
- 고유 ID를 생성하기 위해 UUID (Universally Unique Identifier) 패키지 설치
npm i uuid
const { v4: uuid } = require('uuid');
// ...
let comments = [
{
id: uuid(),
username: 'Todd',
comment: 'lol that is so funny!',
},
// ...
app.post('/comments', (req, res) => {
const { username, comment } = req.body;
comments.push({ username, comment, id: uuid() });
res.redirect('/comments');
});
- uuid를 호출할 때마다 새로운 범용 고유 식별자가 생성된다.
- uuid는 정수 값이 아닌 문자열 ID를 반환한다.
361. RESTful 댓글 Update
- post 요청처럼 patch 요청도 페이로드를 포함할 수 있다.
app.patch('/comments/:id', (req, res) => {
const { id } = req.params;
const newCommentText = req.body.comment;
const foundComment = comments.find(c => c.id === id);
foundComment.comment = newCommentText;
})
- patch 요청은 기존 리소스에 업데이트하거나 추가할 때 사용하고, 페이로드(전송되는 데이터 자체)만 포함하고 있다.
- post 요청의 경우, body에 댓글의 텍스트뿐 아니라 username 등 모든 정보를 포함하고 있다.
- 위 방법의 경우 배열에 있는 객체를 변경하는 방법으로써, 객체를 변형하지 않는 불변성을 강조하는 JavaScript의 흐름에는 맞지 않다.
362. Express 메서드 재정의
- 댓글 편집 폼 만들기
app.get('/comments/:id/edit', (req, res) => {
const { id } = req.params;
const comment = comments.at((c) => c.id === id);
res.render('comments/edit', { comment });
});
메서드 오버라이드
- 패키지 설치
npm i method-override
- 브라우저 폼처럼 클라이언트가 해당 작업을 지원하지 않는 환경에서 put, delete 등의 HTTP 동사를 쓸 수 있게 해준다. 미들웨어
- 쿼리스트링을 이용하는 방법과, HTTP 헤더를 요청에 전달하는 방법이 있다.
- 쿼리스트링 방법
app.use(methodOverride('_method'))
'_method'
는 쿼리스트링에서 찾으려는 문자열을 입력한 것이다.- 폼 method가 무엇으로 설정되어 있든 action으로 전달된 method 쿼리 값을 HTTP 동사로 취급하게 된다.
<form method="POST" action="/comments/<%= comment.id %>?_method=PATCH">
<textarea name="comment" id="comment" cols="40" rows="10"><%= comment.comment %></textarea>
<button>Save</button>
</form>
363. RESTful 댓글 Delete
- 댓글 삭제 엔드포인트 설정법
- 경로는 /comments/:id로 동일
- HTTP 동사 DELETE 사용하기
- PATCH 요청처럼 override 메서드를 사용할 수 있다.
app.delete('/comments/:id', (req, res) => {
const { id } = req.params;
comments = comments.filter(c => c.id !== id);
res.redirect('/comments');
})
<form method="POST" action="/comments/<%= comment.id %>?_method=DELETE">
<button>Delete</button>
</form>
365. 데이터베이스 개요
- DB는 어마어마한 데이터를 효율적으로 저장, 압축하여 관리하기 쉽고 접속하기 쉽게 만들어 준다.
- 데이터 사이즈를 더 작게 압축
- 다양한 메서드와 언어로 된 수많은 도구와 레이어를 사용하려면 데이터베이스 탑에서 데이터를 쉽게 삽입, 문의, 갱신, 삭제할 수 있다.
- DB를 쓰면 일의 효율이 높아진다.
- 순수한 데이터가 저장된 데이터베이스의 탑에는 레이어가 있다. ⇒ 데이터베이스 관리 시스템(DBMS)
- 보안 기능, 관리자 접속을 누구에게 허용할 지 제어하는 기능
366. SQL과 NoSQL 데이터베이스
- SQL(Structured Query Language)
- MySQL, Postgres, Oracle, Microsoft SQL Server, SQLite, ...
- 구조화된 쿼리 언어 즉, 기본 구문을 공유하는 관계형 데이터베이스
- 스키마와 테이블을 세팅한 후 DB에 무언가를 추가할 수 있다.
- NoSQL
- MongoDB, CouchDB, Neo4j, Cassandra, Redis, ...
- 일반적인 유형: 문서 데이터 저장소, 키-값 저장소, 그래프 저장소 등
- SQL의 구조화된 쿼리 언어를 사용하지 않고 많은 요소를 포괄하는 방식
- 데이터를 여러 테이블로 나눌 필요가 없다. 스키마와 패턴을 미리 지정하지 않아도 되고, 따르지 않아도 된다.
- 대신 데이터를 있는 그대로 가져와 주어진 것에 대한 관련 정보를 DB의 한 인스턴스 안에 저장하면 된다. 유연한 저장소 개념!
367. Mongo를 배워야 하는 이유
- MERN 스택 또는 MEAN 스택
- MongoDB, Express, React(Angular), Node
- 구문 또는 스키마 정의를 신경쓰지 않아도 되고, 기본 키, 외래 키, 한계 얘기 등에 시간을 뺏기지 않아도 된다.
- 결국 언젠가는 SQL 데이터베이스를 잘 숙지하는 데 시간을 투자할 가치는 있다.
370. Mongo Shell

- 현재 Mongo shell은 deprecated 되었고 mongosh를 설치하라고 나온다.
help

db
아무것도 작업한 게 없으면 test라는 DB가 나온다.show databases
혹은 show dbs
입력
데이터베이스 만들기
- 앱에 쓸 데이터를 저장할 공간을 만들어 보자.
use db이름

- Mongo는 실제 저장할 데이터가 있을 때까지 대기한다. 지금 show dbs를 해도 보이지 않는다.
371. BSON이란?
- Mongo가 어떤 종류의 정보를 기대하는가
BSON
- 이진법 JSON (압축된 버전)
- 모든 키는 따옴표로 감싸야 한다.
- JSON의 문제는 홈페이지나 문서에 들어갈 때 상당히 느리다는 것
- 텍스트 기반 형식이며, 텍스트 파싱이 느릴 수 있다.
- 읽을 수 있는 형식으로 저장되므로 공간 효율성도 좋지 않다.
372. Mongo 데이터베이스에 삽입하기
데이터 입력하기

insertOne
은 집합에 작성될 한 가지, 한 객체만을 전달한다.
show collections
- collection(집합)을 사용하는 주된 이유 중 하나는 조회할 수 있다는 것

- 실제 JavaScript 객체를 전달할 수도 있기 때문에 유효한 JSON을 사용할 필요가 없다.
- 하나를 추가하면 acknowledged(승인)되었다고 나온다.
- 조회는
db.collection.find()
로 한다.
- “_id” 특성은 Mongo에 의해 자동 생성된다. (기본 키, 모든 요소에 대해 고유함)
- 이 데이터베이스에서 나가려면
show dbs
,use local
입력

- 추가할 때 아직 존재하지 않는 집합이면 생성되고, 존재하면 기존 집합에 추가된다.
- 여기까지가 CRUD의 Create
373. Mongo 데이터베이스에서 찾기
- 데이터베이스에서 데이터를 쿼리(찾거나) 또는 읽는 법. CRUD의 Read



- 없는 결과는 출력되지 않는다.
- 결과가 하나만 나오길 원한다면
findOne()
메서드를 쓰면 된다. - findOne은 실제 문서를 반환한다.
- find가 반환하는 것은 커서
- 커서는 마치 포인터 또는 결과의 참조라고 할 수 있다. 결과를 즉시 얻는 것과는 다르다.
374. Mongo 데이터베이스 업데이트하기
- CRUD의 업데이트
- 인수로는 적어도 업데이트 할 것(선택자)과, 방법 두 가지를 전달해야 한다.
- 업데이트 연산자를 사용해야 한다.
$set

updateOne
은 매치되는 첫 항목만,updateMany
는 매치되는 모두를 업데이트한다.
- 탭 키를 치면 자동완성된다.

- 현재 매치된 문서에 없는 것을 업데이트하면 새로운 키-값 쌍이 생긴다.
use sample_airbnb
db.listingsAndReviews.updateMany(
{ security_deposit: { $lt: 100 } },
{
$set: { security_deposit: 100, minimum_nights: 1 }
}
)
- $set 연산자는 필드를 대체하거나 추가하는 역할을 한다.
- 그 외에
$currentDate
연산자는 문서에 현재 날짜를 설정할 때 사용한다. (마지막 업데이트 날짜를 현재 날짜로 업데이트)
use sample_mflix
db.movies.updateOne( { title: "Tag" },
{
$set: {
plot: "One month every year, five highly competitive friends
hit the ground running for a no-holds-barred game of tag"
}
{ $currentDate: { lastUpdated: true } }
})
- lastUpdated 라고 되어 있는 필드가 무엇이든 값이 true이면 현재 날짜로 설정된다.


replaceOne
은 문서의 특정 내용을 완전히 대체한다.
375. Mongo 데이터베이스에서 삭제
- CRUD에서 delete

- 여러 항목 동시에 지우기

$or
연산자나$lt
연산자를 사용해 조건을 추가할 수도 있다.
376. 기타 Mongo 연산자
- 검색과 업데이트에 논리 추가하기
- 중첩된 항목을 찾는 법
.
마침표 구문
db.dogs.find({'personality.childFriendly': true, size: 'M'})
$gt
초과$gte
이상$lt
미만$lte
이하
$in
배열 안에 어떤 값이 포함되어 있는 문서를 선택
db.dogs.find({breed: {$in: ['Mutt', 'Corgi']}, age: {$lt: 10}})
$nin
안에 없음. 이렇게 했을 때 $in과 정반대 결과가 나오게 된다.
db.dogs.find({breed: {$nin: ['Mutt', 'Corgi']}})
$ne
not equals 같지 않음
- 논리 연산자도 사용 가능
$and
$not
$nor
$or
377. 섹션 주제
- Mongo Shell이 아닌 Node 앱에서 Mongo DB에 접속할 수 있는 방법
- Mongoose를 활용한 CRUD
- 스키마 제약 조건, 모델 인스턴스와 정적 메서드 등
- 미들웨어와 버추얼
378. Mongoose란?
- ODM(Object Data Mapper) 객체 데이터 매퍼, 객체 문서 매퍼로 알려져 있다.
- SQL 기반 데이터베이스의 경우 이런 유형의 소프트웨어를 ORM (Relational) 객체 관계(관계성) 매퍼라 부른다.
- 데이터베이스를 번역해 프로그래밍 언어로 만들고, 여러 기능을 제공한다.
- Mongoose는 Mongo에서 회신하는 데이터와 Mongo에 삽입하려는 데이터를 매핑하여 메서드를 추가할 수 있는 JavaScript 객체로 만든다.
- Node.js와 JavaScript를 Mongo에 연결할 수 있도록 도와주는 툴
- JavaScript 내에서 Mongo DB를 훨씬 편리하게 만들어주는 여러 툴과 메서드를 제공하여 사용 경험을 개선해 준다.
- 유효성 검사를 추가할 수 있고, 사전에 프리셋 스키마를 정의할 수 있다.
- 데이터가 Mongoose를 통해 레이아웃된 스키마를 따르도록 강제할 수 있다.
379. Mongo에 Mongoose를 연결하기
- 설치하기
npm i mongoose
const mongoose = require('mongoose');
- mongoose를 백그라운드에서 실행 중인 Mongo DB 서버에 연결하기

const mongoose = require('mongoose');
const main = async () => {
await mongoose.connect('mongodb://localhost:27017/movieApp');
}
main().catch((err) => {
console.log('OH NO ERROR!!!');
console.log(err);
})
380. 우리의 첫 번째 Mongoose 모델
- Mongoose를 이용하는 핵심 목표는 JavaScript에서 MongoDB와 더 쉽게 상호작용하기 위해서다.
- 모델 이해하기
- 모델은 Mongoose가 생성하는 JavaScript 클래스로, MongoDB의 정보를 나타낸다.
- 구체적으로는 어떤 집합의 정보를 나타내는 것
- MongoDB에서 가져온 데이터로 모델을 만들고, 상호 작용을 도와 데이터베이스나 쿼리로 새로운 정보를 보내고, 삭제 또는 업데이트하는 것을 도와준다.
- JavaScript 파일에서 데이터를 사용하거나 접근하려면 각 데이터를 정의하는 모델을 만들어야 한다.
- 스키마(Schema) 정의하기
- 스키마는 Mongo의 각기 다른 키 집합을 JavaScript의 다른 타입으로 구조를 짜는 것
- SchemaTypes

const movieSchema = new mongoose.Schema({
title: String,
year: Number,
score: Number,
rating: String,
});
- 스키마를 이용한 모델 만들기
mongoose.model()
- 인수로 모델 이름을 나타내는 문자열과 스키마를 받는다.
- 모델 이름은 단수형이면서 첫 번째 문자는 대문자로 작성한다.
const Movie = mongoose.model('Movie', movieSchema);

- Movie 클래스의 새로운 인스턴스를 만들어 Mongo DB에 저장하기
const amadeus = new Movie({ title: 'Amadeus', year: 1986, score: 9.2, rating: 'R' });
- Node REPL을 이용해 파일을 실행한다.
node
.load index.js
amadeus
를 입력하면 생성한 인스턴스에 접근할 수 있다.- 데이터베이스에 저장하고 싶으면
.save
를 호출하면 된다. 생성한 객체에 사용되는 메서드로, 모델을 생성하면 따라온다. - 수정할 수도 있다. 그러면 JavaScript만 수정된 상태인데, 다시 save를 호출해야 한다.






381. 대량 삽입하기
insertMany
메서드- 모델의 단일 인스턴스를 생성하는 경우 save를 호출해서 데이터베이스에 저장해야 하지만, insertMany 메서드를 사용할 때는 save를 호출하지 않아도 된다.
- Mongo에서 사용한 insertMany와 동일한 것으로, 이 경우는 Mongoose의 JavaScript에서 Promise를 반환하는 모델 메서드다.
Movie.insertMany([
{ title: 'Amelie', year: 2001, score: 8.3, rating: 'R' },
{ title: 'Alien', year: 1979, score: 8.1, rating: 'R' },
{ title: 'The Iron Giant', year: 1999, score: 7.5, rating: 'PG' },
{ title: 'Stand By Me', year: 1986, score: 8.6, rating: 'R' },
{ title: 'Moonrise Kingdom', year: 2012, score: 7.3, rating: 'PG-13' }
])
.then(data => {
console.log("IT WORKED!")
console.log(data);
})
node index.js
후 mongo shell에서 db.movies.find()를 호출하면 새로운 영화들을 볼 수 있다.

382. Mongoose로 찾기
- 쿼리는 thenable 객체로, Promise와는 다르다.

- 특정 조건 찾기

$lt
$gte
등의 연산자를 사용해 찾을 수도 있다.Movie.find({ year: {$gte: 2010}})
- find로 찾으면 배열 안에 담기는데, findOne으로 영화 하나만 찾을 수도 있다.

exec()
메서드는 thenable 객체가 아니라 완전한 Promise를 준다.
- Express 앱에서는
findById
메서드를 특히 많이 사용하게 된다. findOne({_id: id})
와 같다.- 앱에서 루트나 매개변수 안에 id를 넣어 활용할 수 있다. id를 request body에 넣을 수도 있다.
383. Mongoose로 업데이트하기
updateOne
,updateMany
메서드- 갱신된 정보를 반환하지 않고, 몇 개의 항목이 수정되었는지만 알려준다.
- updateOne은 쿼리에 매치되는 첫 번째 항목을 우리가 제시한 것으로 갱신한다.
$set
연산자는 필요없다. - updateMany 여러 항목 수정하기


Movie.updateMany({title: {$in: ['Amadeus', 'Stand By Me']}}, {score: 10}).then(res => console.log(res))
findByIdAndUpdate
findOneAndUpdate
- 수정 전 버전을 반환받는데, 갱신된 버전의 객체를 보고 싶으면 세 번째 인수로 new 옵션 객체를 전달하면 된다. 이 옵션은 false가 디폴트 값이지만 true가 일반적으로 많이 쓰인다.
- 댓글, 게시물 등의 갱신 라우트 또는 patch 라우트 등을 만들 때 유용하게 쓰일 만한 옵션.
Movie.findOneAndUpdate({title: 'The Iron Giant'}, {score: 7.8}, {new: true}).then(m => console.log(m))
useFindAndModify
를 false로 변경하면 된다.384. Mongoose로 삭제하기
remove
를 실행하면 동작은 하지만,deleteOne
나deleteMany
또는bulkWrite
를 사용하라는 경고가 뜬다.- 역시 관련 정보나 문서를 반환하지 않는 메서드이다. 삭제된 개수만 반환한다.
Movie.deleteMany({year: {$gte: 1999}}).then(msg => console.log(msg))
findOneAndRemove
,findByIdAndRemove
- 삭제된 데이터가 필요하다면 이 두 메서드를 사용한다.

update
, remove
, delete
등은 아무 데이터도 반환되지 않고,
수정되거나 삭제된 개수와 확정 메시지만 반환된다.
findOneAndUpdate
, findOneAndDelete
, findByIdAndUpdate
, findByIdAndDelete
등은
수정되거나 삭제된 데이터가 반환된다.385. Mongoose 스키마 유효성 검사
const mongoose = require('mongoose');
const main = async () => {
await mongoose.connect('mongodb://localhost:27017/shopApp');
};
main().catch((err) => {
console.log('OH NO ERROR!!!');
console.log(err);
});
const productSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
price: {
type: Number,
},
});
const Product = mongoose.model('Product', productSchema);
const bike = new Product({ name: 'Mountain Bike', price: 599 });
bike
.save()
.then((data) => {
console.log('IT WORKED!');
console.log(data);
})
.catch((err) => {
console.log('OH NO ERROR!');
console.log(err);
});
- name이 필수 항목이므로 적지 않으면 유효성 검사를 통과하지 못해 에러가 뜨게 된다.
const Product = mongoose.model('Product', productSchema);
const bike = new Product({ price: 599 });
bike
.save()
.then((data) => {
console.log('IT WORKED!');
console.log(data);
})
.catch((err) => {
console.log('OH NO ERROR!');
console.log(err.errors.name.properties.message);
});

- 스키마에 없는 정보를 추가하면 해당 항목은 무시된다.
386. 추가 스키마 제약 조건

- 개별 타입에 적용할 수 있는 특정 제약 조건도 있다.
- match로 정규 표현식을 전달할 수도 있다. 정규 표현식을 쓰지 않아도 되도록 해주는 패키지도 있음

const productSchema = new mongoose.Schema({
name: {
type: String,
required: true,
maxlength: 20,
},
price: {
type: Number,
required: true,
min: 0,
},
onSale: {
type: Boolean,
default: false,
},
categories: [String], // ⭐ 문자열로만 이루어진 배열
qty: {
online: {
type: Number,
default: 0,
},
inStore: {
type: Number,
default: 0,
},
},
});
387. Mongoose 업데이트 유효성 검사하기
- 업데이트 시에는 유효성 검사가 먹히지 않는다.
- Mongoose 뿐만 아니라 많은 ODM, ORM 들의 특징
FindOneAndUpdate
의 옵션 중runValidators
를 true로 설정하면 유효성 검사가 동작해 제약 조건이 적용된다.
388. Mongoose 유효성 검사 오류
- 커스텀 유효성 검사기 메시지 (오류 메시지)를 설정하는 방법
- 보통은 데이터를 서버로 보내기 전에 클라이언트 사이드에서도 유효성 검사를 한다. 그런 다음 서버 측에서도 검사를 하는 것이다.
const productSchema = new mongoose.Schema({
...,
price: {
type: Number,
required: true,
min: [0, 'Price must be positive ya dodo!'],
},
...,
});
enum
은 문자열 값으로 된 배열을 제공한 후에, 해당 값이 그 배열 안에 있는지 확인하는 유효성 검사를 한다.- 여러 항목을 유효하게 만들 수 있어 유용하다.
const productSchema = new mongoose.Schema({
...,
size: {
type: String,
enum: ['S', 'M', 'L'],
},
...,
});
389. 인스턴스 메서드
- 커스텀 메서드를 스키마에 추가하는 방법
- Mongoose가 이미 제공하는 기능 외에 추가로 모델에 기능을 정의하거나 추가할 수 있다.
- 인스턴스 메서드
- 꼭 화살표 함수가 아닌 기존의 함수 표현식을 사용해야 한다.
productSchema.methods.greet = function () {
console.log("HELLLO!!! HI!! HOWDY!!! ")
console.log(`- from ${this.name}`)
}

- 특정 인스턴스를 찾을 때도 인스턴스 메서드에 엑세스할 수 있다.
const findProduct = async () => {
const foundProduct = await Product.findOne({ name: 'Bike Helmet' });
foundProduct.greet();
}
- 이런 류의 인스턴스 메서드를 사용할 일이 많을 것
- 인증(authentication) 작업 시 사용자(User) 모델이 있을 것이고, 사용자에겐 이메일, 비밀번호 같은 정보들이 있으면
checkCredentials
나checkAuthenticate
,checkValidate
같은 메서드를 만들어 DB에 저장된 비밀번호와 입력값이 같은지 확인할 수 있다. (실제로는 비밀번호를 DB에 저장하진 않는다.)
- onSale을 토글하는 메서드
productSchema.methods.toggleOnSale = function () {
this.onSale = !this.onSale;
return this.save(); // 효력을 발휘하려면 저장해야 함
}
- addCategory
productSchema.methods.addCategory = function (newCat) {
this.categories.push(newCat);
return this.save();
}
const findProduct = async () => {
const foundProduct = await Product.findOne({ name: 'Mountain Bike' });
console.log(foundProduct)
await foundProduct.toggleOnSale();
console.log(foundProduct)
await foundProduct.addCategory('Outdoors')
console.log(foundProduct)
}
390. 정적 메서드 추가하기
- 인스턴스가 아닌 모델 자체에 적용되는 정적 메서드
statics
뒤에 원하는 메서드를 추가하면 된다.
- 여기에서 this는 개별 인스턴스가 아니라 모델 클래스 자체를 가리킨다.
productSchema.statics.fireSale = function () {
return this.updateMany({}, { onSale: true, price: 0 })
}
Product.fireSale().then(res => console.log(res))
- 모델의 정적 메서드는 모델 전체에서 항목을 찾거나 업데이트하거나 생성, 삭제할 수 있는 더 편하고 유용한 방식
391. 가상 Mongoose
virtual
- 실제 데이터베이스 자체에는 존재하지 않는 스키마에 특성을 추가할 수 있게 해준다.
const personSchema = new mongoose.Schema({
first: String,
last: String,
});
personSchema.virtual('fullName').get(function () {
return `${this.first} ${this.last}`;
});
const Person = mongoose.model('Person', personSchema);
- 인스턴스 메서드를 추가하는 것과는 다르다. setter와 getter를 정의할 수 있다.

- person이 자동으로 복수형 people로 변환되어 있다.

- fullName 특성은 없는 것을 확인할 수 있다. (가상 특성)

392. Mongoose를 미들웨어로 정의하기
- Mongoose는 특정 작업 전후에 코드를 실행할 수 있게 해준다.
.post
미들웨어는 init, validate, save, remove 등 어떤 메서드이든 뒤에서 실행.pre
미들웨어는 어떤 메서드이든 앞에서 실행된다.
- 이를 작동시키려면 콜벡에서
next
라는 매개변수를 수용하고, 함수 마지막에서next()
를 실행해야 한다.

personSchema.pre('save', async function () {
this.first = 'YO';
this.last = 'MAMA';
console.log("ABOUT TO SAVE!!!!")
})
personSchema.post('save', async function () {
console.log("JUST SAVED!!!!")
})


393. 섹션 주제
- Mongoose, Mongo를 Express 앱과 연결하기
- Express로 완전한 CRUD 구현하기
- 상품 정의 및 상품 인덱스 만들기
394. Express와 Mongoose 기본 설정
- EJS용 Express를 구성하고, 뷰 디렉토리 설정을 포함한 기본 설정 후 mongoose 통합
npm i express mongoose ejs
const express = require('express');
const app = express();
const path = require('path');
const mongoose = require('mongoose');
mongoose
.connect('mongodb://localhost:27017/farmStand', { useNewUrlParser: true })
.then(() => {
console.log('MONGO CONNECTION OPEN!!!');
})
.catch(() => {
console.log('OH NO MONGO CONNECTION ERROR!!!!');
console.log(err);
});
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.get('/dog', (req, res) => {
res.send('WOOF!');
});
app.listen(3000, () => {
console.log('APP IS LISTENING ON PORT 3000!');
});
- index.js에는 서버를 시작하는 기본 코드만 두고, 모든 모델 및 라우트는 별개 파일로 이동
395. 모델 만들기
mkdir models
touch product.js
const mongoose = require('mongoose');
// 스키마 정의 후 모델 생성
const productSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
min: 0,
},
category: {
type: String,
lowercase: true,
enum: ['fruit', 'vegetable', 'diary'],
},
});
// 모델 컴파일링
const Product = mongoose.model('Product', productSchema);
// 모듈 내보내기
module.exports = Product;
- index.js에서 DB 연결
// 모델 요청
const Product = require('./models/product');
- 데이터베이스에 초기 데이터를 심기 위해 시드 파일 생성
- 웹 앱과는 별개로 DB에서 데이터를 요청할 때마다 단독으로 실행됨
const mongoose = require('mongoose');
const Product = require('./models/product');
mongoose
.connect('mongodb://localhost:27017/farmStand', { useNewUrlParser: true })
.then(() => {
console.log('MONGO CONNECTION OPEN!!!');
})
.catch(() => {
console.log('OH NO MONGO CONNECTION ERROR!!!!');
console.log(err);
});
const p = new Product({
name: 'Ruby Grapefruit',
price: 1.99,
category: 'fruit',
});
p.save()
.then((p) => {
console.log(p);
})
.catch((e) => {
console.log(e);
});
396. 프로덕트 인덱스
- 라우트를 위한 비동기 콜백 패턴
app.get('/products', async (req, res) => {
const products = await Product.find({});
res.render('products/index', { products })
});
<ul>
<% for (let product of products) { %>
<li><%= product.name %></li>
<% } %>
</ul>
397. 프로덕트 디테일
- 단일 상품의 상세 페이지 설정 /products/:id
- id 대신 상품명을 사용하지 않는 이유
- 상품명이 중복될 수 있음
- 상품명의 띄어쓰기와 문자가 URL에서 안전하지 않을 수 있다.
- URL을 웹 슬러그로 바꿔야 한다. (slugify)
findOne
을 사용해서 {_id: id}를 전달하는 것보다findByID
를 호출하는 것이 간편findByName
이나findBySlug
를 사용할 수도 있다.
398. 프로덕트 만들기
- 폼과 폼을 제출할 두 개의 라우트가 필요
- app.get에 /products/new 입력
// NEW
app.get('/products/new', (req, res) => {
res.render('products/new', { categories })
})
<form action="/products" method="POST">
// CREATE
app.post('/products', async (req, res) => {
const newProduct = new Product(req.body); // 새로운 상품 생성
await newProduct.save(); // 저장 기다렸다가
res.redirect(`/products/${newProduct._id}`) // 리다이렉트
})
app.use(express.urlencoded({ extended: true }));
- req.body 보안 관련 작업을 위해 Sanitize 같은 보안 모듈이 있다.
399. 프로덕트 업데이트하기
- 개별 상품 업데이트를 위한 edit 라우트 추가
// EDIT
app.get('/products/:id/edit', async (req, res) => {
const { id } = req.params;
const product = await Product.findById(id);
res.render('products/edit', { product, categories })
})
- 제출할 update 엔드포인트 생성
- PUT 요청은 객체를 재정의하거나 교체할 때 사용
- PATCH 요청은 문서나 객체의 일부를 변경할 때 사용
- PUT 요청을 위해 메서드 오버라이드 사용하기
npm i method-override
const methodOverride = require('method-override');
// ...
app.use(methodOverride('_method'))
<form action="/products/<%=product._id%>?_method=PUT" method="POST">
// UPDATE
app.put('/products/:id', async (req, res) => {
const { id } = req.params;
const product = await Product.findByIdAndUpdate(id, req.body, { runValidators: true, new: true });
res.redirect(`/products/${product._id}`);
})
findByIdAndUpdate
에 id와 데이터를 전달한다.400. 카테고리 셀렉터 위의 탄젠트
- 상품 편집 시 해당 상품의 카테고리가 선택되어 있도록 하기 (selected)
<select name="category" id="category">
<% for(let category of categories){ %>
<option value="<%=category%>" <%= product.category === category ? 'selected': '' %>><%=category%></option>
<% } %>
</select>
401. 프로덕트 삭제하기
findByIdAndDelete
// DELETE
app.delete('/products/:id', async (req, res) => {
const { id } = req.params;
const deletedProduct = await Product.findByIdAndDelete(id);
res.redirect('/products');
})
402. 보너스: 카테고리별로 필터링하기
- 카테고리 필터링을 위해 URL 설정
- 정보의 부분집합을 필터링하거나 검색할 때 쿼리스트링 사용
<h1><%= product.name %></h1>
<ul>
<li>Price: $<%= product.price%></li>
<li>Category: <a href="/products?category=<%= product.category%>"><%= product.category%></a></li>
</ul>
<a href="/products">All Products</a>
<a href="/products/<%=product._id%>/edit">Edit Product</a>
<form action="/products/<%=product._id%>?_method=DELETE" method="POST">
<button>Delete</button>
</form>

<%if(category !== 'All') {%>
<a href="/products">All Products</a>
<% } %>
본 스터디는 Udemy의 <【한글자막】 The Web Developer 부트캠프 2022> 강의를 활용해 진행됐습니다. 강의에 대한 자세한 정보는 아래에서 확인하실 수 있습니다.
프밍 스터디는 Udemy Korea와 함께 합니다.