1. Mongoose란 무엇인가
Mongoose는 MongoDB를 위한 ODM(Object Data Modeling) 라이브러리입니다. ODM은 객체와 문서(Document) 간의 매핑을 담당하며, JavaScript 객체를 MongoDB 문서로 변환하고 그 반대의 작업도 수행합니다.
MongoDB는 스키마가 없는(Schema-less) NoSQL 데이터베이스이지만, Mongoose를 사용하면 애플리케이션 레벨에서 스키마를 정의할 수 있습니다. 이를 통해 데이터의 구조를 명확히 하고, 유효성 검사를 수행하며, 타입 변환을 자동으로 처리할 수 있습니다.
Mongoose 설치 및 연결
npm install mongoose
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/myapp')
.then(() => console.log('MongoDB 연결 성공'))
.catch(err => console.error('MongoDB 연결 실패:', err));
2. 스키마(Schema) 정의와 타입
스키마는 MongoDB 컬렉션에 저장될 문서의 구조를 정의합니다. Mongoose는 다양한 스키마 타입을 지원합니다.
지원하는 스키마 타입
- String: 문자열
- Number: 숫자
- Date: 날짜
- Buffer: 바이너리 데이터
- Boolean: 불리언
- Mixed: 혼합 타입 (Schema.Types.Mixed)
- ObjectId: MongoDB ObjectId (Schema.Types.ObjectId)
- Array: 배열
- Decimal128: 고정밀 소수점
- Map: Map 객체
- UUID: UUID 타입
스키마 정의 예제
const mongoose = require('mongoose');
const { Schema } = mongoose;
const userSchema = new Schema({
name: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
age: {
type: Number,
min: 0,
max: 150
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
createdAt: {
type: Date,
default: Date.now
},
tags: [String],
profile: {
bio: String,
website: String
}
});
스키마 옵션
const schema = new Schema({ ... }, {
timestamps: true, // createdAt, updatedAt 자동 생성
collection: 'users', // 컬렉션 이름 지정
versionKey: false, // __v 필드 비활성화
strict: true // 스키마에 정의된 필드만 저장
});
3. 모델(Model) 생성 및 사용
모델은 스키마를 기반으로 생성되며, 실제 데이터베이스 작업을 수행하는 인터페이스입니다.
모델 생성
const User = mongoose.model('User', userSchema);
문서 생성 및 저장
// 방법 1: new + save()
const user = new User({
name: '홍길동',
email: 'hong@example.com',
age: 25
});
await user.save();
// 방법 2: create()
const user = await User.create({
name: '김철수',
email: 'kim@example.com',
age: 30
});
// 방법 3: insertMany()
const users = await User.insertMany([
{ name: '이영희', email: 'lee@example.com' },
{ name: '박민수', email: 'park@example.com' }
]);
문서 조회
// 전체 조회
const allUsers = await User.find();
// 조건 조회
const adults = await User.find({ age: { $gte: 18 } });
// 단일 문서 조회
const user = await User.findOne({ email: 'hong@example.com' });
// ID로 조회
const user = await User.findById('507f1f77bcf86cd799439011');
// 특정 필드만 선택
const users = await User.find().select('name email -_id');
// 정렬, 제한, 건너뛰기
const users = await User.find()
.sort({ createdAt: -1 })
.limit(10)
.skip(20);
문서 수정
// findByIdAndUpdate
const updatedUser = await User.findByIdAndUpdate(
userId,
{ name: '새이름' },
{ new: true, runValidators: true }
);
// updateOne
await User.updateOne(
{ email: 'hong@example.com' },
{ $set: { age: 26 } }
);
// updateMany
await User.updateMany(
{ role: 'user' },
{ $set: { isActive: true } }
);
문서 삭제
// findByIdAndDelete
await User.findByIdAndDelete(userId);
// deleteOne
await User.deleteOne({ email: 'hong@example.com' });
// deleteMany
await User.deleteMany({ isActive: false });
4. 유효성 검사(Validation)
Mongoose는 스키마 레벨에서 강력한 유효성 검사 기능을 제공합니다.
내장 검사기
const productSchema = new Schema({
name: {
type: String,
required: [true, '상품명은 필수입니다'],
minlength: [2, '상품명은 2자 이상이어야 합니다'],
maxlength: [100, '상품명은 100자 이하여야 합니다']
},
price: {
type: Number,
required: true,
min: [0, '가격은 0 이상이어야 합니다']
},
category: {
type: String,
enum: {
values: ['electronics', 'clothing', 'food'],
message: '{VALUE}는 유효한 카테고리가 아닙니다'
}
},
sku: {
type: String,
match: [/^[A-Z]{3}-\d{4}$/, 'SKU 형식이 올바르지 않습니다']
}
});
커스텀 검사기
const userSchema = new Schema({
email: {
type: String,
validate: {
validator: function(v) {
return /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(v);
},
message: props => `${props.value}는 유효한 이메일이 아닙니다`
}
},
phone: {
type: String,
validate: {
validator: async function(v) {
const count = await this.constructor.countDocuments({ phone: v });
return count === 0;
},
message: '이미 등록된 전화번호입니다'
}
}
});
유효성 검사 에러 처리
try {
await user.save();
} catch (error) {
if (error.name === 'ValidationError') {
for (const field in error.errors) {
console.log(`${field}: ${error.errors[field].message}`);
}
}
}
5. 미들웨어(Middleware/Hooks)
미들웨어는 특정 작업 전후에 실행되는 함수입니다. pre(전)와 post(후) 훅으로 구분됩니다.
Document 미들웨어
// 저장 전 비밀번호 해싱
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const bcrypt = require('bcrypt');
this.password = await bcrypt.hash(this.password, 10);
next();
});
// 저장 후 로깅
userSchema.post('save', function(doc) {
console.log(`새 사용자 생성됨: ${doc.email}`);
});
Query 미들웨어
// find 쿼리 전에 실행
userSchema.pre('find', function() {
this.where({ isDeleted: false });
});
// findOne 쿼리 전에 실행
userSchema.pre('findOne', function() {
this.where({ isDeleted: false });
});
// 업데이트 전 타임스탬프 갱신
userSchema.pre('findOneAndUpdate', function() {
this.set({ updatedAt: new Date() });
});
Aggregate 미들웨어
userSchema.pre('aggregate', function() {
this.pipeline().unshift({ $match: { isDeleted: false } });
});
에러 처리 미들웨어
userSchema.post('save', function(error, doc, next) {
if (error.name === 'MongoServerError' && error.code === 11000) {
next(new Error('이미 존재하는 이메일입니다'));
} else {
next(error);
}
});
6. 쿼리 빌더와 Population
쿼리 빌더
Mongoose는 체이닝 방식의 쿼리 빌더를 제공합니다.
const results = await User.find()
.where('age').gte(18).lte(65)
.where('role').equals('user')
.where('tags').in(['developer', 'designer'])
.select('name email age')
.sort('-createdAt')
.limit(10)
.lean()
.exec();
주요 쿼리 메서드
- where(): 조건 지정
- equals(): 값 일치
- gt(), gte(), lt(), lte(): 비교 연산
- in(), nin(): 포함 여부
- regex(): 정규식 매칭
- exists(): 필드 존재 여부
- lean(): 순수 JavaScript 객체 반환 (성능 향상)
Population
Population은 다른 컬렉션의 문서를 참조하여 자동으로 채워주는 기능입니다.
// 스키마 정의
const authorSchema = new Schema({
name: String,
email: String
});
const postSchema = new Schema({
title: String,
content: String,
author: {
type: Schema.Types.ObjectId,
ref: 'Author'
},
comments: [{
type: Schema.Types.ObjectId,
ref: 'Comment'
}]
});
const Author = mongoose.model('Author', authorSchema);
const Post = mongoose.model('Post', postSchema);
// Population 사용
const post = await Post.findById(postId)
.populate('author')
.populate({
path: 'comments',
select: 'text createdAt',
options: { sort: { createdAt: -1 }, limit: 5 }
});
// 중첩 Population
const post = await Post.findById(postId)
.populate({
path: 'comments',
populate: {
path: 'author',
select: 'name'
}
});
7. 가상 속성(Virtuals)과 인스턴스 메서드
가상 속성
가상 속성은 데이터베이스에 저장되지 않지만, 문서에서 접근할 수 있는 속성입니다.
const userSchema = new Schema({
firstName: String,
lastName: String,
birthYear: Number
});
// getter 가상 속성
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
// setter 가상 속성
userSchema.virtual('fullName').set(function(name) {
const [firstName, lastName] = name.split(' ');
this.firstName = firstName;
this.lastName = lastName;
});
// 계산된 속성
userSchema.virtual('age').get(function() {
return new Date().getFullYear() - this.birthYear;
});
// JSON/Object 변환 시 가상 속성 포함
userSchema.set('toJSON', { virtuals: true });
userSchema.set('toObject', { virtuals: true });
const user = new User({
firstName: '길동',
lastName: '홍',
birthYear: 1990
});
console.log(user.fullName); // '길동 홍'
console.log(user.age); // 계산된 나이
인스턴스 메서드
인스턴스 메서드는 개별 문서에서 호출할 수 있는 메서드입니다.
userSchema.methods.comparePassword = async function(candidatePassword) {
const bcrypt = require('bcrypt');
return bcrypt.compare(candidatePassword, this.password);
};
userSchema.methods.generateToken = function() {
const jwt = require('jsonwebtoken');
return jwt.sign(
{ id: this._id, email: this.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
};
userSchema.methods.toPublicJSON = function() {
return {
id: this._id,
name: this.name,
email: this.email,
createdAt: this.createdAt
};
};
const user = await User.findOne({ email: 'hong@example.com' });
const isMatch = await user.comparePassword('password123');
const token = user.generateToken();
정적 메서드
정적 메서드는 모델 자체에서 호출하는 메서드입니다.
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email: email.toLowerCase() });
};
userSchema.statics.findActiveUsers = function() {
return this.find({ isActive: true, isDeleted: false });
};
userSchema.statics.getStatistics = async function() {
return this.aggregate([
{ $group: { _id: '$role', count: { $sum: 1 } } }
]);
};
const user = await User.findByEmail('Hong@Example.com');
const activeUsers = await User.findActiveUsers();
const stats = await User.getStatistics();
결론
Mongoose는 MongoDB와 Node.js 애플리케이션을 연결하는 강력한 ODM 라이브러리입니다. 스키마 정의를 통해 데이터 구조를 명확히 하고, 유효성 검사로 데이터 무결성을 보장하며, 미들웨어와 가상 속성으로 비즈니스 로직을 효과적으로 구현할 수 있습니다. Population 기능은 관계형 데이터베이스의 조인과 유사한 기능을 제공하여 문서 간 참조를 쉽게 처리할 수 있게 해줍니다.
'Node.js' 카테고리의 다른 글
| Node.js MongoDB 연동 (0) | 2026.03.17 |
|---|---|
| Node.js 데이터베이스 연동(Database Integration) 완벽 가이드 (1) | 2026.03.16 |
| Node.js의 쿠키 관리(Cookie Management) (0) | 2026.03.16 |
| Node.js의 세션 관리(Session Management) (0) | 2026.03.15 |
| Node.js의 OAuth2.0 구현 (1) | 2026.03.15 |