Nest.js 좀 더 자세한 모듈

오병진·2022년 10월 6일
7

Nest.js의 모듈은 평범하게 다음과 같이 생겼습니다.

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

그리고 imports 부분에 값들이 다양하게 들어가기 시작합니다.

@Module({
  imports: [
  	ConfigModule.forRoot()
  ],
})

또는

import someConfig from "./config/some.config";

@Module({
  imports: [
  	ConfigModule.forFeature(someConfig)
  ],
})

그리고

@Module({
  imports: [
  	ConfigModule.register({ folder: './config' })
  ],
})
export const AppModule
@Module({})
export class ConfigModule {
  static register(): DynamicModule {
    return {
      module: ConfigModule,
      providers: [ConfigService],
      exports: [ConfigService],
    };
  }
}

등 그냥 Moduleimport 해 올 것이지
뭐가 메서드가 그냥 많습니다.

  • forRoot/forRootAsync
  • register/registerAsync
  • forFeature/forFeatureAsync

위 세가지 내용을 정리하자면

forRoot/forRootAsync

이는 가장 처음에 연결해야하는 것들
그리고 대부분의 영역에서 사용될 것들입니다.

ConfigModule.forRoot()
TypeOrmModule.forRoot({})
MongooseModule.forRoot('mongodb://localhost/nest')
GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
    })

대부분이 기초 설정에 의거한 내용들이더라고요.
물론 인자값을 전달하기에 동적모듈을 리턴합니다.

동적모듈이라고 어려울게 아닌, 그저 그냥 모듈 구성에 인자값으로 인한 변화만 있을뿐이다
라고 생각하시면 좋습니다.

register/registerAsync

이는 동적 모듈을 제공하기 위해서 사용됩니다.

register에 넣어주는 인자값에 의하여 제공되는 모듈의 구성이 다른거죠.

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

다음처럼 제공자에 동적인 값들을 넣어 줄 수 있습니다.

그리고 그것은 다음과 같은 결과를 가져올 가능성이 있습니다.

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

정리하자면, register는 동적모듈을 제공하기위해 존재합니다.
물론 직접 선언하고 관리하기에 좀 다르게 할 수는 있지만
적어도 동적모듈을 제공하기 위해서는 forRoot() 또는 register()라는 컨벤션을 추천합니다.
협업을 위해서요 :D

forFeature/forFeatureAsync

이는 자체 주입토큰이 있는 동적 공급자를 생성하기 위해서 사용합니다.

위의 forRoot, register와는 다르게 말이죠

예를들어 TypeOrm을 봅시다.
이 친구는 forRoot로 커넥션을 생성한 이후, 각 모듈에 사용하는 엔터티들을 각각 지정해주어야 합니다.
다음처럼요

@Module({
  imports: [TypeOrmModule.forFeature([Photo])],
  providers: [PhotoService],
  controllers: [PhotoController],
})
export class PhotoModule {}

PhotoModule에서는 Photo라는 엔터티를 사용하겠다는 의미입니다.
그리고 이 덕분에 레포지토리를 DI 할 수 있어집니다.

@Injectable()
export class PhotoService {
  constructor(
    @InjectRepository(Photo)
    private readonly photoRepository: Repository<Photo>,
  ) {}

  findAll(): Promise<Photo[]> {
    return this.photoRepository.find();
  }
}

만약 다음과 같은 커스텀 레포지토리를 사용한다면
importsrepository를 가져와야합니다.

@EntityRepository(Author)
export class AuthorRepository extends Repository<Author> {}
@Module({
  imports: [TypeOrmModule.forFeature([AuthorRepository])],
  controller: [AuthorController],
  providers: [AuthorService],
})
export class AuthorModule {}
@Injectable()
export class AuthorService {
  constructor(private readonly authorRepository: AuthorRepository) {}
}

Module 설정 시 나오는 친구들

가끔 useFactory와 같이 Module을 설정 할 때 튀어나오는 친구들이 있습니다.

  • useFactory
  • inject
  • async/await
  • useClass
  • useExisting

예를 들어 다음과 같이 TypeOrm 옵션을 비동기적으로 전달하는 경우입니다.

useFactory

TypeOrmModule.forRootAsync({
  useFactory: () => ({
    type: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: 'root',
    database: 'test',
    entities: [__dirname + '/**/*.entity{.ts,.js}'],
    synchronize: true,
  }),
});

그리고 비동기이거나 DI가 될 수도 있습니다.

inject, async

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (config: ConfigService) => config.get('database'),
  inject: [ConfigService],
})

그리고 클래스를 집어넣기도 합니다

useClass

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useClass: TypeOrmConfigService,
  inject: [ConfigService],
}),

이 경우 TypeOrmConfigService의 객체를 생성하고, 그를 이용하여 옵션개체를 생성합니다.

@Injectable()
class TypeOrmConfigService implements TypeOrmOptionsFactory {
  createTypeOrmOptions(config: ConfigService): TypeOrmModuleOptions {
    return config.get('database');
  }
}

왜 하필 createTypeOrmOptions가 메서드명이냐 를 찾아보니
useClass는 다음과 같은 형태의 값을 취하며

useClass : Function {
  new (...args: any[]): TypeOrmOptionsFactory;
}

그러기에 생성자가 TypeOrmOptionsFactory를 리턴해야만 합니다.
그리고 TypeOrmOptionsFactory는 다음과 같습니다.

interface TypeOrmOptionsFactory {
    createTypeOrmOptions(connectionName?: string): Promise<TypeOrmModuleOptions> | TypeOrmModuleOptions;
}

고로 implements TypeOrmOptionsFactory를 하여 createTypeOrmOptions를 구현해야 하는 것 입니다.

그리고 TypeOrmModuleOptions는 저희가 위에서 그토록 보던

{
  type: 'mysql',
  host: 'localhost',
  port: 3306,
  username: 'root',
  password: 'root',
  database: 'test',
  entities: [__dirname + '/**/*.entity{.ts,.js}'],
  synchronize: true,
}

다음과 같은 값들이 포함되어 있습니다.

어으 길다

useClassuseFactory에 관해 더 자세한건 Provider에서 계속..!

profile
지나가는 사람입니다. 마저 지나갈게요 :D

0개의 댓글