관리 메뉴

거니의 velog

(18) mongoose를 이용한 MongoDB 연동 실습 7 본문

React_백엔드 프로그래밍

(18) mongoose를 이용한 MongoDB 연동 실습 7

Unlimited00 2024. 2. 21. 11:37

8. 데이터 삭제와 수정

(1) 데이터 삭제

* 이번에는 데이터를 삭제해 보겠다. 데이터를 삭제할 때는 여러 종류의 함수를 사용할 수 있다.

(1) remove() : 특정 조건을 만족하는 데이터를 모두 지운다.

(2) findByIdAndRemove() : id를 찾아서 지운다.

(3) findOneAndRemove() : 특정 조건을 만족하는 데이터 하나를 찾아서 제거한다.

* 우리는 위 함수 중 findByIdAndRemove() 를 사용하여 데이터를 제거해 볼 것이다.

[src/api/posts/posts.ctrl.js]

/*
  DELETE /api/posts/:id
*/
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);
  }
};

* 코드를 저장하고, Postman으로 조금 전 GET 요청을 했던 주소에 DELETE 요청을 해 보자.

DELETE http://localhost:4000/api/posts/65d49b76025f0b389c04bf2a

remove 구현 완료 : 데이터가 삭제 되어 No Content 상태이므로 아무 것도 뜨지 않는 것이 정상이다.

* 그 다음에 똑같은 주소로 GET 요청을 해 보면, 404 오류가 발생하면서 'Not Found'라는 문구가 응답될 것이다.


(2) 데이터 수정

* 마지막으로 update 함수를 구현해 보자. 데이터를 업데이트할 때는 findByIdAndUpdate() 함수를 사용한다. 이 함수를 사용할 때는 세 가지 파라미터를 넣어 주어야 한다. 첫 번째 파라미터는 id, 두 번째 파라미터는 업데이트 내용, 세 번째 파라미터는 업데이트의 옵션이다.

[src/api/posts/posts.ctrl.js]

/*
  PATCH /api/posts/:id
  {
    title: '수정',
    body: '수정 내용',
    tags: ['수정', '태그']
  }
*/
export const update = async (ctx) => {
  const { id } = ctx.params;
  try {
    const post = await Post.findByIdAndUpdate(id, ctx.request.body, {
      new: true, // 이 값을 설정하면 업데이트된 데이터를 반환합니다.
      // false 일 때에는 업데이트 되기 전의 데이터를 반환합니다.
    }).exec();
    if (!post) {
      ctx.status = 404;
      return;
    }
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

* 마지막 API 구현이 완료되었다. 잘 되는지 한번 확인해 보자. 조금 전 GET 요청을 했던 id를 가진 포스트는 현재 삭제되었으니, 다시 GET /api/posts 요청을 해서 유효한 id 값을 복사하자. 그리고 해당 id를 가진 포스트를 업데이트해 보자.

* PATCH 메서드는 데이터의 일부만 업데이트해도 되므로, body에는 title만 넣어 보겠다.

PATCH http://localhost:4000/api/posts/65d49ba8025f0b389c04bf2c

{
	"title" : "수정"
}

update 구현 완료

* 제목이 잘 바뀐 것을 확인할 수 있다.

* 축하한다! 이제 여러분은 MongoDB를 연동한 REST API를 개발할 수 있게 되었다!


9. 요청 검증

* 이 절에서는 요청을 검증하는 방법을 알아보자. 앞서 read API를 실행할 때, id가 올바른 ObjectId 형식이 아니면 500 오류가 발생했다. 500 오류는 보통 서버에서 처리하지 않아 내부적으로 문제가 생겼을 때 발생한다.

* 잘못된 id를 전달했다면 클라이언트가 요청을 잘못 보낸 것이니 400 Bad Request 오류를 띄워 주는 것이 맞다. 그러려면 id 값이 올바른 ObjectId 인지 먼저 확인해야 하는데, 이를 검증하는 방법은 다음과 같다.

import mongoose from 'mongoose';

const { ObjectId } = mongoose.Types;
ObjectId.isValid(id);

* 지금 ObjectId를 검증해야 하는 API는 read, remove, update 이렇게 세 가지이다. 모든 함수에서 이를 검증하기 위해 검증 코드를 각 함수 내부에 일일이 삽입한다면 똑같은 코드의 중복이 발생할 것이다.

* 코드를 중복해 넣지 않고, 한 번만 구현한 다음 여러 라우트에 쉽게 적용하는 방법이 있다. 바로 미들웨어를 만드는 것이다. post.ctrl.js의 코드 상단에 다음 미들웨어를 작성해 주자.

[src/api/posts/posts.ctrl.js]

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

const { ObjectId } = mongoose.Types;

export const checkObjectId = (ctx, next) => {
  const { id } = ctx.params;
  if (!ObjectId.isValid(id)) {
    ctx.status = 400; // Bad Request
    return;
  }
  return next();
};

(...)

* 그리고 src/api/posts/index.js에서 ObjectId 검증이 필요한 부분에 방금 만든 미들웨어를 추가하자.

[src/api/posts/index.js]

import Router from 'koa-router';
import * as postsCtrl from './posts.ctrl.js';

const posts = new Router();

posts.get('/', postsCtrl.list);
posts.post('/', postsCtrl.write);

const post = new Router(); // /api/posts/:id
post.get('/', postsCtrl.read);
post.delete('/', postsCtrl.remove);
post.patch('/', postsCtrl.update);

posts.use('/:id', postsCtrl.checkObjectId, post.routes());

export default posts;

* /api/posts/:id 경로를 위한 라우터를 새로 만들고, posts에 해당 라우터를 등록해 주었다. 이렇게 하면 중복되는 코드가 별로 없어서 깔끔하지만, 라우트 경로들이 한눈에 들어오지 않으므로 취향에 따라서는 불편하게 느낄 수도 있다. 이러한 방식이 불편하다면 굳이 이렇게까지 리팩토링하지 않아도 상관없다.

* 코드를 다 작성했으면 GET /api/posts/:id 요청을 할 때 aaaaa와 같이 일반 ObjectId의 문자열 길이가 다른, 잘못된 id를 넣어 보자. 500 대신에 400 Bad Request라는 에러가 발생할 것이다.


(2) Request Body 검증

* 이제 write, update API 에서 전달받은 요청 내용을 검증하는 방법을 알아보자. 포스트를 작성할 때 서버는 title, body, tags 값을 모두 전달받아야 한다. 그리고 클라이언트가 값을 빼먹었을 때는 400 오류가 발생해야 한다. 지금은 따로 처리하지 않았기 때문에 요청 내용을 비운 상태에서 write API를 실행해도 요청이 성공하여 비어 있는 포스트가 등록된다.

* 객체를 검증하기 위해 각 값을 if문으로 비교하는 방법도 있지만, 여기서는 이를 수월하게 해 주는 라이브러리인 Joi를 설치하여 사용해 보겠다.

https://github.com/hapijs/joi

 

GitHub - hapijs/joi: The most powerful data validation library for JS

The most powerful data validation library for JS. Contribute to hapijs/joi development by creating an account on GitHub.

github.com

* yarn으로 Joi를 설치해 주자.

$ yarn add @hapi/joi

* 그 다음에는 write 함수에서 Joi를 사용하여 요청 내용을 검증해 보자.

[src/api/posts/posts.ctrl.js]

import Post from '../../models/post';
import mongoose from 'mongoose';
import Joi from 'joi';

(...)

/*
  POST /api/posts
  {
    title: '제목',
    body: '내용',
    tags: ['태그1', '태그2']
  }
*/
export const write = async (ctx) => {
  const schema = Joi.object().keys({
    // 객체가 다음 필드를 가지고 있음을 검증
    title: Joi.string().required(), // required() 가 있으면 필수 항목
    body: Joi.string().required(),
    tags: Joi.array().items(Joi.string()).required(), // 문자열로 이루어진 배열
  });

  // 검증 후, 검증 실패시 에러처리
  const result = Joi.validate(ctx.request.body, schema);
  if (result.error) {
    ctx.status = 400; // Bad Request
    ctx.body = result.error;
    return;
  }

  const { title, body, tags } = ctx.request.body;
  const post = new Post({
    title,
    body,
    tags,
  });
  try {
    await post.save();
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

* 설정을 마쳤다. write API를 호출할 때 Request Bosy에 필요한 필드가 빠져 있다면 400 오류를 응답하게 되는데, 응답 내용에 에러를 함께 반환한다. 직접 tags 배열을 제외하고 API 요청을 한번 해 보자.

POST http://localhost:4000/api/posts

{
    "title" : "제목",
    "body" : "내용"
}

* 응답이 다음과 같이 나타난다.

{
    "isJoi": true,
    "name": "ValidationError",
    "details": [
        {
            "message": "\"tags\" is required",
            "path": [
                "tags"
            ],
            "type": "any.required",
            "context": {
                "key": "tags",
                "label": "tags"
            }
        }
    ],
    "_object": {
        "title": "제목",
        "body": "내용"
    }
}

* write API를 수정한 뒤에 update API의 경우도 마찬가지로 Joi를 사용하여 ctx.request.body를 검증해 주자. write API에서 한 것과 비슷하지만, 여기서는 .required()가 없다.

/*
  PATCH /api/posts/:id
  {
    title: '수정',
    body: '수정 내용',
    tags: ['수정', '태그']
  }
*/
export const update = async (ctx) => {
  const { id } = ctx.params;

  // write 에서 사용한 schema 와 비슷한데, required() 가 없습니다.
  const schema = Joi.object().keys({
    title: Joi.string(),
    body: Joi.string(),
    tags: Joi.array().items(Joi.string()),
  });

  // 검증 후, 검증 실패시 에러처리
  const result = Joi.validate(ctx.request.body, schema);
  if (result.error) {
    ctx.status = 400; // Bad Request
    ctx.body = result.error;
    return;
  }

  try {
    const post = await Post.findByIdAndUpdate(id, ctx.request.body, {
      new: true, // 이 값을 설정하면 업데이트된 데이터를 반환합니다.
      // false 일 때에는 업데이트 되기 전의 데이터를 반환합니다.
    }).exec();
    if (!post) {
      ctx.status = 404;
      return;
    }
    ctx.body = post;
  } catch (e) {
    ctx.throw(500, e);
  }
};

* 이렇게 수정하면, 다음과 같이 문자열을 전달해야 하는 title 값에 숫자를 넣을 경우 에러가 나타날 것이다.

PATCH http://localhost:4000/api/posts/65d49bb3025f0b389c04bf2e

{
	"title" : 123123
}

* 응답은 다음과 같이 나타난다.

{
    "isJoi": true,
    "name": "ValidationError",
    "details": [
        {
            "message": "\"title\" must be a string",
            "path": [
                "title"
            ],
            "type": "string.base",
            "context": {
                "value": 123123,
                "key": "title",
                "label": "title"
            }
        }
    ],
    "_object": {
        "title": 123123
    }
}