스터디 포스트 >  웹 개발 실전 프로젝트

node 뿌시기(MEN stack)-2

조재홍 멘토
이로운 개발자가 되고 싶은 조재홍입니다.

웹 개발 실전 프로젝트 - 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 });
});
index.js
<h1>All The Cats</h1>
<ul>
    <% for (let cat of cats) { %>
    <li><%= cat %></li>
    <% } %> 
</ul>
cats.ejs
 

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 });
  }
});
index.js
<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>
<% } %>
subreddit.ejs
 

347. Express의 정적 Assets 사용하기

  • 정적 파일은 CSS나 JavaScript 파일 등을 말한다.
  • 요청을 받을 때마다 실행하는 app.use 안에 express.static 미들웨어를 사용
  • 인수로 제공하고 싶은 에셋 폴더 또는 파일을 전달한다.
  • views 디렉토리 설정과 같은 방법으로 public도 설정해준다.
  • public/js , public/css 등 public 내의 모든 폴더와 파일에 접근할 수 있다.
app.use(express.static(path.join(__dirname, 'public')));
index.js
<link rel="stylesheet" href="/style.css">
ejs
 

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!');
});
 
notion image
req.body 출력
req.body 출력
 
 
 
notion image
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 });
});
index.js
<h1>Comments</h1>
<ul>
    <% for (let c of comments) { %> 
    <li><%= c.comment %> - <b><%= c.username %></b></li>
    <% } %>
</ul>
index.ejs
notion image
 

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>
new.ejs
  • 데이터가 제출될 수 있는 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');
});
notion image
 

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>
/edit으로 가지 않는다.
 

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

notion image
  • 현재 Mongo shell은 deprecated 되었고 mongosh를 설치하라고 나온다.
help
notion image
db 아무것도 작업한 게 없으면 test라는 DB가 나온다.

show databases 혹은 show dbs 입력

 

데이터베이스 만들기

  • 앱에 쓸 데이터를 저장할 공간을 만들어 보자.

use db이름

notion image
  • Mongo는 실제 저장할 데이터가 있을 때까지 대기한다. 지금 show dbs를 해도 보이지 않는다.
 

371. BSON이란?

  • Mongo가 어떤 종류의 정보를 기대하는가

BSON

  • 이진법 JSON (압축된 버전)
  • 모든 키는 따옴표로 감싸야 한다.
  • JSON의 문제는 홈페이지나 문서에 들어갈 때 상당히 느리다는 것
    • 텍스트 기반 형식이며, 텍스트 파싱이 느릴 수 있다.
    • 읽을 수 있는 형식으로 저장되므로 공간 효율성도 좋지 않다.
 

372. Mongo 데이터베이스에 삽입하기

데이터 입력하기

notion image
  • insertOne 은 집합에 작성될 한 가지, 한 객체만을 전달한다.

show collections

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

373. Mongo 데이터베이스에서 찾기

  • 데이터베이스에서 데이터를 쿼리(찾거나) 또는 읽는 법. CRUD의 Read
https://www.mongodb.com/docs/mongodb-shell/crud/read/
notion image
notion image
  • 없는 결과는 출력되지 않는다.
  • 결과가 하나만 나오길 원한다면 findOne() 메서드를 쓰면 된다.
    • findOne은 실제 문서를 반환한다.
    • find가 반환하는 것은 커서
      • 커서는 마치 포인터 또는 결과의 참조라고 할 수 있다. 결과를 즉시 얻는 것과는 다르다.

374. Mongo 데이터베이스 업데이트하기

  • CRUD의 업데이트
  • 인수로는 적어도 업데이트 할 것(선택자)과, 방법 두 가지를 전달해야 한다.
  • 업데이트 연산자를 사용해야 한다. $set
https://www.mongodb.com/docs/mongodb-shell/crud/update/
  • updateOne 은 매치되는 첫 항목만, updateMany 는 매치되는 모두를 업데이트한다.
  • 탭 키를 치면 자동완성된다.
Charlie를 찾아 age를 3에서 5로 업데이트했다.
Charlie를 찾아 age를 3에서 5로 업데이트했다.
  • 현재 매치된 문서에 없는 것을 업데이트하면 새로운 키-값 쌍이 생긴다.
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이면 현재 날짜로 설정된다.
lastChanged에 ISODate 정보가 들어가 있음을 볼 수 있다.
lastChanged에 ISODate 정보가 들어가 있음을 볼 수 있다.
시간 업데이트
시간 업데이트
  • replaceOne 은 문서의 특정 내용을 완전히 대체한다.
 

375. Mongo 데이터베이스에서 삭제

  • CRUD에서 delete
https://www.mongodb.com/docs/mongodb-shell/crud/delete/
  • 여러 항목 동시에 지우기
notion image
  • $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 서버에 연결하기
https://mongoosejs.com/docs/index.html
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
      • notion image
      const movieSchema = new mongoose.Schema({
        title: String,
        year: Number,
        score: Number,
        rating: String,
      });
  • 스키마를 이용한 모델 만들기 mongoose.model()
    • 인수로 모델 이름을 나타내는 문자열과 스키마를 받는다.
    • 모델 이름은 단수형이면서 첫 번째 문자는 대문자로 작성한다.
    • const Movie = mongoose.model('Movie', movieSchema);
    • Mongoose가 모델 이름을 따서 자동으로 ‘movies’라는 집합을 생성하게 된다. (자동으로 복수형이 되고, 첫 문자도 소문자로 바뀐다.)
    • 즉, 집합 이름은 movies, 모델 이름은 Movie가 된다.
      • notion image
  • Movie 클래스의 새로운 인스턴스를 만들어 Mongo DB에 저장하기
const amadeus = new Movie({ title: 'Amadeus', year: 1986, score: 9.2, rating: 'R' });
  • Node REPL을 이용해 파일을 실행한다.
    • node
    • .load index.js
      • notion image
    • amadeus 를 입력하면 생성한 인스턴스에 접근할 수 있다.
      • notion image
    • 데이터베이스에 저장하고 싶으면 .save 를 호출하면 된다. 생성한 객체에 사용되는 메서드로, 모델을 생성하면 따라온다.
      • notion image
        notion image
    • 수정할 수도 있다. 그러면 JavaScript만 수정된 상태인데, 다시 save를 호출해야 한다.
      • notion image
        notion image
 

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()를 호출하면 새로운 영화들을 볼 수 있다.
    • notion image
 

382. Mongoose로 찾기

  • 쿼리는 thenable 객체로, Promise와는 다르다.
notion image
  • 특정 조건 찾기
notion image
  • $lt $gte 등의 연산자를 사용해 찾을 수도 있다.
    • Movie.find({ year: {$gte: 2010}})
  • find로 찾으면 배열 안에 담기는데, findOne으로 영화 하나만 찾을 수도 있다.
    • notion image
  • exec() 메서드는 thenable 객체가 아니라 완전한 Promise를 준다.
  • Express 앱에서는 findById 메서드를 특히 많이 사용하게 된다.
    • findOne({_id: id}) 와 같다.
    • 앱에서 루트나 매개변수 안에 id를 넣어 활용할 수 있다. id를 request body에 넣을 수도 있다.
 

383. Mongoose로 업데이트하기

  • updateOne , updateMany 메서드
    • 갱신된 정보를 반환하지 않고, 몇 개의 항목이 수정되었는지만 알려준다.
    • updateOne은 쿼리에 매치되는 첫 번째 항목을 우리가 제시한 것으로 갱신한다. $set 연산자는 필요없다.
    • notion image
      notion image
    • updateMany 여러 항목 수정하기
    • Movie.updateMany({title: {$in: ['Amadeus', 'Stand By Me']}}, {score: 10}).then(res => console.log(res))
 

384. Mongoose로 삭제하기

  • remove 를 실행하면 동작은 하지만, deleteOnedeleteMany 또는 bulkWrite 를 사용하라는 경고가 뜬다.
    • 역시 관련 정보나 문서를 반환하지 않는 메서드이다. 삭제된 개수만 반환한다.
    • Movie.deleteMany({year: {$gte: 1999}}).then(msg => console.log(msg))
  • findOneAndRemove, findByIdAndRemove
    • 삭제된 데이터가 필요하다면 이 두 메서드를 사용한다.
    • notion image
 
💡
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);
  });
notion image
  • 스키마에 없는 정보를 추가하면 해당 항목은 무시된다.
 

386. 추가 스키마 제약 조건

https://mongoosejs.com/docs/schematypes.html#schematype-options
  • 개별 타입에 적용할 수 있는 특정 제약 조건도 있다.
  • match로 정규 표현식을 전달할 수도 있다. 정규 표현식을 쓰지 않아도 되도록 해주는 패키지도 있음
notion image
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}`)
}
콘솔에서 Product의 새 인스턴스를 만들고 greet 메서드를 호출해보면 잘 작동한다.
콘솔에서 Product의 새 인스턴스를 만들고 greet 메서드를 호출해보면 잘 작동한다.
  • 특정 인스턴스를 찾을 때도 인스턴스 메서드에 엑세스할 수 있다.
const findProduct = async () => {
    const foundProduct = await Product.findOne({ name: 'Bike Helmet' });
    foundProduct.greet();
}
  • 이런 류의 인스턴스 메서드를 사용할 일이 많을 것
    • 인증(authentication) 작업 시 사용자(User) 모델이 있을 것이고, 사용자에겐 이메일, 비밀번호 같은 정보들이 있으면 checkCredentialscheckAuthenticate , 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);
person.js
  • 인스턴스 메서드를 추가하는 것과는 다르다. setter와 getter를 정의할 수 있다.
notion image
  • person이 자동으로 복수형 people로 변환되어 있다.
notion image
  • fullName 특성은 없는 것을 확인할 수 있다. (가상 특성)
notion image
 

392. Mongoose를 미들웨어로 정의하기

  • Mongoose는 특정 작업 전후에 코드를 실행할 수 있게 해준다.
    • .post 미들웨어는 init, validate, save, remove 등 어떤 메서드이든 뒤에서 실행
    • .pre 미들웨어는 어떤 메서드이든 앞에서 실행된다.
  • 이를 작동시키려면 콜벡에서 next 라는 매개변수를 수용하고, 함수 마지막에서 next() 를 실행해야 한다.
https://mongoosejs.com/docs/middleware.html#pre
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!!!!")
})
notion image
notion image
 

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;
      models/product.js
  • index.js에서 DB 연결
    • // 모델 요청
      const Product = require('./models/product');
      index.js
  • 데이터베이스에 초기 데이터를 심기 위해 시드 파일 생성
    • 웹 앱과는 별개로 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);
        });
      seeds.js
 

396. 프로덕트 인덱스

  • 라우트를 위한 비동기 콜백 패턴
    • app.get('/products', async (req, res) => {
        const products = await Product.find({});
        res.render('products/index', { products })
      });
      index.js
      <ul>
          <% for (let product of products) { %>
          <li><%= product.name %></li>
          <% } %> 
      </ul>
      products/index.ejs
 

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">
    • app.post에 /products 입력
    • // CREATE
      app.post('/products', async (req, res) => {
        const newProduct = new Product(req.body); // 새로운 상품 생성
        await newProduct.save(); // 저장 기다렸다가
        res.redirect(`/products/${newProduct._id}`) // 리다이렉트
      })
    • req.body에 접근하기 위해 Express 미들웨어로 파싱
    • 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">
      products/edit.ejs
      // 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와 데이터를 전달한다.
    • 디폴트로 유효성 검사가 있으므로 세 번째 options 인수로 runValidators를 true로 설정한다. 또 기존 정보 대신 수정된 문서를 리턴하기 위해 new를 true로 설정한다.
 

400. 카테고리 셀렉터 위의 탄젠트

  • 상품 편집 시 해당 상품의 카테고리가 선택되어 있도록 하기 (selected)
    • <select name="category" id="category">
          <% for(let category of categories){ %>
          <option value="<%=category%>" <%= product.category === category ? 'selected': '' %>><%=category%></option>
          <% } %>
      </select>
      products/edit.ejs
 

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>
      products/show.ejs
    • 쿼리스트링이 없으면 All Products, 있으면 해당하는 카테고리 상품을 필터링해서 보여준다.
    • index.js 수정
      index.js 수정
      <%if(category !== 'All') {%>
      <a href="/products">All Products</a>
      <% } %>
      products/index.ejs
       
       

       
       
      본 스터디는 Udemy의 <【한글자막】 The Web Developer 부트캠프 2022> 강의를 활용해 진행됐습니다. 강의에 대한 자세한 정보는 아래에서 확인하실 수 있습니다.
       
       
      프밍 스터디는 Udemy Korea와 함께 합니다.
       
       

       
원하는 스터디가 없다면? 다른 스터디 개설 신청하기
누군가 아직 원하는 스터디를 개설하지 않았나요? 여러분이 직접 개설 신청 해 주세요!
이 포스트는
"웹 개발 실전 프로젝트" 스터디의 진행 결과입니다
진행중인 스터디
웹 개발 실전 프로젝트
JavaScript를 이용한 DOM 조작으로 프론트엔드 분야를 배우고, Node JS와 Express, DB, HTTP, REST API 등 백앤드 지식을 학습할수 있습니다. 이론만 공부하는 스터디가 아니라 13개 이상의 프로젝트로 이루어진 실습 위주의 커리큘럼으로 개인 프로젝트 결과물을 가져갈 수 있다는 메리트가 있습니다.
조재홍 멘토
이로운 개발자가 되고 싶은 조재홍입니다.