개발자 도전기/[STUDY] JS || TS

NestJS | NestJS와 DB 연결, 환경 변수 설정, DTO, 회원가입 기능 구현

답수 2023. 2. 27. 21:59
728x90
반응형

 

 

MongoDB 연동 및 셋업

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 Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

 

NestJS에서 MongoDB를 이용하려고 문서를 보는데, node에서 사용하는 ODM은 Mongoose로만 가능한줄 알았는데 typeORM으로도 사용이 가능하다고 한다. 하지만 가장 많이 활용되고 그만큼 편리한(무엇보다 현업에서 내가 사용하고 있기도 하고...) 몽구스를 이용해보려고 한다.

 

Mongoose는 스키마리스한 mongodb에 정해진 Schema 문법을 통해 스키마에 강제성을 준다. 즉 유연함이라는 강점이 오히려 독이 될 수 있는 몽고디비에 안정성 측면에서도 꽤 도움이 되는 아주 유용한 도구라고 보면 된다. Mongoose에 더 자세하게 알고 싶다면 제로초님의 글 참고하면 좋을 듯.

 

MongoDB 및 환경변수 설정

 

mongoose 설치 명령어
$ npm i @nestjs/mongoose mongoose

 

참고로 MongoDB를 이용하기 위해서 MongoDB Atlas와 MongoDB Compass를 사용한다. 해당 프로그램의 설치와 사용 방법에 대한 글은 패스!

 

 

다음으로 Nest에서 환경 변수 설정을 위해 해당 패키지를 설치한다.

Configuration 설치 명령어
$ npm i --save @nestjs/config

express에서는 dotenv 패키지 설치한 이후, 이 모듈을 import해서 dotenv.config();와 같이 따로 코드를 입력하여 등록했어야 했는데, Nest에서는 기본적으로 config 모듈을 제공한다.

 

몽구스와 환경변수 설정은 다음과 같은 설정으로 의존성을 주입할 수 있다.

// src/app.module.ts
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import mongoose from 'mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CatsModule } from './cats/cats.module';
import { LoggerMiddleware } from './common/middlewares/logger/logger.middleware';

@Module({
  imports: [
    ConfigModule.forRoot(),  // <--- 여기!!!
    MongooseModule.forRoot(process.env.MONGODB_URI),  // <--- 여기!!!
    CatsModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  private readonly isDev: boolean = process.env.MODE === 'dev' ? true : false;
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*');
    mongoose.set('debug', true);
  }
}

ConfigModule에서 forRoot() 메서드는 환경 변수들을 읽는 get()메서드를 제공하는ConfigService 공급자를 등록한다.

MongooseModule에서 forRoot() 메서드는 몽구스 패키지의 mongoose.connect() 와 동일한 구성 객체를 받는다.

 

Model injection

위에서 말했다시피 몽구스는 모두 스키마에 의해 작성된다. 몽구스에 의해 작성된 스키마는 몽고디비의 컬렉션에 매핑되고 그 컬렉션 내에 있는 도큐먼트를 정의한다. 즉 스키마(Schema)는 모델(Models)을 정의하는데 사용된다. 모델은 DB로부터 도큐먼트를 생성하고 조회하는 역할을 한다.

 

이제 cat schema를 작성해보자.

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
import { Document, SchemaOptions } from 'mongoose';

const options: SchemaOptions = {
  timestamps: true,
  versionKey: false,
};

@Schema(options)
export class Cat extends Document {
  @Prop({
    required: true,
    unique: true,
  })
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @Prop({
    required: true,
  })
  @IsString()
  @IsNotEmpty()
  name: string;

  @Prop({
    required: true,
  })
  @IsString()
  @IsNotEmpty()
  password: string;

  @Prop()
  @IsString()
  imgUrl: string;

  readonly readOnlyData: {
    id: string;
    email: string;
    name: string;
  };
}

export const CatSchema = SchemaFactory.createForClass(Cat);

@Schema() 데코레이터를 통해 스키마의 옵션들을 설정할 수 있다. 각자 프로젝트에 맞게 커스텀하면 될 듯 하다. 

@Prop() 데코레이터는 도큐먼트의 속성을 정의한다. 위의 email 속성을 예시로 들자면, required: true 를 통해 반드시 이메일 값이 존재해야 한다는 것이고, unique: true를 통해 이메일 값은 유니크한 값이 되어야 한다는 것이다.

그외에 @IsEmail(), @IsString() 등의 데코레이터는 class-validator 패키지에서 받아와서 사용하는 것으로, 클래스 내의 속성들을 유효성 검사할 수 있는 것들이다. 되게 유용하게 사용될 것 같다. 이 패키지를 사용하기 위해서는 main.ts에서 글로벌파이프로 등록해야 한다.

// src/main.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/exceptions/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());  // <--- HERE!!!!!!!!
  app.useGlobalFilters(new HttpExceptionFilter());
  const PORT = process.env.PORT;
  await app.listen(PORT);
}
bootstrap();

 

DTO(Data Transfer Objecrt)

DTO란 계층간 데이터 교환을 위한 객체를 뜻한다. 나한테는 되게 낯선 개념이다. 기존에 express로 작업할 때는 브라우저로부터 데이터를 받으면 컨트롤러 레이어에서 이를 받고, 해당 데이터가 유효하다면 그 데이터를 서비스 레이어로 직접 전달하는 방식으로 진행했었다.

 

DTO를 사용하게 된다면 데이터를 각 레이어 계층에서 직접 전송하지 않고, DTO를 보내게 된다. 이미지로 본다면 다음과 같다.

이미지대로라면 클라이언트에서 바디에 데이터를 심어서 컨트롤러로 전송할 때 DTO객체로 만들어서 밸리데이션, 타이핑 검사를 하고 컨트롤러로 보내고, DTO를 다시 서비스로 보내는 등 다른 레이어로 데이터를 전송할 때 dto를 사용하게 된다.

 

그렇다면 dto를 사용하는 이유가 뭘까? 이 레퍼런스를 보면 왜 DTO를 사용해야 하는지에 대해서 나온다.

There are a few other important concepts in Nest. js:

  • DTO: Data transfer object is an object that defines how data will be sent over the network.
  • Interfaces: TypeScript interfaces are used for type-checking and defining the types of data that can be passed to a controller or a Nest service.

음. 뭔가 부족하다. 여튼 위의 글들로 알 수 있는 dto의 필요성으로는 데이터의 형식을 일관성있게 다룰 수 있고, 이를 통해 잘못된 데이터 형식을 전송하는 것을 방지할 수 있다는 것이다. 또한 컨트롤러 레이어와 서비스 레이어 사이의 의존성을 줄일 수도 있을 것이다.

아직 익숙하지는 않지만, 만약 dto 사용에 적응이 된다면 데이터의 형식을 명확하게 정의하기 때문에 코드 가독성와 유지보수에도 유리할 것 같다. 

 

회원가입을 위한 dto로 다음과 같이 작성했다.

dto를 클래스로 작성한 이유? 데코레이터 패턴을 적용할 수도 있고 상속하여 재사용성을 증가시킬 수도 있음
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

export class CatRequestDto {
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @IsNotEmpty()
  password: string;

  @IsString()
  @IsNotEmpty()
  name: string;
}

 

회원가입 Controller 작성

// src/cats/cats.controller.ts

import { Body, Controller, Get, Post, UseInterceptors } from '@nestjs/common';
import { SuccessInterceptor } from 'src/common/interceptors/success.interceptor';
import { CatsService } from './cats.service';
import { CatRequestDto } from './dto/cats.request.dto';

@Controller('cats')
@UseInterceptors(SuccessInterceptor)
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Post()
  async signUp(@Body() body: CatRequestDto) {
    return await this.catsService.signUp(body);
  }
  // ...
}

위에서 작성했던 Dto 클래스를 signUp() 메서드의 body의 타입으로 정의한다.

 

회원가입 Service 작성

// src/cats/cats.service.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import * as bcrypt from 'bcrypt';
import { Cat } from './cats.schema';
import { CatRequestDto } from './dto/cats.request.dto';

@Injectable()
export class CatsService {
  constructor(@InjectModel(Cat.name) private readonly catModel: Model<Cat>) {}

  async signUp(body: CatRequestDto) {
    const { email, name, password } = body;
    const isCatExist = await this.catModel.exists({ email });

    if (isCatExist) {
      throw new UnauthorizedException('Already exists the cat!');
    }

    const hashedPassword = await bcrypt.hash(password, 10);

    const cat = await this.catModel.create({
      email,
      name,
      password: hashedPassword,
    });

    return cat.readOnlyData;
  }
}

서비스 레이어에서 DB에 접근하기 위해서는 모델을 서비스에 주입해야 한다. 이를 이해서 @InjectModel() 데코레이터를 사용한다.

 

테스트 후 콤파스에 들어와봤다. 데이터가 차곡차곡 잘 쌓인다. 

728x90
반응형
LIST