Express-mongoDB로 CRUD API 만들기(02)

해당 시리즈는 한양대학교 정보시스템학과 2021학년도 2학기 데이터베이스시스템 수업 및 시험 대체 과제로 기획되었습니다.
  1. 시즌 399429호 블로그 엔진 만들기 결심
  2. 힙하게 Typescript 보일러플레이트 구성하기
  3. Express로 CRUD api 만들기 (01)

mongoDB란?

mongoDB는 NoSQL 데이터베이스 중 하나로, Document 방식을 택하고 있다. Document는 데이터를 Key Value로 나누어 저장하는 방식으로, 다음과 같은 모습이다.

Key Value
name 홍길동
age 20

Document들이 여러개 모인 집합은 Collection이라고 한다.
이는 json의 저장 방식과 동일하기 때문에, Object 방식으로 데이터를 관리하는 언어에서 사용하기 좋다.

사용하기

본 내용은 express CRUD api 만들기에서 이어진다.

node에서 mongoDB에 접근하기 위해 mongoose 라이브러리를 사용하자.
dotenv는 환경변수를 사용하기 위해 사용한다.

yarn add mongoose dotenv

환경변수를 .env에 다음과 같이 설정한다. 나는 mongoDB를 로컬에서 사용하고 있기 때문에, mongodb://localhost:2653/users로 접근하도록 했다. (2653은 mondoDB 실행시 내가 설정한 포트다)

# .env
# port number that the server will listen on
PORT=3624
# MongoDB URI
# MONGO_URI=mongodb://localhost:<port>/<db-name>
MONGO_URI=mongodb://localhost:2653/users

index.ts를 다음과 같이 작성하여 mongoose가 mongoDB에 접속할 수 있도록 한다.

// index.ts
...
app.use(express.static('public'))
app.use(express.urlencoded({ extended: true }))
app.use(express.json())

mongoose
  .connect(process.env.MONGO_URI, { useNewUrlParser: true }) // Connect to MongoDB
  .then(() => console.log('MongoDB Connected'))
  .catch((err: Error) => console.log(err))
...

스키마(Schema)

RDBMS에서 Schema란 데이터베이스를 구성하는 레코드의 크기, Key, Record간의 관계, 검색 방법 등을 정의한 것이다.

MongdoDB는 RDBMS와는 달리 고정 Schema라는 개념이 존재하지 않는다. 즉, 같은 Collection 내에 있어도 Document는 서로 다른 Schema를 가질 수 있다.

덕분에 자유도가 높다는 장점이 있지만 동시에 명시적인 구조가 없어 어떤 필드가 어떤 데이터 타입으로 저장되어야 하는 지 알기 어렵다는 단점도 갖는다. 이를 해결하기 위해 Mongoose는 고정 Schema를 사용한다.

Mongoose의 Schema는 MongoDB의 Document에 저장되는 Data 구조를 JSON 형태로 정의하는데, 이는 RDBMS의 테이블 정의와 비슷하다.

mongoose Scheme는 다음의 자료형들을 지원한다.

Data Types Description
String 표준 자바스크립트와 Node.js의 문자열 type
Number 표준 자바스크립트와 Node.js의 숫자 type
Date ISODate format data type
Buffer Node.js의 binary type(이미지, PDF, 아카이브 등)
Boolean 표준 자바스크립트와 Node.js의 불리언 type
Array 표준 자바스크립트와 Node.js의 배열 type
Scheme.types.ObjectId MongoDB의 ObjectId type
Scheme.types.Mixed 표준 자바스크립트와 Node.js의 모든 type

primary key인 _id는 자동으로 생성된다.

모델(Model)

Schema가 데이터의 구조를 정의했다면 Model은 실제 데이터를 담는 객체다.

with Typescript

Schema는 데이터 타입을 지정하는 역할을 하는데, db가 아닌 js(ts) 코드에서는 typescript가 제공하는 type 혹은 interface가 같은 역할을 한다. mongoose는 이러한 typescript의 type과 schema가 일치할 수 있도록 Schema 및 model을 생성할 때에 제너릭으로 타입을 지정할 수 있도록 해두었다.

먼저 User와 Post에 대한 type을 types 디렉토리에 만들어주자.

// types/User.ts
export interface User {
  userId: string;
  name: string;
  email: string;
  tier: string;
}
// types/Post.ts
import type { User } from './User'

export interface Post {
  url: string;
  title: string;
  content: string;
  imagePath: string;
  creator: User;
}

creator 필드는 User 객체를 향한 reference를 갖게 할 것이기 때문에 User 타입으로 지정해준다.

다음으로 User, Post에 대한 스키마와 모델을 생성하자.

// models/user.ts
import type { User } from '../types/User'
import { Schema, model } from 'mongoose'

// 스키마 정의
const userSchema = new Schema<User>({
  userId: { type: String, required: true },
  name: { type: String, required: true },
  email: { type: String, required: true },
  tier: { type: String, required: true }
})

// 모델 생성 및 export
export default model<User>('User', userSchema)
import { Schema, model } from 'mongoose'
import type { Post } from '../types/Post'

// 스키마 정의
const postSchema = new Schema<Post>({
  url: { type: String, required: true },
  title: { type: String, required: true },
  content: { type: String, required: true },
  imagePath: { type: String, required: true },
  creator: { type: Schema.Types.ObjectId, ref: 'User', required: true }
}, {
  timestamps: true
})

// 모델 생성 및 export
module.exports = model<Post>('Post', postSchema)

creator 필드의 정의에서 볼 수 있듯이 mongoose에서는 ref를 사용하여 foreign key를 설정하는 것이 가능하다.

Route 설정하기

모든걸 index.ts에 다 넣어둘 수는 없으니 routes 디렉토리에 posts.tsusers.ts를 만들어서 분리해주자.
이전에 만들었던 CRUD와 비슷하게 작성해준다.
데이터의 조회는 일단 mongoDB가 자동으로 생성해주는 objectId(_id)를 통해서 하도록 작성해두었다.

// routes/users.ts
import type { Request, Response } from 'express'
import { Router } from 'express'
import User from '../models/user'

const route = Router()

// get all users
route.get('/', async (req: Request, res: Response) => {
  const users = await User.find({})
  res.send(users)
})

// get user by id
route.get('/:id', async (req: Request, res: Response) => {
  const user = await User.findById(req.params.id)
  if (user) {
    res.send(user)
  } else {
    res.status(404).send({ message: `User with id ${req.params.id} not found` })
  }
})

// create user
route.post('/', async (req: Request, res: Response) => {
  const user = new User(req.body)
  await user.save()
  res.send(user)
})

// update user
route.put('/:id', async (req: Request, res: Response) => {
  const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true })
  if (user) {
    res.send(user)
  } else {
    res.status(404).send({ message: `User with id ${req.params.id} not found` })
  }
})

// delete user
route.delete('/:id', async (req: Request, res: Response) => {
  const user = await User.findByIdAndDelete(req.params.id)
  if (user) {
    res.send(user)
  } else {
    res.status(404).send({ message: `User with id ${req.params.id} not found` })
  }
})

export default Router
// routes/posts.ts
import type { Request, Response } from 'express'
import { Router } from 'express'
import Post from '../models/Post'

const route = Router()

// get all posts
route.get('/', async (req: Request, res: Response) => {
  const posts = await Post.find({})
  res.send(posts)
})

// get post by id
route.get('/:id', async (req: Request, res: Response) => {
  const post = await Post.findById(req.params.id)
  res.send(post)
})

// create post
route.post('/', async (req: Request, res: Response) => {
  const post = new Post(req.body)
  await post.save()
  res.send(post)
})

// update post
route.put('/:id', async (req: Request, res: Response) => {
  const post = await Post.findByIdAndUpdate(req.params.id, req.body, { new: true })
  res.send(post)
})

// delete post
route.delete('/:id', async (req: Request, res: Response) => {
  await Post.findByIdAndRemove(req.params.id)
  res.send({})
})

export default Router

Reference

  1. https://poiemaweb.com/mongoose
  2. https://velog.io/@y1andyu/TypeScript-Express-node.js-설정하기
  3. https://expressjs.com
  4. https://docs.mongodb.com/
  5. https://mongoosejs.com/

Subscribe to jiwon.me

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe