MongoDB Academy · 강의

Mongoose 미들웨어: 사전 및 사후 훅

학습자는 저장 전에 비밀번호를 해시하거나 조회 후 기록을 남기는 등의 작업을 위해 문서 및 쿼리 미들웨어 훅을 작성합니다.

레슨 4/413개 단계

Mongoose 미들웨어: 사전 및 사후 훅은(는) CoddyKit의 무료 MongoDB Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 MongoDB Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. MongoDB Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

What Is Mongoose Middleware?

Mongoose middleware (also called hooks) are functions that run before or after specific operations like save, find, updateOne, deleteOne, and more. They enable you to inject custom logic into the lifecycle of document and query operations without cluttering your route handlers. Common uses include hashing passwords before save, logging query times, enforcing soft deletes, and populating related data after find.

Two Types: Document and Query Middleware

Mongoose has two distinct categories of middleware: Document middleware hooks into operations on a specific Document instance (save, validate, remove, init). Query middleware hooks into query operations invoked on the Model (find, findOne, updateOne, deleteOne, etc.). The key difference is what this refers to—in document middleware, this is the document; in query middleware, this is the query object.

// Document middleware: 'this' = the document
userSchema.pre('save', function () {
  console.log('Saving document:', this.email);
});

// Query middleware: 'this' = the Query object
userSchema.pre('find', function () {
  console.log('Running query:', this.getQuery());
});

Pre-save: Hashing Passwords

The pre-save hook is the most common document middleware. It runs before a document is saved to MongoDB. The canonical use case is hashing passwords: when a user document is saved with a new or modified password, hash it with bcrypt before storing. The this.isModified('password') check prevents re-hashing an already-hashed password on unrelated saves.

const bcrypt = require('bcrypt');

userSchema.pre('save', async function () {
  // 'this' is the User document being saved
  if (!this.isModified('password')) {
    return; // skip if password hasn't changed
  }
  const saltRounds = 12;
  this.password = await bcrypt.hash(this.password, saltRounds);
  // The hashed value replaces the plain password before MongoDB stores it
});

isNew and isModified Helpers

Document middleware has access to tracking helpers: this.isNew is true when the document is being inserted for the first time (not updated). this.isModified(path) returns true if the specified field has been changed since the document was last saved or fetched. These helpers let you run hooks conditionally—only on creation, or only when a specific field changes.

userSchema.pre('save', async function () {
  if (this.isNew) {
    // Only runs when creating a new user, not on updates
    this.verificationToken = crypto.randomBytes(32).toString('hex');
    this.verificationExpires = new Date(Date.now() + 24 * 60 * 60 * 1000);
  }

  if (this.isModified('email')) {
    // Only runs when the email field specifically changed
    this.emailVerified = false; // reset verification on email change
  }
});

Post-save: Side Effects After Saving

Post-save hooks run after a document is successfully persisted. They receive the saved document as the first argument and (in older Mongoose) a next callback. Post hooks are ideal for side effects that should happen after a successful save: sending a welcome email, updating a search index, publishing an event to a message queue, or clearing a cache. Errors in post hooks do not roll back the save.

userSchema.post('save', async function (doc) {
  // 'doc' is the saved document
  if (doc.isNew) {
    // Note: 'isNew' is false here (doc was just saved, so it's no longer new)
    // Track this with a flag set in pre-save:
  }
});

// Pattern: set a flag in pre-save, read it in post-save
userSchema.pre('save', function () {
  this._wasNew = this.isNew; // save state before it changes
});

userSchema.post('save', async function (doc) {
  if (doc._wasNew) {
    await sendWelcomeEmail(doc.email, doc.name);
    await analyticsTracker.track('user_created', { userId: doc._id });
  }
});

Query Middleware: Pre-find for Soft Deletes

A classic query middleware pattern is implementing soft deletes. Instead of removing documents, set a deletedAt field. Then add a pre('find') hook that automatically adds { deletedAt: null } to every find query, ensuring deleted documents are never returned by default. This provides an audit trail while making the soft-delete logic transparent to the rest of the application.

const postSchema = new mongoose.Schema({
  title: String,
  content: String,
  deletedAt: { type: Date, default: null }
});

// Automatically exclude soft-deleted documents from all find queries
postSchema.pre(/^find/, function () {
  // 'this' is the Query object
  this.where({ deletedAt: null });
  // /^find/ matches find, findOne, findOneAndUpdate, etc.
});

// Now Post.find({}) never returns deleted posts
// To explicitly query deleted posts, you'd call Post.find({}).bypassMiddleware() or use .lean() with the native driver

Query Middleware: Pre-find Automatic Population

You can use query middleware to automatically populate a reference field on every find. This ensures that a referenced document is always resolved without requiring callers to add .populate() to every query. While convenient, be careful—automatic population adds a second query for every find and can degrade performance if the reference is large or not always needed.

const reviewSchema = new mongoose.Schema({
  productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' },
  userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  rating: Number,
  comment: String
});

// Always populate author info on find
reviewSchema.pre(/^find/, function () {
  this.populate({
    path: 'userId',
    select: 'name avatar'
  });
});

// Now Review.find() always includes user name and avatar

Pre-deleteOne: Cascade Deletes

Mongoose does not enforce cascade deletes (deleting related documents when a parent is deleted) automatically. You can implement cascade behavior using document middleware. A pre-deleteOne hook on a User model can delete all posts, comments, and sessions belonging to that user before the user document itself is removed. This keeps referential integrity without foreign key constraints.

userSchema.pre('deleteOne', { document: true, query: false }, async function () {
  // 'this' is the User document being deleted
  const userId = this._id;

  // Cascade delete related documents
  await Promise.all([
    Post.deleteMany({ authorId: userId }),
    Comment.deleteMany({ userId: userId }),
    Session.deleteMany({ userId: userId }),
    Notification.deleteMany({ userId: userId })
  ]);

  console.log('Cascade deleted data for user:', userId);
});

// Trigger:
// const user = await User.findById(id);
// await user.deleteOne(); // triggers pre-deleteOne above

Middleware Error Handling

If a pre-hook function throws an error or rejects a Promise, the operation it precedes is aborted. This lets you perform validation or authorization checks in middleware and abort saves or queries by throwing. For example, a pre-save hook that validates business logic (not just schema validation) can throw an error that bubbles up to the .save() call's catch block in the application code.

orderSchema.pre('save', async function () {
  if (this.total <= 0) {
    throw new Error('Order total must be positive');
  }

  // Check inventory synchronously before saving the order
  const product = await Product.findById(this.productId).lean();
  if (!product || product.stock < this.quantity) {
    throw new Error('Insufficient inventory for this order');
  }
});

// In route handler:
try {
  const order = new Order({ productId, quantity, total });
  await order.save(); // throws if pre-save hook rejects
} catch (err) {
  res.status(400).json({ error: err.message });
}

Aggregate Middleware

Mongoose also supports middleware for aggregation pipelines. A pre-aggregate hook gives you access to the pipeline array before it is sent to MongoDB, allowing you to prepend stages (like filtering soft-deleted documents) or append stages (like injecting a default limit). Access the pipeline via this.pipeline() inside the hook function.

postSchema.pre('aggregate', function () {
  // 'this' is the Aggregate object
  // Add a $match stage at the beginning to exclude soft-deleted documents
  this.pipeline().unshift({
    $match: { deletedAt: null }
  });
});

// Now Post.aggregate([...]) automatically excludes deleted posts
// at the start of every aggregation pipeline

Middleware Pitfalls: Query Methods That Bypass Hooks

Not all write operations trigger document middleware. updateMany(), findOneAndUpdate(), replaceOne() called on the Model (not an instance) bypass document save hooks—they are query middleware and need separate hooks if you want to intercept them. For example, a pre-save password hashing hook does NOT run when you call User.updateOne({}, { $set: { password: plain } }). Always hash in the application code for query-based updates.

// WRONG: password NOT hashed — bypasses pre-save hook
await User.updateOne({ _id: userId }, { $set: { password: plainPassword } });

// RIGHT for query-level updates: hash before calling updateOne
const hashed = await bcrypt.hash(plainPassword, 12);
await User.updateOne({ _id: userId }, { $set: { password: hashed } });

// Or: fetch, modify, save — triggers pre-save hook
const user = await User.findById(userId);
user.password = plainPassword; // hook will hash it
await user.save();

Quick Check

Test your understanding of MongoDB & NoSQL Databases concepts from this lesson.

Lesson Recap

In this lesson you learned: pre hooks run before an operation and can abort it by throwing an error; post hooks run after and receive the result as an argument, document middleware (pre-save, pre-deleteOne) uses 'this' as the document, while query middleware uses 'this' as the Query object, and query-level write methods (updateOne, updateMany, findOneAndUpdate) bypass document middleware — always be aware of which hooks fire for each operation type. This completes the MongoDB & NoSQL Databases course track!

무료로 시작

AI 튜터와 함께 JavaScript을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
30
레슨
120

자주 묻는 질문

“Mongoose 미들웨어: 사전 및 사후 훅” 강의는 무료인가요?

네 — “Mongoose 미들웨어: 사전 및 사후 훅” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 MongoDB Academy 강의 전체를 잠금 해제할 수 있습니다. MongoDB Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“Mongoose 미들웨어: 사전 및 사후 훅”에서 뭘 배우나요?

학습자는 저장 전에 비밀번호를 해시하거나 조회 후 기록을 남기는 등의 작업을 위해 문서 및 쿼리 미들웨어 훅을 작성합니다. 브라우저에서 직접 실행하는 실습 코드로 MongoDB Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

MongoDB Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 MongoDB Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“Mongoose 미들웨어: 사전 및 사후 훅” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 MongoDB Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 MongoDB Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 공식 Node.js 드라이버로 연결하기
  2. Mongoose 스키마, 모델 및 가상 속성
  3. Mongoose 쿼리, 연결 및 Lean 문서
  4. Mongoose 미들웨어: 사전 및 사후 훅
← MongoDB Academy(으)로 돌아가기