게시: 2021년 3월 2일
typescript 환경에서 mongoose 모듈을 사용하여 methods와 statics를 사용할 상황이 생겼는 데, nodejs에서는 바로 실행되던 부분이 typescript에서는 조금 더 까다롭게 설정이 되서 포스팅을 적는다. typescript 자체가 워낙 자료형에 민감하기 때문에 더욱 그런 것 같다. 그 덕분에 코드 리뷰나 협업에서는 편리함을 느끼지만 간혹 이렇게 불편함이 생길 때가 있는 것 같다.
우선 전체 코드를 보도록 하자.
큰 틀을 보자면 User라는 mongodb의 모델이 사용할 변수와 methods, statics 들의 자료형을 interface를 통해서 명시하고 해당 interface를 포함하는 model을 생성한다. 자세한 사항은 아래에서 차근차근 진행하도록 하자.
// user.ts
import mongoose, { Schema, Document, Model } from "mongoose";
import bcrypt from "bcrypt";
interface IUser {
username: string;
hashedPassword: string;
}
interface IUserDocument extends IUser, Document {
setPassword: (password: string) => Promise<void>;
checkPassword: (password: string) => Promise<boolean>;
}
interface IUserModel extends Model<IUserDocument> {
findByUsername: (username: string) => Promise<IUserDocument>;
}
const UserSchema: Schema<IUserDocument> = new Schema({
username: { type: String, required: true },
hashedPassword: { type: String, required: true },
});
UserSchema.methods.setPassword = async function (password: string) {
const hash = await bcrypt.hash(password, 10);
this.hashedPassword = hash;
};
UserSchema.methods.checkPassword = async function (password: string) {
const result = await bcrypt.compare(password, this.hashedPassword);
return result;
};
UserSchema.statics.findByUsername = function (username: string) {
return this.findOne({ username });
};
const User = mongoose.model<IUserDocument, IUserModel>("User", UserSchema);
export default User;
아래의 interface는 User 모델에서 사용할 변수들의 이름과 자료형을 명시하는 역할을 한다.
interface IUser {
username: string;
hashedPassword: string;
}
methods를 사용할 때는 이 method를 호출한 객체가 method 내에서의 this가 되고,
statics를 사용할 때는 이 statics를 호출한 객체에 상관없이 this가 모델 자체가 된다.
따라서, 위의 코드에서 setPassword와 checkPassword는 한 객체에 대해서 각각 해당 객체에 비밀번호를 설정하거나 해당 객체의 비밀번호와 일치하는 지 여부를 확인할 수 있다.
반면, findByUsername은 Model 내에서 해당 username인 User를 반환할 수 있다.
아래의 interface는 User 모델에서 사용할 변수에 덧붙여 methods의 이름과 자료형을 명시하는 역할을 한다.
다만, 위의 interface와 다른 점은 IUser 뿐만 아니라 mongoose의 class 인 Document도 extends 한다는 점이다.
이후 해당 interface를 토대로 mongoose Schema를 생성한다.
interface IUserDocument extends IUser, Document {
setPassword: (password: string) => Promise<void>;
checkPassword: (password: string) => Promise<boolean>;
}
const UserSchema: Schema<IUserDocument> = new Schema({
username: { type: String, required: true },
hashedPassword: { type: String, required: true },
});
UserSchema.methods.setPassword = async function (password: string) {
const hash = await bcrypt.hash(password, 10);
this.hashedPassword = hash;
};
UserSchema.methods.checkPassword = async function (password: string) {
const result = await bcrypt.compare(password, this.hashedPassword);
return result;
};
아래의 interface는 위의 변수와 methods 그리고 mongoose의 Document class를 포함한 interface를 Model 형태로 extends하고 User 모델에서 사용할 statics를 명시하는 역할을 한다.
interface IUserModel extends Model<IUserDocument> {
findByUsername: (username: string) => Promise<IUserDocument>;
}
UserSchema.statics.findByUsername = function (username: string) {
return this.findOne({ username });
};
위에서 만든 IUserDocument(변수 + methods의 interface)와 statics 까지 포함한 interface를 기반으로 User model을 선언한다. 이 후 해당 model을 import하면 위의 변수, methods, statics 기능을 모두 사용할 수 있다.
const User = mongoose.model<IUserDocument, IUserModel>("User", UserSchema);
export default User;