0Pricing
MongoDB Academy · 강의

Mongoose 쿼리, 연결 및 Lean 문서

학습자는 Mongoose 쿼리 도우미를 연결하고, 원시 POJO 성능을 위해 .lean()을 사용하며, 쿼리 API를 네이티브 드라이버와 비교합니다.

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

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

Mongoose Query Objects

When you call a Mongoose query method like User.find(), it returns a Query object rather than a Promise. This Query object is lazy—it does not execute until you explicitly call it with .then(), await, or .exec(). Before execution, you can chain additional query modifiers to build up the complete query. This chainable API is one of Mongoose's most ergonomic features.

const User = require('./models/user');

// This does NOT execute immediately — returns a Query object
const query = User.find({ active: true });

// Now execute it with await
const users = await query;

// Or chain modifiers before executing:
const result = await User.find({ active: true })
  .sort({ createdAt: -1 })
  .limit(10)
  .select('name email -_id');
  // select() projects fields: '+field' includes, '-field' excludes

Chaining Query Modifiers

Mongoose query modifiers like .sort(), .limit(), .skip(), .select(), and .populate() can be chained in any order before execution. The underlying Query object accumulates all modifiers and sends a single optimized query to MongoDB. This is functionally equivalent to passing options to the native driver's find(filter, options) but reads more naturally as a fluent builder.

const orders = await Order
  .find({ status: 'completed', userId: currentUserId })
  .sort({ createdAt: -1 })              // newest first
  .skip(page * pageSize)                // pagination offset
  .limit(pageSize)                      // page size
  .select('_id total status createdAt') // projection
  .lean();                              // return plain objects (discussed next)

console.log('Orders on this page:', orders.length);

The .lean() Method: Raw POJO Performance

By default, Mongoose wraps every document returned by a query in a Mongoose Document instance—a heavy object with change tracking, methods, virtuals, and middleware hooks. The .lean() method tells Mongoose to return plain JavaScript objects (POJOs) instead. Lean queries are typically 2-5x faster and use less memory because Mongoose skips the Document wrapping. Use .lean() for read-only operations where you don't need document methods or save/update hooks.

// Without .lean() — heavy Mongoose Document objects
const docsWithMethods = await User.find({ active: true });
// docsWithMethods[0].save() works, but incurs overhead

// With .lean() — plain JavaScript objects, much faster
const pureObjects = await User.find({ active: true }).lean();
// pureObjects[0].save() does NOT work — it's a plain object
// But JSON.stringify, spread operators, and array methods are all faster

console.log(typeof docsWithMethods[0].save); // 'function'
console.log(typeof pureObjects[0].save);     // 'undefined'

When to Use .lean() vs Full Documents

Use .lean() when: you are only reading data (GET endpoints), you need to serialize to JSON quickly, or you are processing many documents in bulk. Do not use .lean() when: you need to call .save() on the result, use virtual properties, run document middleware, or access instance methods. A good rule of thumb: API reads → lean, mutation flows → full Mongoose documents.

// API read endpoint — use .lean() for speed
router.get('/products', async (req, res) => {
  const products = await Product.find({}).lean(); // fastest, no doc wrapper
  res.json(products);
});

// Update endpoint — use full Mongoose document to access instance methods
router.post('/users/:id/deactivate', async (req, res) => {
  const user = await User.findById(req.params.id); // full document, NO .lean()
  await user.sendDeactivationEmail(); // instance method won't work with .lean()
  user.active = false;
  await user.save(); // document method won't work with .lean()
  res.json({ success: true });
});

findById and findOne Convenience Methods

Mongoose adds convenience query methods not available in the native driver. Model.findById(id) is equivalent to Model.findOne({ _id: id }) and automatically converts string IDs to ObjectId. Model.findByIdAndUpdate(id, update, options) and Model.findByIdAndDelete(id) combine lookup and modification in a single atomic operation. These methods greatly reduce boilerplate in CRUD route handlers.

// findById — automatic ObjectId conversion from string
const user = await User.findById('64a1b2c3d4e5f6789012345a').lean();

// findByIdAndUpdate — find, update, and return result atomically
const updatedProduct = await Product.findByIdAndUpdate(
  productId,
  { $set: { price: 199.99 }, $inc: { updateCount: 1 } },
  { new: true, runValidators: true }  // return new doc, run validators
);

// findByIdAndDelete — find and delete atomically
const deletedUser = await User.findByIdAndDelete(userId);
console.log('Deleted:', deletedUser ? deletedUser.email : 'not found');

Counting Documents

Mongoose provides efficient document counting methods. Model.countDocuments(filter) applies a filter and counts matching documents—it scans matching documents and uses indexes. Model.estimatedDocumentCount() uses collection metadata for an approximate but instantaneous count without a filter—useful for dashboard totals on large collections where exact counts are not critical.

// Exact count with filter — uses an index if available
const activeUsers = await User.countDocuments({ active: true, role: 'user' });
console.log('Active users:', activeUsers);

// Fast approximate count — no filter, uses collection stats
const totalProducts = await Product.estimatedDocumentCount();
console.log('Approximate total products:', totalProducts);

// In Express pagination:
const [data, total] = await Promise.all([
  User.find({}).skip(offset).limit(pageSize).lean(),
  User.countDocuments({})
]);
res.json({ data, total, pages: Math.ceil(total / pageSize) });

Populate: Resolving References

.populate() is one of Mongoose's most powerful features—it replaces an ObjectId reference field with the actual referenced document, fetched from another collection. Under the hood, Mongoose issues a second query to the referenced collection and substitutes the IDs. It is equivalent to $lookup in the aggregation pipeline but with a simpler API.

const Order = mongoose.model('Order', new mongoose.Schema({
  userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  productIds: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Product' }]
}));

// Populate the userId reference with the full User document
const order = await Order
  .findById(orderId)
  .populate('userId', 'name email')    // only select name and email from User
  .populate('productIds', 'name price') // populate array of references
  .lean();

console.log(order.userId.email);      // 'alice@example.com'
console.log(order.productIds[0].name); // 'Laptop'

Mongoose vs Native Driver: When to Choose

Mongoose adds validation, populate, middleware, and a convenient query API—at the cost of some overhead. Choose Mongoose when: your application has well-defined, stable schemas; you want schema validation without JSON Schema validators; you need populate for reference resolution; or you are building a conventional REST API. Choose the native driver when: you need maximum performance, are working with dynamic schemas, are building aggregation-heavy analytics, or are writing a microservice with minimal dependencies.

// Mongoose: ergonomic, validates, populate works
const user = await User.findOne({ email }).select('-password').populate('profile');

// Native driver: faster, raw, no middleware
const user = await db.collection('users')
  .findOne({ email }, { projection: { password: 0 } });

exec() and Error Handling

Calling .exec() explicitly converts a Mongoose Query to a Promise and is the traditional way to execute queries when using .catch() promise chaining. With async/await, you can omit .exec()—a bare await User.find({}) works fine. However, some developers prefer .exec() for clarity or when building query objects programmatically. Both patterns produce identical results.

// With .exec() — explicit Promise conversion
const user = await User.findOne({ email }).exec();

// Without .exec() — implicit execution via await
const user = await User.findOne({ email });

// Error handling with try/catch (both forms work the same)
try {
  const user = await User.findById(id);
  if (!user) throw new Error('User not found');
} catch (err) {
  if (err.name === 'CastError') {
    res.status(400).json({ error: 'Invalid ID format' });
  } else {
    res.status(500).json({ error: err.message });
  }
}

Query Builder Pattern

Because Mongoose queries are lazy, you can conditionally build queries in separate statements before execution. This is useful when query parameters are optional—add sort or filters only when the parameter is present. This pattern is much cleaner than constructing dynamic query strings and keeps the code readable.

async function searchProducts(filters) {
  let query = Product.find();

  if (filters.category) {
    query = query.where('category').equals(filters.category);
  }
  if (filters.maxPrice) {
    query = query.where('price').lte(filters.maxPrice);
  }
  if (filters.inStock) {
    query = query.where('stock').gt(0);
  }

  const sortField = filters.sortBy || 'createdAt';
  query = query.sort({ [sortField]: -1 }).limit(50).lean();

  return query; // executes here via await in the caller
}

Aggregate Pipeline in Mongoose

Mongoose models also support the aggregation pipeline via Model.aggregate(pipeline). Unlike regular Mongoose queries, aggregation bypasses schema casting, Mongoose middleware, and populate—it behaves similarly to calling the native driver's aggregate directly. Aggregate returns a plain array of objects, never Mongoose Documents. Use Model.aggregate() for complex analytics and reporting that don't benefit from Mongoose's abstractions.

// Aggregation in Mongoose — bypasses Mongoose middleware and casting
const salesByRegion = await Order.aggregate([
  { $match: { status: 'completed' } },
  {
    $group: {
      _id: '$region',
      totalRevenue: { $sum: '$total' },
      orderCount: { $sum: 1 },
      avgOrder: { $avg: '$total' }
    }
  },
  { $sort: { totalRevenue: -1 } }
]);

// salesByRegion is a plain array — no Mongoose Document wrapper

Quick Check

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

Lesson Recap

In this lesson you learned: Mongoose query methods return lazy Query objects that can be chained with .sort(), .limit(), .skip(), .select(), and .populate() before execution, .lean() returns plain JavaScript objects for better performance on read-only operations, and Model.aggregate() bypasses Mongoose abstractions and behaves like the native driver for analytics pipelines. Next up we explore Mongoose middleware — pre and post hooks for custom logic around save, find, and other operations.

자주 묻는 질문

“Mongoose 쿼리, 연결 및 Lean 문서” 강의는 무료인가요?

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

“Mongoose 쿼리, 연결 및 Lean 문서”에서 뭘 배우나요?

학습자는 Mongoose 쿼리 도우미를 연결하고, 원시 POJO 성능을 위해 .lean()을 사용하며, 쿼리 API를 네이티브 드라이버와 비교합니다. 브라우저에서 직접 실행하는 실습 코드로 MongoDB Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

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

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

“Mongoose 쿼리, 연결 및 Lean 문서” 강의는 얼마나 걸리나요?

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

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

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

이 강의의 모든 강의

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