프로젝트에서 비밀번호 변경 기능을 구현하면서 user의 암호화된 password를 받아오려고 findOne 기능을 이용했습니다.

const user = await User.findOne({ userId });
const hashedPassword = user.password;

 

  이렇게 하다 보니 password 값만 받아오면 되는데 불필요하게 user의 모든 정보를 받아오는 것 같아서 바로 password를 받아올 수 없을까 고민했습니다.

const hashedPassword = await User.findOne({ userId }).password;

 

  그래서 이렇게 한 줄로 표현을 해봤지만 제대로 작동하지 않았습니다.

const hashedPassword = await User.findOne({ userId }, "password");

 

  하지만 이렇게 findOne 안의 중괄호 뒤에 원하는 필드 값을 넣어주면, 그 값을 받아올 수 있습니다. 참고로 "password" 대신 "-_id password" 라고 입력을 한다면, _id 부분을 제외하고 password 값만 객체로 받아옵니다. 어차피 객체에서 꺼내주는 작업을 해야하는 것은 똑같기 때문에 그냥 "password"라고 작성하겠습니다. 

// "password"를 입력했을 경우
{
  _id: new ObjectId('684e4ccc91757a381b66e7ad'),
  password: '$2b$10$sXCscjG5Wn6FKBEfn.lwF.NOVyAO/6EP5HDO.OBgn9GbXXCVzjIZS'
}

// "-_id password"를 입력했을 경우
{
  password: '$2b$10$sXCscjG5Wn6FKBEfn.lwF.NOVyAO/6EP5HDO.OBgn9GbXXCVzjIZS'
}

 

  console.log(hashedPassword)를 찍어보면 위와 같은 값이 return 된다는 것을 알 수 있습니다. 이 값안에는 mongoDB에서 자동으로 부여한 _id값과 password가 객체로 들어있으므로, 바로 사용할 수는 없고 password를 꺼내주는 작업을 해주어야 합니다.

const hashedPassword = await User.findOne({ userId }, "password")
  .lean()
  .then((user) => user?.password);

 

  이렇게 하면 user의 password만 가지고 와서 사용할 수 있습니다. findOne이 동기 처리 되어있기 때문에, then을 사용해서 findOne의 결과값을 받아온 후 optional chaining을 사용해 password 값을 가져옵니다. user가 존재하면 받아온 객체 안에 있는 password 값을 return해주고, user가 존재하지 않는다면 undefined 값을 반환해 줄 것입니다. 

  하나 더 참고할 것은 find나 findOne을 사용하면 필요한 정보뿐만 아니라 불필요한 정보도 다 가져오기 때문에, 서버의 부담을 줄이고 속도를 개선하기 위해서 간략한 정보만 가져오는 lean()을 사용합니다.

$2b$10$sXCscjG5Wn6FKBEfn.lwF.NOVyAO/6EP5HDO.OBgn9GbXXCVzjIZS

 

  다시 console.log(hashedPassword)를 찍어보면 위와 같이 password 값만 잘 받아온 것을 볼 수 있습니다.

1. 서론

   mongoose를 이용해서 팀 프로젝트를 시작했는데, 프로젝트에서 주어졌던 기능들을 다 구현하고 난 후에 추가 기능으로 게시판을 만들기로 하면서 게시판의 고유한 ID 값을 어떻게 부여해야 할 것인가 팀원들과 많은 의견을 나눴습니다. 회원 가입 후에 userId를 부여할 때는 nanoid를 사용하여 고유한 값을 넣어주면 되었지만, 게시판의 경우에는 게시글이 생성될 때마다 boardId에 난수값을 주는 것보다는 1, 2, 3 이렇게 순차적으로 부여하는 것이 직관적이지 않을까 해서 이런 방식으로 구현을 하기로 했습니다. 댓글의 경우에는 댓글이 생성될 때마다 boardId와 무관하게 commentId가 1씩 증가하는 것보다는, 게시글의 몇 번째 comment인지 알 수 있도록 boardId마다 commentId가 따로 부여되게 하는 것이 좋겠다고 생각해서 boardId와 commentId를 계획한 대로 생성할 수 있는 방법을 찾아보기 시작했습니다. 

1. boardId는 게시글이 생성될 때마다 1씩 순차적으로 부여한다. 예) boardId: 1, 2, 3, 4, ...
2. CommentId는 BoardId마다 따로 부여한다. 예) boardId: 1 - commentId 1, boardId2: commentId 1, ...

 

  같은 백엔드 팀원분이 코드를 짜주셨는데, 게시글을 생성할 때마다 boardId가 1씩 잘 증가했지만, 한 가지 문제점이 있었습니다. 만약에 게시글이 4개가 있고 boardId가 1, 2, 3, 4로 부여되었을 때, 3과 같은 중간값을 삭제하게 되면 1, 2, 4가 되어서 그 후에 게시글을 생성하면 1, 2, 4, 5, 6, 7 이렇게 중간값이 빈 채로 id값이 잘 생성이 되지만, 4와 같은 마지막 id를 삭제하고 다시 게시글을 등록하면 아무 일도 없던 것처럼 빈값이 없이 1, 2, 3, 4, 5, 6, 7, ... 이런 식으로 id값이 부여되었습니다. 정리하면 아래와 같습니다.

1. boardId 1, 2, 3, 4에서 3이 삭제될 경우 다음에 글을 올리면 boardId는 5가 된다. -> 1, 2, 4, 5
2. boardId 1, 2, 3, 4에서 4가 삭제될 경우 다음에 글을 올리면 boardId는 4가 된다. -> 1, 2, 3, 4

 

  따라서 기존에 짰던 코드를 적용하지 않고 다른 방식을 찾아보았고, mongoose에는 autoincrement 기능이 없다고 해서 이것저것 찾아보다가 mongoose-auto-increment라는 플러그인을 찾을 수 있었습니다.

 

  하지만 이 플러그인은 9년 전에 업데이트가 끊겨서 사용할 수가 없었습니다. 그래서 다른 방법을 찾아보다가 counter라는 Collection을 따로 만들어서 autoincrement를 하는 방법을 찾았고 이대로 구현해 보려 했지만, 프로젝트 기간이 얼마 안 남아서 직접 구현하기에는 무리가 있었습니다. 그래서 오피스 아워 시간에 코치님께 질문을 드렸더니 mongoose sequence plugin을 찾아주셨고, 이 플러그인의 문서를 보니 그동안 생각했던 것들이 다 구현되어 있어서 신기했습니다.


2. 사용법

1) 설치

npm install --save mongoose-sequence

 2) Global sequence

const mongoose = require("mongoose");
const AutoIncrement = require("mongoose-sequence")(mongoose);

 

  사용법은 간단합니다. 먼저 스키마를 정의하는 파일 안에 위 두 줄을 추가합니다. 

<Schema_name>.plugin(AutoIncrement, {inc_field: "<field_name>"});

 

  스키마를 다 정의하고 난 후에 <정의한 스키마 이름>.plugin(AutoIncrement, {inc_filed: <필드명>}) 을 추가합니다.

// schemas/board.js

const { Schema } = require("mongoose");
const mongoose = require("mongoose");
const AutoIncrement = require("mongoose-sequence")(mongoose);

const BoardSchema = new Schema({
  nickname: {
    type: String,
    required: true,
  },
  title: {
    type: String,
    required: true,
  },
  contents: {
    type: String,
    required: true,
  },
  createdAt: {
    type: Date,
    required: true,
    default: () => new Date(),
  },
});

BoardSchema.plugin(AutoIncrement, {
  inc_field: "boardId"
});
module.exports = BoardSchema;
// routes/board.js

router.post("/", async (req, res, next) => {
    ...
    
    const board = await Board.create({
      nickname,
      title,
      contents,
    });
    
    ...
});

 

  예시는 다음과 같습니다. 중요한 것은 스키를 정의할 때 boardId라는 항목을 따로 정의해주지 않아도 된다는 것입니다. 따로 정의해주지 않아도 글을 작성할 때마다 mongoose sequence plugin이 자동으로 boardId 값을 부여해 줍니다. 따라서 router에서 스키마 모델에 create를 해줄 때에도 boardId를 넣지 않아도 됩니다. 

 

  mongoDB Compass로 확인을 해보면 counters Collection이 자동으로 생성되면서, inc_field로 지정해 준 boardId 값이 id 값으로 들어가고, 자료가 하나 생성될 때마다 seq 값이 1씩 증가하는 것을 볼 수 있습니다.

 

  boards Collection을 보면 글을 올릴 때마다 boardId가 자동으로 생성되는 것을 볼 수 있습니다. routes/board.js에서 create할 때 boardId를 넣지 않았지만, 자동으로 생성이 됩니다. 마지막 글을 지우고 새 글을 올리면, 삭제된 글의 id값이 부여되는 것이 아닌 그다음 id값이 잘 부여됩니다. 


3) Scoped counters

  이번에는 위에서 정의한 commentId처럼 특정 필드 값마다 따로 increment되는 방식입니다. 사용법은 위와 비슷합니다. 다른 점은 inc_field 설정하는 부분에 코드를 몇 줄 더 추가해 줘야 합니다.

<Schema_name>.plugin(AutoIncrement, {
  id: <counter_collection_id>,
  reference_fields: <참조할 field_name>,
  inc_field: <field_name>,
});

 

  id에는 counter Collection에 들어갈 id 이름을 입력해줍니다. reference_fields에는 참조할 field_name을 입력해 줍니다. 예시에는 boardId마다 commentId 값을 따로 생성해 줄 것이기 때문에, boardId를 입력했습니다. inc_field는 자동으로 값을 증가시켜 줄 field_name 값을 입력해 줍니다.

// schemas/counter.js

const { Schema } = require("mongoose");
const mongoose = require("mongoose");
const AutoIncrement = require("mongoose-sequence")(mongoose);

const CommentSchema = new Schema({
  nickname: {
    type: String,
    required: true,
  },
  boardId: {
    type: String,
    required: true,
  },
  contents: {
    type: String,
    required: true,
  },
  createdAt: {
    type: Date,
    required: true,
    default: () => new Date(),
  },
});

CommentSchema.plugin(AutoIncrement, {
  id: "comment_sequence",
  reference_fields: "boardId",
  inc_field: "commentId",
});

module.exports = CommentSchema;
// routes/comment.js

router.post("/", async (req, res, next) => {
    ...

    const { boardId } = req.params;

    const comment = await Comment.create({
      nickname,
      boardId,
      contents,
    });
    
    ...
});

 

  예시 코드는 다음과 같습니다. 2번째 예시와 다른 점은 스키마를 정의하는 부분에 boardId를 추가해 줬고, 아래 plugin 부분에 id와 reference_fields 부분이 추가되었다는 것입니다. 그리고 router에서 스키마 모델을 create해주는 부분에서 참조하는 field 이름을 추가해 줘야 합니다. 위의 경우에는 boardId마다 commentId가 따로 생성되므로 boardId를 params로 받아서 comment를 생성할 때 boardId 값을 넣어줬습니다. params가 router.post("/:boardId", ~)가 아닌 이유는 경로 설정을 /boards/:boardId/comment로 했기 때문입니다.

 

  생성된 DB 자료를 mongoDB Compass로 보면 이해하기 쉬울 것입니다. counter Collection을 보면 boardId마다 자료가 따로 생성되는 것을 볼 수 있습니다. comment_sequence 값은 id 값으로 자료마다 동일하게 들어가있습니다. boardId값은 reference_value의 필드 안에 생성된 것을 볼 수 있습니다. 그리고 해당 글에 comment가 달릴 때마다 seq가 증가합니다.

 

  comments Collection을 보면 boardId마다 commentId가 다르게 부여되는 것을 볼 수 있습니다.


3. 마치며

  코딩 공부를 처음 시작하고 수행한 첫 프로젝트에서 가장 인상 깊었던 부분이라 블로그를 만들게 되면 꼭 첫 글로 이 주제를 다뤄야겠다고 생각했었는데, 글을 쓰다 보니 사실상 프로젝트 후기가 되어서 필요한 내용보다 글이 길어졌습니다. 프로젝트를 하면서 간단한 코드처럼 보이지만, 그 코드를 이해하고 쓰기 위해서 많은 시간 공부해야 한다는 것을 알 수 있었습니다. 프로젝트가 2주뿐이라 counters Collection을 초기화 하는 코드까지는 공부를 못하고 mongoDB Compass에 들어가서 seq 숫자를 0으로 바꿔줬었지만, 나중에 또 mongoose를 다룰 일이 있으면 초기화하는 것까지 다뤄보겠습니다. 긴 글 읽어주셔서 감사합니다.


참고 문헌

1. mongoose sequence plugin 공식 문서

+ Recent posts