본문 바로가기

Backend/NestJS

[NestJS] NestJS 공식 문서 정독(2)(Dynamic Module)

728x90
모듈을 동적으로 생성하는 방법에 대한 기록이다.

개요

모듈에 관한 기록을 남긴 적이 있다.

 

[NestJS] Nest 모듈 설계

NestJS 모듈에 대한 공부 기록이다. 모듈이란? 모듈이란 클래스나 함수 같은 소프트웨어 컴포넌트가 아닌, 여러 컴포넌트를 조합한 큰 작업을 수행할 수 있게 하는 단위이다. 가령 배달 서비스가

choi-records.tistory.com

 

모듈은 전체 어플리케이션의 하나의 기능적 부분으로

이용하는 프로바이더와 컨트롤러와 같은 구성 그룹을 지정함으로써 실행 범위를 정의한다.

 

지금까지는 주로 정적인 모듈을 다뤄왔다.

정적인 모듈의 동작 과정을 다시 살펴보고 동적 모듈의 동작 방식을 알아보자.

정적 모듈

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

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

 

서비스 파일은 Injectable 데커레이터를 이용해 정의되었다고 해보자.

 

Nest는 어떻게 AuthService 프로바이더 클래스가 User Module 내부에서 사용될 수 있게 할까?

  1. AuthModule과 내부 프로바이더 등의 클래스를 인스턴스화한다.
  2. AuthModule에서 exports한 프로바이더를 다른 모듈에서 이용할 수 있게 한다.

이때 정적 모듈 바인딩의 단점은

해당 모듈의 프로바이더는 항상 같은 형태로 전달된다는 것이다.

따라서 소비 모듈에서 import되는 프로바이더의 구성 방식에 영향을 줄 수 없다.

 

이를 해결해 줄 수 있는 것이 바로 동적 모듈이다.

동적 모듈을 이용해 다른 모듈에 props에 따라 커스터마이징 된 모듈을 제공한다.

동적 모듈

동적 모듈의 대표적인 예시가 NestJS에서 제공하는 ConfigModule이다.

 

options 객체를 통해 폴더명을 전달하면 해당 폴더 하위의 .env 파일을 관리할 수 있도록 하는 기능을 구현해 보자.

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 {}

지정한 폴더 config 하위의 .env 파일 내 데이터를 사용 가능하게 만들려고 한다.

위의 동적 모듈의 동작 방식을 보자.

  1. ConfigModule은 일반 클래스이므로 register()라는 static 메서드가 있음을 알 수 있다.(인스턴스가 아닌 클래스에서 호출하기 때문에)
  2. register() 메서드는 options 객체를 받는다.
  3. register() 메서드는 결국 모듈 클래스를 반환해야 한다는 것을 알 수 있다.

register 메서드는 DynamicModule 인터페이스를 가지고,

동적 모듈은 module이라는 추가 prop을 가지는 것 말고는 정적 모듈과 다른 점이 없다.

export interface DynamicModule extends ModuleMetadata {
    /**
     * A module reference
     */
    module: Type<any>;
    /**
     * When "true", makes a module global-scoped.
     *
     * Once imported into any module, a global-scoped module will be visible
     * in all modules. Thereafter, modules that wish to inject a service exported
     * from a global module do not need to import the provider module.
     *
     * @default false
     */
    global?: boolean;
}

동적 모듈 API(register 메서드)는 단순히 모듈을 반환하지만

반환하는 모듈은 @Module 데커레이터를 수정하는 것이 아닌 프로그래밍 방식으로 지정한다.

 

이제 동적 모듈을 생성해 보자.

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

정적 모듈에서는 Module 데커레이터의 인자로 ModuleMetadata 인터페이스의 객체가 전달됐다.

Module 데커레이터는 클래스 데커레이터이므로 클래스의 생성자에 접근하고,

정적 모듈에서는 register 메서드 같은 커스터마이징을 할 필요가 없었기 때문이다.

 

반면 동적 모듈에서는 register 메서드를 지정해 줘야 하기 때문에

직접 클래스를 생성하고, register 메서드를 static 메서드로 정의해줘야 하는 것이다.

register 메서드에서는 위에서 볼 수 있듯 DynamicModule을 반환하고 있다.

 

register 메서드는 options 객체를 전달받는다.

이걸 어떻게 가져다가 쓸까?

 

사실 모듈은 기본적으로 다른 프로바이더가 사용할 주입 가능한 서비스를 제공하고 내보내는 호스트 역할이다.

그렇다면 options 객체를 읽어야 하는 것은 프로바이더인 ConfigService이다.

 

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];
  }
}

위는 options 객체를 읽고, env 파일에 접근하는 방법에 대한 비즈니스 로직이다.

하지만 위의 코드를 보면 ConfigService가 런타임에만 제공되는 options 객체에 의존하는 것을 알 수 있다.

따라서 런타임 시 먼저 옵션 객체를 Nest IOC 컨테이너에 바인딩한 후 ConfigService에 주입될 수 있도록 해줘야 한다.

 

그렇다면 ConfigModule의 providers 속성을 통해 options 객체를 프로바이더로 정의해 보자.

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

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

이제 상수 토큰을 이용해 options 객체를 프로바이더로 정의했기 때문에

ConfigService에 Inject 데커레이터를 이용해 options 객체를 주입할 수 있다.

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: Record<string, any>) {
    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];
  }
}

커스텀 프로바이더에 대한 기록은 아래를 참고하자.

 

[NestJS] NestJS 공식 문서 정독(1)(Custom Provider,Async Provider)

NestJS의 Fundamentals 부분을 정독해 보려고 한다. Custom Provider에 대한 첫 번째 정독 기록이다. 개요 DI(Dependency Injection) 지금까지는 의존성 주입으로 거의 생성자 기반 주입만을 다뤄봤다. 어플리케이

choi-records.tistory.com


메서드명 가이드라인

동적 모듈에서 쓰이는 메서드는 보통 forRoot, register, forFeature이 있다.

위의 메서드들을 구분하는데에는 Nest에서 권장하는 가이드라인이 존재한다.

  • register : 호출 모듈에서만 사용할 특정 구성으로 동적 모듈을 구성하려고 할 때 사용
  • forRoot : 동적 모듈을 한 번 구성하고 해당 config를 여러 위치에서 재사용할 때 사용
  • forFeature : forRoot와 유사한 config를 사용하지만 호출 모듈의 요구 사항(모듈이 액세스해야 하는 레포 또는 logger가 사용해야 하는 context)에 특정한 일부 구성을 수정해야 할 때 사용

참고

 

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

마치며

비동기 메서드를 가지는 동적 모듈은 추후에 기록하려고 한다.

 

잘못된 정보에 대한 피드백은 환영입니다.

감사합니다.

728x90