on1ystar 머릿속을 정리, 기록하는 곳

Node.js/Express Mongoose(몽구스) Middlewares(미들웨어)/Statics(스태틱) 함수



의문점이나 지적 등의 관심 및 조언을 위한 댓글이나 메일은 언제나 환영이고 감사합니다.

이전 포스트에서 이어짐

우리가 Model을 이용해 document를 저장하거나 수정할 때, 각 필드에 여러 작업들을 해야할 때가 있다.

예를 들어 사용자를 생성할 때 e-mail을 조회 한다던 지, 패스워드를 암호화 한다던 지, 이전 코드에서 hashtags를 파싱하기 위해 문자열 함수들을 사용한다던 지 등의 사전 작업들을 할 수 있다.

이런 코드들은 사실 DB에 저장하기 전에 수행하는 데이터 전처리나 추가적인 유효성 검사같은 것이므로 컨트롤러 로직 안에 불필요하게 복붙하고 다니며 지저분하게 만들 필요없이 몽구스가 스키마 단에서 처리해 주는 기능을 제공한다.

https://mongoosejs.com/docs/middleware.html

Middleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions. Middleware is specified on the schema level and is useful for writing plugins.

미들웨어(Middleware, pre, post hook)는 비동기 함수를 실행하는 동안 제어가 전달되는 함수다. 미들웨어는 스키마 수준에서 지정되며 플러그인 작성에 유용하다.

즉, 우리가 DB에 쿼리할 때 스키마별로 작성해 둔 Middleware들이 쿼리 실행 앞, 뒤에 실행되어 일련의 작업들을 수행한다.

또한 이 Middleware들은 스키마로 모델을 생성(정의)하기 전에 선언해야 한다.


☄ Pre hook 사용해서 hashtags 파싱하기



이전에 비디오를 생성하고 저장할 때 hasgtags를 파싱하던 방법이다.

// controllers/videoController.js

// ... 생략

await Video.create({
  title,
  description,
  hashtags: hashtags
    .split(",")
    .map((word) =>
      word.trim()[0] === "#" ? `${word.trim()}` : `#${word.trim()}`
    ),
});

위 로직을 미들웨어로 옮길 수 있다.

// models/Video.js

// ... 생략

videoSchema.pre("save", async function () {
  console.log(this);
  this.hashtags = this.hashtags[0]
    .split(",")
    .map((word) => (word.startsWith("#") ? word : `#${word}`));
});

위 미들웨어는 save 미들웨어로 pre 메소드를 사용해 save 되기 전, 비동기 함수를 호출시킨다.

안에 작성한 this 키워드는 저장될 document 객체를 담고 있다.

  • 주의할 점

위에서 화살표 함수를 안쓰고 굳이 fucntion 키워드를 사용해 함수를 작성했는데, 왜 인지는 모르겠지만 화살표 함수로 작성하면 this 키워드를 사용 시 에러가 뜬다. 에러메세지도 컨트롤러의 create에서 발생해 찾기 어려웠다…

  • 이유

주의할점은 this를 통해 해당 메서드를 불러온 객체의 값을 이용해야 하는데 화살표 함수를 사용하게 되면 lexical this를 사용하게 되어 해당 메서드를 이용하는데 불편하게 됩니다.

https://darrengwon.tistory.com/415


☄ 모델에 statics 함수 등록해서 hashtags 파싱하기



그런데 한 가지 문제가 있다. save에 대한 pre hook 미들웨어를 사용해 비디오 생성 시 자동으로 hashtags를 파싱하도록 했다. 하지만 비디어 수정 후 저장은 또 다른 문제다.

// controllers/videoController.js

// ... 생략

export const postEdit = async (req, res) => {
  const { id } = req.params;
  const { title, description, hashtags } = req.body;
  const video = await Video.exists({ _id: id });
  if (!video) {
    return res.render("404", { pageTitle: "Video Not Found" });
  }
  await Video.findByIdAndUpdate(id, {
    title,
    description,
    hashtags: hashtags
      .split(",")
      .map((word) =>
        word.trim()[0] === "#" ? `${word.trim()}` : `#${word.trim()}`
      ),
  });
  return res.redirect(`/videos/${id}`);
};

위의 경우 모델의 findByIdAndUpdate 함수를 사용해 조회와 수정을 동시에 수행하도록 했다. findByIdAndUpdate은 내부적으로 findOneAndUpdate()를 호출한다.

하지만 findByIdAndUpdate 함수에서는 등록한 pre 또는 post save() hook이 작동하지 않는다.

Pre and post save() hooks are not executed on update(), findOneAndUpdate(), etc.

또한, 따로 findOneAndUpdate()에 대한 hook을 만들려 해도 수정하려는 document에 접근할 수 없다.

You cannot access the document being updated in pre('updateOne') or pre('findOneAndUpdate') query middleware.

따라서 우리의 비디오 모델의 경우, hashtags를 파싱하도록 커스터마이징한 statics 함수를 모델에 등록하는 방법을 사용하는 것이 매우 좋다.

즉, Video 모델에서만 처리해야 하는 로직들을 statatics 함수로 만들어 사용해 모듈화

// models/Video.js

// ... 생략

videoSchema.static("formatHashtags", (hashtags) =>
  hashtags
    .split(",")
    .map((word) =>
      word.trim().startsWith("#") ? word.trim() : `#${word.trim()}`
    )
);

formatHashtags라는 이름의 statics 함수를 videoSchema에 등록하는 방법이다.

그러면 Video 모델에서 다른 모델 함수와 동일하게 사용할 수 있다.

// controllers/videoContoroller.js

// ... 생략

export const postEdit = async (req, res) => {
  const { id } = req.params;
  const { title, description, hashtags } = req.body;
  const video = await Video.exists({ _id: id });
  if (!video) {
    return res.render("404", { pageTitle: "Video Not Found" });
  }
  await Video.findByIdAndUpdate(id, {
    title,
    description,
    hashtags: Video.formatHashtags(hashtags), // 수정
  });
  return res.redirect(`/videos/${id}`);
};

export const postUpload = async (req, res) => {
  const { title, description, hashtags } = req.body;
  try {
    await Video.create({
      title,
      description,
      hashtags: Video.formatHashtags(hashtags), // 수정
    });
    return res.redirect("/");
  } catch (err) {
    console.warn("Video.create error: ", err);
    return res.status(404).render("upload", {
      pageTitle: "Upload Video",
      errprMessage: err._message,
    });
  }
};


Reference

https://nomadcoders.co/wetube

https://mongoosejs.com/docs/middleware.html

https://mongoosejs.com/docs/guide.html#statics