TIL 22-05-16 (2)

thisisyjin·2022년 5월 16일
0

TIL 📝

목록 보기
50/113

React.js

Today I Learned ... react.js

🙋‍♂️ Reference Book

🙋‍ My Dev Blog


CH 22. mongoDB

  • mongoDB
  • mongoose

데이터베이스

  • 서버 개발시 데이터베이스 사용시 웹 서비스에서 사용되는 데이터를 저장하고, 효율적으로 조회 및 관리 가능.
  • 기존에는 RDBMS를 자주 사용했음. (MySQL, OracleDB 등)
    -> 데이터 스키마가 고정적이여서 기존 데이터를 모두 수정해야 새 데이터를 등록할 수 있었음.

✅ 스키마란?

  • 데이터베시으세 어떤 형식의 데이터를 넣을지에 대한 정보.
    -> 회원 정보 스키마라면? 계정명, 이메일, 이름 등
  • 또한, 데이터 양이 커졌을 때의 확장성의 문제도 있었음. (여러 컴퓨터에 분산시킬 수 없었음)

mongoDB

  • mongoDB는 위와 같은 기존 RDBMS의 한계를 극복한 문서 지향적 NoSQL데이터베이스임.
  • 유동적인 스키마를 지닐 수 있음.
  • 기존 데이터까지 수정할 필요는 X.
  • 데이터 양이 많아져도 분산해서 처리 가능 (확장성)

데이터의 구조가 자주 바뀌면 - mongoDB가 유리
데이터를 필터링해야하거나, ACID 규칙을 지켜야 하면 - RDBMS가 유리

🔎 용어 설명

  • ACID = 원자성/일관성/고립성/지속성
    -> DB 트랜잭션이 안전하게 처리되는 것을 보장하기 위한 성질.
  • 트랜잭션 = 데이터베이스의 상태를 변경시키기 위해 수행하는 작업 단위.
    = SELECT, UPDATE, INSERT, DELETE

문서(document)

  • 레코드(record)와 비슷한 개념.
  • 한 개 이상의 key-value 쌍으로 구성됨.
{
  "_id": ObjectID("4058293492938493f123a81"),
  "username": "thisisyjin",
  "name": { first: "Yeonjin", last: "Lee" }
}

-> 문서는 BSON (바이너리 형태의 JSON)로 저장됨.

새로운 문서를 만들면 _id 라는 고유값을 자동으로 생성함.
-> 시간 / 머신 아이디 / 프로세스 아이디 / 순차번호로 되어있음.
-> 즉, _id는 고유한 값임.

여러 문서가 들어있는 곳을 컬렉션이라고 함.
-> cf) RDBMS에서는 테이블 개념을 사용함. 각 테이블에서 같은 스키마를 가져야 함.
-> MongoDB에서는 다른 스키마를 가진 문서들이 한 컬렉션에 있을 수 있음.

// 컬렉션

// 문서
{
  "_id": ObjectID("4058293492938493f123a81"),
  "username": "thisis"
},
// 문서
{
  "_id": ObjectID("405829cafdf8493f123f3f6"),
  "username": "yjin",
  "tel": "010-1234-1234"
},  

-> 첫번째 문서와 두번째 문서의 스키마는 일치하지 않아도 OK.


MongoDB 구조

  • 서버 하나에 DB를 여러개 가질 수 있음.
  • 각 DB는 여러개의 컬렉션으로 구성됨.
  • 컬렉션 내부에는 문서들이 들어있음.

스키마 디자인

  • RDBMS의 경우에는 각 포스트와 댓글마다 테이블을 만들고, JOIN 함.
  • 반면, NoSQL 에서는 모든 것을 문서 하나에 넣음.
{
  _id: ObjectId,
  title: String,
  body: String,
  username: String,
  createdDate: Date,
  comments: [
    {
      _id: ObjectID,
      text: String,
      createdDate: Date,
    },
  ],
};
  • 포스트 문서 내부에 댓글(comments)을 넣음.

문서 내부에 또다른 문서가 위치 = 서브다큐먼트


mongoDB 서버

mongoDB 서버 설치

  • mongoDB 서버를 이용하려면, 설치부터 해야함.

macOS에서는 Homebrew로 설치 가능!

$ brew tap mongodb/brew
$ brew install mongodb-community@4.2
$ brew services start mongodb-community@4.2

# Successfully started `mongodb-community@4.2`

참고 - 나는 여기서 설치가 제대로 되지 않아 (mongo 명령어 안됨)
아래와 같이 재설치해줌.

$ brew tap mongodb/brew
$ brew install mongodb-community@5.0
$ brew services start mongodb-community@5.0

mongoDB 작동 확인

http://localhost:27017/ 에 접속한 후

위와 같은 문구가 뜨면 제대로 작동하는 것임.

+) mongo 명령어 작성시 터미널 기반 mongoDB 클라이언트가 실행됨.
여기서 version() 이라는 명령어를 입력해보기.
-> 정상작동!


mongoose 설치

  • mongoose는 Node.js 환경에서 MongoDB 기반 ODM(Object Data Modeling) 라이브러리임.
    -> 객체 데이터 모델링. DB 문서들을 js 객체처럼 사용할 수 있게 해줌.
  • 지난 시간에 진행했던 blog 프로젝트를 이어서 진행함.

mongoose, dotenv 설치

$ yarn add mongoose dotenv
  • dotenv : 환경변수들을 파일에 넣고 사용할 수 있게 해줌.
  • mongoose로 MongoDB에 접속할 때, 서버에 주소 or 계정, 비밀번호가 필요한 경우 환경변수로 설정하는 것이 좋음.
    -> 민감하거나 환경별로 달라질 수 있으므로 코드 안에 직접 작성하지 X.

참고로, 깃헙에 업로드시 .gitignore 을 추가하여 환경변수가 들어있는 파일은 제외시켜야 함.

.env 환경변수 파일

  • /blog-backend (루트)에 환경변수 파일 .env 생성.
  • 서버에서 사용할 포트 / MongoDB 주소를 넣어줌.
PORT=4000
MONGO_URI=mongodb://localhost:27017/blog

-> 띄어쓰기 금지 / 세미콜론이나 컴마로 구분할 필요 X

mongodb://localhosst:27017/blog에서 'blog'는 우리가 사용할 DB의 이름이다.


src/index.js 수정

  • dotenv를 불러와서 config()함수 호출.
// 🔻 최상단에서 config() 호출함. 
require('dotenv').config();
const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');

// 🔻 process.env 내부 값에 대한 레퍼런스 (PORT)
const { PORT } = process.env;

const api = require('./api');

const app = new Koa();
const router = new Router();

router.use('/api', api.routes()); // api 라우트 적용

// 라우터 적용 전에 미들웨어를 적용해야 함
app.use(bodyParser());

// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());

// 🔻 port값 설정 
const port = PORT || 4000;
app.listen(port, () => {
  console.log('Listening to port %d', port);
});

참고 : 자바스크립트에서 %d란 ?

  • 문자열 내부에서 %d나 %s 등을 사용하면 원하는 값 대입 가능.
    (C언어의 printf와 같이)
    = console.log('Listening to port ' + port) 와 같음.
  • 템플릿 리터럴의 경우 es6 문법이라 따로 바벨같은 트랜스파일러가 필요함.

.env 파일에서 PORT를 4001로 바꾼 후 서버를 재시작해보면 다음과 같다.


mongoose로 서버와 DB 연결

  • mongoose의 connect() 함수를 이용함.

src/index.js 수정

require('dotenv').config();
const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');
const mongoose = require('mongoose');

const api = require('./api');

// process.env 내부 값에 대한 레퍼런스
const { PORT, MONGO_URI } = process.env;

mongoose
  .connect(MONGO_URI)
  .then(() => {
    console.log('Connected to MongoDB');
  })
  .catch((e) => {
    console.error(e);
  });


// 🔽 이하 동일
const app = new Koa();
const router = new Router();

router.use('/api', api.routes()); // api 라우트 적용

// 라우터 적용 전에 미들웨어를 적용해야 함
app.use(bodyParser());

// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());

const port = PORT || 4000;
app.listen(port, () => {
  console.log('Listening to port ' + port);
});

🔽 결과


esm

  • ES 모듈 import/export 문법은 아직은 Node.js 환경에서 정식 지원하지 않음.
  • 단, 확장자를 .mjs로 하고 --experimental-modules 옵션을 넣어주면 가능

-> esm 이라는 라이브러리를 사용하면 쉽게 import/export 를 사용할 수 있음.

$ yarn add esm

eslint 설정

  1. 기존 src/index.js 를 main.js 로 이름을 변경한 후,
    index.js를 새로 생성.

index.js

/* eslint-disable no-global-assign */
require = require('esm')(module /*, options*/);
module.exports = require('./main.js');

no-global-assign 이란, JS 예약어를 변수명으로 사용할 수 없는 옵션이다.
이를 무시하기 위해 주석을 추가해줘야 한다.

  1. package.json 수정
  "scripts": {
    "start": "node -r esm src",
    "start:dev": "nodemon --watch src/ -r esm src/index.js"
  }

-r esm src를 추가해준다.

  1. eslintrc.json 수정
  "parserOptions": {
    "ecmaVersion": 2018,
    "sourceType": "module"
  },

-> sourceType 옵션을 추가해줌.

  • import/export를 사용해도 오류가 발생하지 않도록.

기존 코드 변경

  • 기존 exports 코드를 export const로 변경해줌.
  • 기존 module.exports 코드를 export default로 변경해줌.
  • 기존 require 코드를 import로 변경해줌.

단, main.js 에서 require('dotenv').config() 는 import로 바꾸지 ❌

전부 바꿨다면, postman으로 요청을 보내 에러가 없는것을 확인함.

jsconfig.json

프로젝트 루트 디렉터리(/blog-backend)에 jsconfig.json 파일을 생성.

{
  "compilerOptions": {
    "target": "es6",
    "module": "es2015"
  },
  "include": ["src/**/*"]
}

-> 자동 완성을 통해 모듈을 불러올 수 있음.

vsCode에서는 import/export 문법 사용시
자동으로 import문을 불러온다. (require은 X)


DB 스키마, 모델

mongoose에는 스키마(schema), 모델(model)이라는 개념이 있음.

스키마모델
문서 내부 필드가 어떤 형식인지 정의스키마를 사용하여 만드는 인스턴스.
실제 작업을 처리할 수 있는 함수들을 지닌 객체.
{ title: String, date: Date ... }mongoose.model(...)

DB는 모델(model)을 이용하여 데이터를 읽고 쓴다.


1) 스키마 생성

  • 모델을 만드려면 먼저 스키마를 생성해야 함.

블로그 포스트의 경우에는 어떤 데이터가 필요할지?

  1. 제목
  2. 내용
  3. 태그
  4. 작성일
필드명데이터 타입설명
titleString제목
bodyString본문
tagsArrayOf(String)태그
publishedDateDate작성일

src/models 디렉터리 생성 후, post.js 작성.

import mongoose from 'mongoose';

const { Schema } = mongoose;

const PostSchema = new Schema({
  title: String,
  body: String,
  tags: [String],
  publishedDate: {
    type: Date,
    default: Date.now,
  },
});
  • 스키마를 만들 때는 mongoose의 Schema를 이용하여 정의.
  • 각 필드 이름과 데이터 타입 정보가 들어간 객체를 인자로 줌.
  • 필드의 기본값은 default값을 설정해줌.

  • 문자열로 된 배열 = [String] 과 같이 나타냄.

❗️ 참고 - Schema에서 지원하는 Type

  • String / Number / Date / Buffer / Boolean / Mixed / ObjectId / Array
  • Mixed는 어떤 데이터도 넣을 수 있는 형식. (Schema.Types.Mixed)
  • ObjectId는 객체 아이디. (주로 다른 객체 참조시)
  • Array는 [ ]로 감싸서 사용.

예>

const BookSchema = new Schema({
    title: String,
    desc: String,
    authors: [AuthorSchema], // 다른 객체를 배열로 참조함
    meta: {
        likes: Number,
    },
    extra: Schema.Types.Mixed
})

-> authors: [AuthorSchema] 는 Author스키마로 이루어진 여러개의 객체가 들어있는 배열을 의미함.


2) 모델 생성

  • mongoose.model() 함수 이용

post.js 수정

import mongoose from 'mongoose';

const { Schema } = mongoose;

const PostSchema = new Schema({
  title: String,
  body: String,
  tags: [String],
  publishedDate: {
    type: Date,
    default: Date.now,
  },
});

// 🔻 추가
const Post = mongoose.model('Post', PostSchema);

export default Post;
  • 모델 인스턴스를 만들고, export default를 통해 내보냄.
  • model() 함수의 첫번째 인자는 스키마 이름 'Post'이고,
    두번째 인자는 스키마 객체임. (=식별자)

컬렉션 명

  • 스키마 명을 Post로 정해주면 컬렉션 명은 posts가 된다.
    -> 실제 DB에 만드는 컬렉션 이름을 posts로 지어줌.
  • 만약 위와 같이 컨벤션을 따르고 싶지 않다면, mongoose.model()의 세번째 인자로 원하는 이름을 넣어줌.

첫번째 인자인 Posts는 다른 스키마에서 현재 스키마를 참조해야 하는 경우 사용함.


mongoDB Compass

  • mongoDB를 위한 GUI 프로그램.
  • DB를 더 쉽게 조회 및 수정 가능.

다운로드 링크


데이터 생성과 조회

1) 데이터 생성

  • post의 인스턴스 생성시 new 키워드 사용.
  • 생성자 함수의 인자로 객체를 넣어줌.
  • 인스턴스를 만들면 바로 DB에 저장? (x)
    -> save() 함수를 실행해야 DB에 저장되는 것임.
    -> save 함수의 리턴값은 Promise 이므로, await을 사용하여 대기할 수 있음.

참고 - async/await 문을 사용할 때는 try/catch 문으로 오류처리를 해줘야 함.

/src/api/posts/posts.ctrl.js 를 새로 작성.

import Post from '../../models/post';

export const write = async (ctx) => {
  const { title, body, tags } = ctx.reqest.body;
  const post = new Post({ title, body, tags });

  try {
    await post.save();
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

... // 추후 작성
  1. postman으로 request를 보내보자. (POST)

-> send를 누를때마다 _id 값이 다르게 나옴.

  1. mongoDB Compass에서 확인
  • blog 데이터베이스가 나타남.

    .env 파일에서 URI를 MONGO_URI=mongodb://localhost:27017/blog로 해주었으므로 마지막에 'blog'가 DB의 이름이 된다.


-> 아까 POST로 등록했던 데이터들이 나타남.


2-1) 데이터 조회

export const list = async (ctx) => {
  try {
    const posts = await Post.find().exec();
    ctx.body = posts;
  } catch (e) {
    ctx.throw(500, e);
  }
};

참고 - mongoose의 find() 함수

Model.find()  // 쿼리 인스턴스를 리턴
Model.find().exec()  // 쿼리 이용시 Promise를 리턴받고 싶다면 - 즉, 서버에 쿼리 요청하려면

2-2) 특정 포스트 조회

특정 id를 가진 데이터 조회시 findById() 함수 사용.

export const read = async (ctx) => {
  const { id } = ctx.params;
  try {
    const post = await Post.findById(id).exec();
    if (!post) { // 존재하지 않으면 undefined
      ctx.status = 404;
      return;
    }
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};
  • ctx.params는 해당 url에 들어가는 id.
  • 위에서 조회했을 때 나왔던 id중 하나를 복사해 넣어보면 됨.

  • 만약 id가 존재하지 않다면 404 에러를 발생시킴. -> Not Found
  • 문자열을 몇개 지우고 요청시 500 에러 (=서버 오류) 발생시킴. -> Internal Server Error
    -> id가 ObjectId가 아니기 때문.

참고 - find().exec()란? (스택오버플로우 답변)


데이터 삭제와 수정

1) 데이터 삭제

삭제하려면 여러 종류의 함수를 이용할 수 있음!

  • remove() : 특정 조건 만족하는 데이터 모두 삭제
  • findOneAndRemove() : 특정 조건 만족하는 데이터 하나만 제거
  • findByIdAndRemove() : id를 찾아서 삭제

이중에서 findByIdAndRemove()를 사용해보자.

export const remove = async (ctx) => {
  const { id } = ctx.params;
  try {
    await Post.findByIdAndRemove(id).exec();
    ctx.status = 204; // No Content
  } catch (e) {
    ctx.throw(500, e);
  }
};

DELETE 요청으로 이미 삭제한 id를 GET으로 불러오려 하면, 404 NOT FOUND 오류가 뜬다.


2) 데이터 수정

findByIdAndUpdate() 함수를 사용함.
-> 세가지 파라미터 필요.

  1. id
  2. 업데이트 내용
    -> ctx.request.body
  3. 업데이트 옵션
    -> { new: true } : 업데이트된 데이터 반환. (false면 업데이트 이전 데이터 반환)
export const update = async (ctx) => {
  const { id } = ctx.params;
  try {
    const post = await Post.findByIdAndUpdate(id, ctx.request.body, {
      new: true, // 옵션 - 업데이트된 데이터 반환. 
    }).exec();
    
    if (!post) {
      ctx.status = 404;
      return;
    }
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

postman에서 PATCH 요청을 해보자.
-> 변경할 필드만 작성하면 된다.

profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글