안녕하세요. 이번 시간에는 몽구스 스키마에 대해 알아보겠습니다. 지난 시간에 몽구스는 스키마가 장점이라고 말씀드렸죠?
MySQL같은 SQL에 익숙하신 분들은 처음에 MongoDB나 Redis같은 nosql을 사용하면 헤맵니다. 바로 테이블이 없기 때문입니다. 다큐먼트에 아무거나 넣어도 에러가 생기지 않습니다! 어떻게보면 에러가 안 나서 매우 편리한 기능인 것 같지만, 실제로 사용하다보면 아무거나 다 들어가서 문제가 생깁니다. 실수로 오타를 낸 데이터도 들어가고, 같은 필드인데 자료형이 다른 경우도 생깁니다.
스키마
이러한 문제를 막기 위해 몽구스는 Schema(스키마)를 도입했습니다. 몽구스는 사용자가 작성한 스키마를 기준으로 데이터를 DB에 넣기 전에 먼저 검사합니다. 스키마에 어긋나는 데이터가 있으면 에러를 발생시킵니다. 즉, 테이블과 어느 정도 비슷한 역할을 합니다. 또한 스키마를 설정할 때 인덱스까지 같이 걸어둘 수도 있습니다. 기본값도 설정해줄 수 있고요. 구조에 관한 편의 기능들을 하나로 모아두었다고 생각하시면 됩니다.
스키마를 한 번 만들어보겠습니다. 보통 한 파일에 하나의 스키마를 작성합니다. 물론 스키마가 커지면 여러 파일로 분산해도 됩니다.
const mongoose = require('mongoose');
const imageSchema = new mongoose.Schema({
width: Number,
height: Number,
});
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true, lowercase: true },
password: { type: String, required: true, trim: true },
nickname: String,
birth: { type: Date, default: Date.now },
point: { type: Number, default: 0, max: 50, index: true },
image: imageSchema,
likes: [String],
any: [mongoose.Schema.Types.Mixed ],
id: mongoose.Schema.Types.ObjectId,
});
userSchema.index({ email: 1, nickname: 1 });
module.exports = mongoose.model('User', userSchema);
저어어얼대 제 블로그 스키마가 아닙니다! 그냥 예시를 들기 위해 만들어보았습니다.
imageSchema는 보조적인 것이고 자세히 보셔야 할 부분은 userSchema입니다. 스키마는 저렇게 만듭니다. 객체 안의 속성명들이 필드의 이름이 됩니다.
email 필드의 자료형은 문자열이고, 필수이고, 유일해야 하고, 소문자여야 한다고 정해주었습니다(uppercase로 대문자도 가능합니다). password 필드는 문자열이고, 필수이고, 공백을 제거한다고 정했습니다(trim은 String.prototype.trim
이라고 생각하시면 됩니다).
nickname 필드는 문자열인데 다른 옵션이 없기 때문에 그냥 String만 적어주었습니다. nickname은 필수가 아니라서 생략해도 됩니다. birth 필드는 Date고요. 기본값으로 현재 시간을 넣습니다. 기본값이란 사용자가 데이터를 넣지 않아도 자동으로 입력되는 값입니다.
point 필드는 숫자이고, 기본값은 0, 최댓값은 50입니다(min 옵션으로 최솟값을 지정할 수도 있습니다). 빠르게 찾을 수 있도록 인덱스도 걸어주었습니다. image 필드에는 객체가 들어갑니다. 따로 imageSchema를 만들어서 image 필드 안에 무엇이 들어가야 할지 정해주었습니다. 이렇게 스키마 안에 다른 스키마를 넣는 것도 가능합니다. 아래와 같이 다른 스키마를 쓰지 않고 표현도 가능합니다.
image: {
width: Number,
height: Number,
},
likes 필드는 문자열의 배열입니다. 배열 표시를 해 두고 [String]
또는 [{ type: String }]
이렇게 활용 가능합니다. 객체를 넣으면 required나 trim 같은 옵션을 줄 수도 있겠죠?
any와 id 필드는 조금 특이합니다. 각각 아무거나 넣어도 되는 필드(배열 안에), ObjectId를 넣을 수 있는 필드입니다.
몽고디비는 복합 인덱스(두 개 이상의 필드에 동시 인덱스)도 가능하기 때문에 스키마에서 직접 걸어줄 수 있습니다. userSchema.index()
부분을 보시면 됩니다.
마지막 줄 mongoose.model()
을 호출할 때 스키마가 등록됩니다. 나중에 스키마를 수정할 때, 미리 저장되어 있던 데이터들은 바꾸지 않으니 조심해야 합니다. 대신 SQL처럼 alter table 명령 없이 자유롭게 수정할 수 있어 좋습니다.
참고로 몽구스는 model의 첫 번째 인자로 컬렉션 이름을 만듭니다. User이면 소문자화 후 복수형으로 바꿔서 users 컬렉션이 됩니다. Book 스키마였다면 books 컬렉션이 됩니다. 이런 강제 개명이 싫다면 세 번째 인자로 컬렉션 이름을 줄 수 있습니다. mongoose.model('User', userSchema, 'myfreename')
위와 같이 스키마를 만들었다면 꼭 서버 실행하는 부분에서 require을 해주어야 합니다. require을 해주는 순간 스키마가 등록(model 메소드가 호출)되어서 앞으로 DB 작업을 할 때 스키마에 맞춰 검사합니다. 실컷 스키마 다 만들어놓고 require을 안 해서 스키마 적용이 안 된다고 하시는 분 많이 봤습니다;;
require('./경로/userSchema'); // 이렇게만 DB 연결 부분에 적어주면 됩니다.
편의 기능
위와 같이 타입을 고정해주는 것만으로도 좋지만 추가적으로 여러 편의 기능도 제공해줍니다. 많이 쓰이는 세 가지 기능을 알아보겠습니다. 먼저 virtual입니다. 다큐먼트에는 없지만 객체에는 있는 가상의 필드를 만들어줍니다.
userSchema.virtual('detail').get(function() {
return `저는 ${this.nickname}이고 생일은 ${this.birth.toLocaleString()}입니다.`;
});
스키마에 virtual을 붙이면 users 컬렉션을 조회할 때 { email: ..., password: ..., nickname: ..., detail: ... }
처럼 detail 필드가 생깁니다. 그리고 get 메소드 안에 넣어준 함수의 return 값이 들어있습니다. 기존 필드들을 활용해서 새로운 가상 필드를 만드는 기능입니다.
그 다음으로 소개할 기능은 스키마나 다큐먼트에 사용자 정의 메소드를 붙이는 겁니다.
userSchema.methods.comparePassword = function(pw ,cb) {
if (this.password === pw) {
cb(null, true);
} else {
cb('password 불일치');
}
};
// 나중에 user 다큐먼트를 받게 되면
user.comparePassword('비밀번호', function(err, result) {
if (err) {
throw err;
}
console.log(result);
});
직접 비밀번호가 일치하는지 확인하는 코드를 짤 필요없이 userSchema에 정의한 메소드로 재사용할 수 있습니다. 또 다큐먼트가 아닌 모델이나 쿼리에 직접 static method를 붙일 수도 있습니다.
userSchema.statics.findByPoint = function(point) {
return this.find({ point: { $gt: point } });
};
userSchema.query.sortByName = function(order) {
return this.sort({ nickname: order });
};
Users.findByPoint(50).sortByName(-1);
statics은 Users 모델에서 바로 쓸 수 있는 메소드이고, query는 한 번 쿼리 후에 이어서 쓰는 메소드입니다. 위와 같이 여러분이 직접 만들 수 있습니다.
마지막으로 소개할 것은 pre와 post 메소드입니다. 스키마에 붙여 사용하는데, 각각 특정 동작 이전, 이후에 어떤 행동을 취할 지를 정의할 수 있습니다. Hook를 건다고 생각하시면 됩니다.
userSchema.pre('save', function(next) {
if (!this.email) { // email 필드가 없으면 에러 표시 후 저장 취소
throw '이메일이 없습니다';
}
if (!this.createdAt) { // createdAt 필드가 없으면 추가
this.createdAt = new Date();
}
next();
});
userSchema.post('find', function(result) {
console.log('저장 완료', result);
});
위의 것은 save하기 전에 호출됩니다. next를 실행하지 않으면 save가 되지 않기 때문에 다큐먼트 저장 전 최종 검증으로 쓸 수 있습니다. 아래 것은 find를 호출한 다음에 실행됩니다. 딱히 사용처는 생각나지 않는데 통계를 내거나 할 때 사용할 수 있을 것 같습니다.
모델 메소드
이렇게 스키마 정의하고 모델과 연결한 후에는 다른 파일에서 이 모델을 불러와 실제 쿼리를 날릴 수 있습니다. 유명한 find와 findOne은 기본이고요. 몽구스에서 제공하는 메소드들이 몇 가지 더 있습니다. 유명한 것들만 간단히 알아봅시다.
find, findOne, findById
사용법은 아래와 같습니다. 프로젝션은 select처럼 원하는 필드만 가져오는 것을 뜻합니다. 자세한 사항은 쿼리 빌더 강좌에 있습니다. 콜백 함수는 넣어주면 콜백 형식(콜백 함수의 첫 번째 인자는 에러, 두 번째 인자는 결과)으로 결과를 받고 넣어주지 않으면 프로미스로 받습니다. 프로미스로 받는 것은 몽구스 프로미스 강좌를 참고하세요.
Users.find(조건, 프로젝션, 콜백) // 조건에 해당하는 모든 것을 쿼리
Users.findOne(조건, 프로젝션, 콜백) // 조건에 해당하는 첫 번째 것을 쿼리
Users.findById(아이디, 프로젝션, 콜백)
findOneAndRemove, findOneAndUpdate, findByIdAndRemove, findByIdAndUpdate
조회하는 메소드가 아니라 삭제 또는 수정하는 메소드입니다. 역시 콜백 유무에 따라 프로미스처럼 쓸 수도 있습니다. update 시에는 객체 형식의 옵션을 줄 수 있는데요. upsert(조건에 맞는 다큐먼트가 없을 시 생성), multi(여러 개 동시 업데이트), new(결과로 변경된 문서 반환) 등이 자주 쓰입니다.
특히 많이 헷갈려하시는 것이 multi와 new 옵션을 주지 않았을 때입니다. multi를 주지 않으면 하나의 다큐먼트만 업데이트되고, new를 주지 않으면 업데이트 후에도 업데이트 전 다큐먼트가 반환됩니다.
Users.findOneAndRemove(조건, 콜백)
Users.findByIdAndRemove(아이디, 콜백)
Users.findOneAndUpdate(조건, 변경, 옵션, 콜백)
Users.findByIdAndUpdate(아이디, 변경, 옵션, 콜백)
Users.findOneAndUpdate({ name: 'zerocho' }, { name: 'babo' }, { multi: true, new: true }) // 예시
다음 시간에는 populate에 대해 알아보겠습니다!
기존 도큐먼트에 새로운 필드를 추가하고 싶으면 어떻게 하는지 문의 드립니다.
구글 검색결과
await keywdAccount.updateMany(
{ },
{ $set: {
contract: message,
}
}, false, true
// { new: true },
);
대로 하면 저는 안됬습니다 ..