우당탕탕 우리네 개발생활

[Nestjs] mongoose와의 조합은 환상이다(feat. configuration) 본문

tech

[Nestjs] mongoose와의 조합은 환상이다(feat. configuration)

미스터카멜레온 2023. 11. 10. 12:00

현업에서 Expressjs + mongoose의 조합으로 대부분의 서버작업을 진행했습니다.

mongoose의 편리성으로 인해 덕을 많이 봤던터라 Nestjs와의 조합도 기대가 되었습니다. Nestjs의 공식사이트에서 가이드를 쉽게 접할 수 있었습니다. 이를 토대로 Nestjs + mongoose환경을 구축해 본 경험을 풀어보겠습니다. 

https://docs.nestjs.com/techniques/mongodb

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

mongoose를 적용해보기위해선 당연히 mongoDB를 사용할 수 있는 준비가 되어있어야 합니다. 아래 링크는 mongoDB를 로컬로 간단히 올려볼 수 있는 예시입니다.

https://khjeong0423.tistory.com/6 

 

[MongoDB] 초간단 로컬 mongodb 사용하기(feat. docker)

몽고DB(MongoDB←HUMONGOUS)는 크로스 플랫폼 도큐먼트 지향 데이터베이스 시스템이다. NoSQL 데이터베이스로 분류되는 몽고DB는 JSON과 같은 동적 스키마형 도큐먼트들(몽고DB는 이러한 포맷을 BSON이라

khjeong0423.tistory.com

 

우선 Nestjs + mongoose조합으로 완성해 둔 개인프로젝트 하나를 공유드립니다. 아래 설명들은 모두 제가 작성한 프로젝트에 기반한 설명입니다. 

https://github.com/mmmmicha/newsfeed.api

 

GitHub - mmmmicha/newsfeed.api

Contribute to mmmmicha/newsfeed.api development by creating an account on GitHub.

github.com

 

1. 라이브러리 설치

Nestjs에서 mongoose를 사용하기 위해서는 아래와 같은 라이브러리들이 필요합니다.

  • mongoose
  • @nestjs/mongoose

아래 명령어를 통해 설치해줍니다.

npm i --save mongoose @nestjs/mongoose

 

2. forRoot 설정 (app.module.ts)

mongoose를 사용하기 위한 가장 기본적인 세팅은 바로 애플리케이션과 mongoDB의 연결입니다. mongoDB에 연결하기 위한 가장 기본적인 세팅을 app.module.ts 파일에 진행해주셔야 합니다. 아래 코드와 같이 진행해 주시면 되겠습니다.

// app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
// 이 부분입니다 --------------------------------------------------
    imports: [MongooseModule.forRootAsync({
        imports: [ConfigModule],
        inject: [ConfigService],
        useFactory: async (configService: ConfigService) => ({
            uri: configService.get<string>('MONGO_URI'),
            dbName: configService.get<string>('MONGO_DB')
        })
    }), 
    ConfigModule.forRoot({
        isGlobal: true,
    }), 
    ...,
    ]
// -------------------------------------------------------------
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule {}

 

코드를 보시면 configService가 mongooseModule에 주입되어 있는 것을 보실 수 있습니다. 이는 필수적이진 않겠지만 Nestjs 내에서 구현해 놓은 configuration이용방법임으로 사용을 권장합니다. 아래 링크를 통해 간단한 이용방법을 숙지하실 수 있습니다.

https://khjeong0423.tistory.com/7

 

[Nestjs] 간단하게 사용하는 Nestjs Configuration

Externally defined environment variables are visible inside Node.js through the process.env global. We could try to solve the problem of multiple environments by setting the environment variables separately in each environment. This can quickly get unwield

khjeong0423.tistory.com

 

3. Schema 생성

application과 mongoDB를 연결하는 세팅이 완료되었다면 본격적으로 Schema를 생성합니다. 이는 mongoDB의 Collection과 매핑이 됩니다. 

nestjs에서 mongoose사용을 위해 정의한 데코레이터 중 아래 2가지를 사용했습니다.

  • @Schema()
  • @Prop()

@Schema() 데코레이터는 앞서 말한 mongoDB의 Collection과 애플리케이션 내에서 Schema로 정의한 class를 매핑해 주는 역할을 합니다. 유의해야할 부분은 사용할 class 를 'User'라고 네이밍을 한 경우 collection으로는 'users'로 자동적으로 's'가 붙은상태로 생성이 됩니다.

 

@Prop() 데코레이터는 Collection내 Document들의 property로 매핑해주는 역할을 합니다. @Prop()의 parameter로 options를 설정할 수 있기 때문에 원하시는 options를 mongoose공식사이트를 통해 확인하여 사용하시면 되겠습니다.

 

아래코드는 스키마 예시입니다.

// user.model.ts

import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from "mongoose";

export type UserDocument = HydratedDocument<User>;

@Schema()
export class User {
    @Prop({
        unique: true,
        required: true,
    })
    email: string

    @Prop({
        unique: true,
        required: true
    })
    password: string

    @Prop()
    hashedRefreshToken: string

    @Prop({
        required: true
    })
    authorities: string[]
}

export const UserSchema = SchemaFactory.createForClass(User);

 

4. forFeature 설정

생성된 Schema를 특정 모듈에 사용하기 위해선 꼭 module.ts 내 명시를 해줘야 합니다.

MongooseModule.forFeature()는 여러 개의 스키마를 설정할 수 있게끔 배열로 파라미터를 받습니다.

아래 코드는 3가지의 스키마를 모듈 내 사용하기 위해 설정한 예시입니다.

import { Module } from '@nestjs/common';
import { SubscriptionController } from './subscription.controller';
import { SubscriptionService } from './subscription.service';
import { MongooseModule } from '@nestjs/mongoose';
import { Subscription, SubscriptionSchema } from 'src/model/subscription.model';
import { Page, PageSchema } from 'src/model/page.model';
import { News, NewsSchema } from 'src/model/news.model';

@Module({
    imports: [MongooseModule.forFeature([
        { name: Subscription.name, schema: SubscriptionSchema }, 
        { name: Page.name, schema: PageSchema },
        { name: News.name, schema: NewsSchema }
    ])],
    controllers: [SubscriptionController],
    providers: [SubscriptionService]
})
export class SubscriptionModule {}

 

5. Injection(주입)

이제 사전준비는 끝났습니다.

Schema를 사용하고자 하는 모듈 내 서비스에 주입하고 이를 사용합니다.

 

@InjectModel()이라는 데코레이터를 사용하여 주입명시를 합니다. 변수에 대한 타입명시는 mongoose라이브러리에 있는 Model을 이용하고 이에 대한 generic으로는 앞서 스키마를 정의할 때 정의해 둔 HydratedDocument를 사용합니다.

import { BadRequestException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { Subscription, SubscriptionDocument } from '../model/subscription.model';
import { CreateSubscriptionDTO } from './dto/createSubscription.dto';
import { Page, PageDocument } from '../model/page.model';
import { News, NewsDocument } from '../model/news.model';

@Injectable()
export class SubscriptionService {
    constructor(
        @InjectModel(Subscription.name) private readonly subscriptionModel: Model<SubscriptionDocument>,
        @InjectModel(Page.name) private readonly pageModel: Model<PageDocument>,
        @InjectModel(News.name) private readonly newsModel: Model<NewsDocument>
    ) {}

    async create(createSubscriptionDTO: CreateSubscriptionDTO): Promise<SubscriptionDocument | undefined> {
        const subs = await this.subscriptionModel.find({ userId: createSubscriptionDTO.userId, pageId: createSubscriptionDTO.pageId, deletedAt: null });
        const page = await this.pageModel.findById(createSubscriptionDTO.pageId);

        if (!page)
            throw new NotFoundException('not found page');

        if (subs.length > 0)
            throw new BadRequestException('already exists subscription');

        const newSub = new this.subscriptionModel(createSubscriptionDTO);
        return newSub.save();
    }

    async findSubscribedPages(userId: string): Promise<any[] | undefined> {
        const subs = await this.subscriptionModel.find({ userId: userId, deletedAt: null });

        const subsIds = subs.map(sub => sub.pageId);
        const subPageList = await this.pageModel.find({ _id: { $in: subsIds } }).lean();

        return subPageList.map(subPage => {
            return {
                ...subPage,
                subscriptionId: subs.filter(sub => sub.pageId === subPage._id.toString())[0]._id.toString()
            }
        });
    }
    
    ...
}

 

이렇게 하여 간단하게 mongoose를 적용했던 예시를 설명드렸습니다. 상황에 맞는 유동적인 다른 방법들도 공식사이트에 많이 설명되어 있습니다. 시간 내어 공식사이트도 확인해 보시길 추천드립니다.

 

감사합니다.