Nest에서 다이나믹 모듈(Dynamic-module) 맛만 보기

InnomDB·2022년 6월 1일
4

Nest

목록 보기
3/6
post-thumbnail

모듈이란 개념부터 확실히 잡고 가겠습니다!
Nest에서 모듈은 @Moudle()은 데코레이터로 주석이 달린 클래스입니다.
모듈 자체 대해 좀 더 자세한 설명을 원하시면 Nest Module에 들어가셔서 학습하시길 바랍니다. 이 포스팅은 다이나믹 모듈에 대해서만 다룰 예정입니다.

동적 모듈(Dynamic module)에 대해서 알기 전에 우선 정적 모듈(Static module)에 대해서 알아보겠습니다.

정적 모듈 먼저 알아보기

일반적으로 우리가 사용하는 모듈들은 정적 모듈입니다.
코드로 살펴보겠습니다.

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule],
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}

Nest 모듈 예제를 볼 때에도 위와 같은 형식으로 많이 사용됩니다.
AuthModule에서 UsersModule에서 내보낸 providers를 사용하는 형식입니다.
이런 구성을 하게 된다면 아래의 코드 처럼 의존성을 주입하실 수 있습니다.

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}
  /*
    Implementation that makes use of this.usersService
  */
}

AuthService에서 UsersModule에서 내보낸 UsersService를 생성자 의존성 주입을 시킬 수 있습니다.
이것을 정적 모듈 바인딩(static module binding)이라 합니다.

어떻게 UsersService가 AuthService에서 사용될 수 있는지에 대한 과정을 간단하게 Nest에서는 설명하고 있습니다.
1. UsersModule에서 공급자(providers)에 적힌 것들을 인스턴스화(토큰화) 시킵니다.
2. 인스턴스화(토큰화)하고 내보낸 공급자(providers) 여기서는 UsersService를 AuthModule에서 선언한 것처럼 AuthModule의 구성요서에 사용할 수 있게 됩니다.
이유는 AuthModule에서 UsersModule을 import해주었기 때문입니다.
3. 이제 AuthService에 UsersService 인스턴스를 삽입합니다.

핵심은 인스턴스를 가져다 쓰는 것입니다.

다이나믹 모듈은 뭐가 다른데?

위에서 사용했던 정적 모듈 바인딩방식은 소비 모듈(위에서는 AuthModule)이 공급자(UsersService)가 구성되는 방식에 영향을 줄 수 없습니다.

Why does this matter?

와이 더즈 디스 매러!! 이것이 왜 중요합니까!! 왜 왜 왜 우리는 다이나믹 모듈을 알야할까요?

다른 사용 사례에서 다르게 동작해야하는 범용 모듈이 필요할 때가 있기 때문입니다. 쉽게 말하면, 같은 코드로 사용 방식을 다르게 할 수 있기 때문입니다.
이렇게만 들으면 감이 잘 안잡히실 텐데 실제 사용 사례로는 Nest의 ConfigModule이 있습니다. 환경변수에 따라서 다르게 동작하는 Module입니다. 코드로 확인해보겠습니다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [ConfigModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

정적 모듈입니다. 어떻게 해야 이 정적 모듈을 매개변수를 통해 동작을 변경 시킬 수 있을까요? 위에서 정적 모듈은 프로바이더를 인스턴스화 시켜 전달한다고 했습니다.이미 인스턴스화가 되었는데 입맛에 맞게 사용이 가능할까요?? 위에서 Module은 Class라 하였고 인스턴스는 Class가 만든 객체입니다. 즉 이미 Class라는 틀을 통해 실체화가 되었습니다.
동적 모듈을 작성하기 위해서는 인스턴스가 아닌 클래스 호출 방식을 사용합니다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [ConfigModule.register({ folder: './config' })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

정적 모듈 바인딩과 차이는 imports안의 배열을 보시면 알 수 있습니다.
ConfigModule.register() Class안의 메서드를 호출 하고 매개변수를 넘기고 있습니다.
우리가 이 코드로 알 수 있는 사실은 무엇일까요?

  1. Class의 인스턴스가 아닌 ConfigModule 클래스에서 호출하기 때문에 static method(정적 메서드)인 register()를 호출 한다. register()는 임의의 이름을 가질 수 있지만 관례상 forRoot() 또는 register()라고 합니다.
  2. register() 메서드는 사용자가 정의하므로 원하는 모든 인수를 사용할 수 있습니다.
  3. 토큰화 시킵니다. (이 부분은 밑에 예시와 함께 알아보겠습니다.)

정리하자면, 클래스 호출 방식으로 정적 메서드를 통해 인자를 전달해 정적 메서드 안에서 인수를 통한 사용자 정의 모듈이 되는 것 입니다. 그리고 마지막으로 토큰화 시켜서 내보냅니다.

자, 이제 여태까지 배웠던 것을 기반으로 options 개체를 어떻게 사용해야 할지 알아보겠습니다.

ConfigModule 내부 구조 알아보기

Config Module은 기본적으로 다른 공급자가 사용할 수 있도록 주입 가능한 서비스(ConfigService)를 제공하고 내보내기 위한 호스트입니다. 동작을 사용자 정의하기 위해 옵션 개체를 읽어야 하는 것은 실제로 우리의 Config Service입니다
잠시 register() 메서드의 옵션을 ConfigService로 전달했다고 가정하고 코드를 살펴보겠습니다.

import { Injectable } from '@nestjs/common';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { EnvConfig } from './interfaces';

@Injectable()
export class ConfigService {
  private readonly envConfig: EnvConfig;

  constructor() {
    const options = { folder: './config' };

    const filePath = `${process.env.NODE_ENV || 'development'}.env`;
    const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
    this.envConfig = dotenv.parse(fs.readFileSync(envFile));
  }

  get(key: string): string {
    return this.envConfig[key];
  }
}

ConfigService는 .env에서 파일을 찾을 수 있게 되었습니다.
하지만 이건 options을 직접 정의해서 사용했기 때문에 의미가 없습니다. 실제로는 register()에서 받은 options을 ConfigService로 주입해야합니다.
이 부분은 커스텀 프로바이더를 참고하시면 됩니다.
이렇게 의존성을 주입하게 된다면 ConfigService는 런타임에만 제공되는 옵션 개체에 따라 달라집니다. 따라서 런타임에 먼저 옵션 개체를 Nest IoC 컨테이너에 바인딩한 다음 Nest가 Config Service에 삽입하도록 해야 합니다.

IoT 컨테이너에 바인딩 하기

import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({})
export class ConfigModule {
  static register(options): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,
        },
        ConfigService,
      ],
      exports: [ConfigService],
    };
  }
}

사용하는 방식은 Non-class-based provider tokens입니다.
이제 'CONFIG_OPTIONS' 공급자를 ConfigService에 주입할 것입니다.
그 방식은 @Inject를 사용하면 됩니다.

import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { Injectable, Inject } from '@nestjs/common';
import { EnvConfig } from './interfaces';

@Injectable()
export class ConfigService {
  private readonly envConfig: EnvConfig;

  constructor(@Inject('CONFIG_OPTIONS') private options) {
    const filePath = `${process.env.NODE_ENV || 'development'}.env`;
    const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
    this.envConfig = dotenv.parse(fs.readFileSync(envFile));
  }

  get(key: string): string {
    return this.envConfig[key];
  }
}

여기서는 간단하게 하기 위해서 문자열로 'CONFIG_OPTIONS이라고 했지만 실제 사용 패턴에서는 이 방식보다는 아래의 방식을 추천드립니다.

export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';

별도의 파일에서 상수로 정의하고 해당 파일을 가져와서 사용하는 패턴입니다.

여태까지 간단하게 Nest에서 제공하는 doc을 쉽게 풀이해서 설명해보았습니다. 최대한 쉽게 설명한다고 해봤지만 제 나름대로의 해석이 섞여 틀린 내용이 있을 수도 있습니다. 끝까지 읽어 주셔서 감사합니다 ^^.

++번외) 직접 다이나믹 모듈 만들어보기!!
다이나믹 모듈 맛 보기 코드
위 링크에 전체 코드가 적혀있습니다.

JwtModule을 만들어보았습니다. 관례상 static method의 이름은 forRoot()로 지었습니다.

privateKey가 맞다면 Hi~ 틀리다면 Bye~가 나오게 구성했습니다..

자세한건 링크를 들어가 코드를 보시면 될 것 같습니다.. 모르는 부분이 생긴다면 댓글 달아주세요!

참고 사이트
다이나믹 모듈에 대해서 더 깊이 알고 싶으시다면 아래의 링크를 추천드립니다.
Nest 고급 다이나믹 모듈

++) 22/6/10(금) 직접 만들어보는 다이나믹 모듈

profile
이노오오옴

0개의 댓글