Nest.js
TypeSafe한 ConfigModule

nest에서는 환경변수를 관리하기 위해 @nestjs/config에서 제공하는 configModule을 사용할 수 있다.

간단한 사용법

export const configuration = () => {
  const config = {
    port: Number(process.env.PORT) || 3000,
    db: {
      host: process.env.DB_HOST,
      port: Number(process.env.DB_PORT),
      user: process.env.DB_USER,
      password: process.env.DB_PASSWORD,
      name: process.env.DB_NAME,
    },
    hash: {
      time: Number(process.env.HASH_TIME) || 10,
    },
    jwt: {
      secert: process.env.JWT_SECRET,
    },
  };
 
  return config;
};
 
ConfigModule.forRoot({
  isGlobal: true,
  load: [configuration],
}),
  //예시
 
  this.configService.get("db.user");

문제점

ConfigService를 그냥 사용하기에는 두 가지 단점이 있다.

  • 타입을 위해 제네릭을 수동으로 입력해야 한다
  • get에 넘겨주는 인자의 타입이 string이다.
this.configService.get("port"); // any
 
this.configService.get<number>("port"); // number

다음과 같이 개발자가 수동으로 제네릭을 넘겨 configService 타입을 맞춰줘야 한다.

또한 get에서 받는 인자의 타입이 string이기 때문에 개발자가 configService의 환경변수 키 값을 전부 알고 있어야한다.

타입 시스템을 사용하는 nest에서 이는 불편하다고 생각해 방법을 찾아봤다.

해결책

타입 시스템을 사용하기 위해 ConfigModule을 사용하는 새로운 TypeSafeConfigModule을 만들었다.

@Global()
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [configuration],
    }),
  ],
  providers: [TypeSafeConfigService],
  exports: [TypeSafeConfigService],
})
export class TypeSafeConfigModule {}

AppModule에서 하던 것과 같이 TypeSafeConfigModule에서 ConfigModule을 import한다.

나는 만들어진 TypeSafeConfigModule을 글로벌 모듈로 사용할 거기 때문에 Global 데코레이터를 추가해 사용했다.

@Injectable()
export class TypeSafeConfigService {
  constructor(private readonly configService: ConfigService) {}
 
  get<T extends Leaves<ENVConfiguration>>(propertyPath: T): LeafTypes<ENVConfiguration, T> {
    return this.configService.get(propertyPath);
  }
}

TypeSafeConfigService에서는 configService를 inject해 제네릭을 통해 get메서드를 한번 감싸 인자와 반환값에 타입을 추가했다.

중첩 객체에 키에 대한 값을 타입으로 사용하기 위해 다음 타입이 필요하다.

export type Leaves<T> = T extends object
  ? {
      [K in keyof T]: `${Exclude<K, symbol>}${Leaves<T[K]> extends never ? "" : `.${Leaves<T[K]>}`}`;
    }[keyof T]
  : never;
 
export type LeafTypes<T, S extends string> = S extends `${infer T1}.${infer T2}`
  ? T1 extends keyof T
    ? LeafTypes<T[T1], T2>
    : never
  : S extends keyof T
    ? T[S]
    : never;

이제 TypeSafeService를 사용하면 인자로 받는 키값도 자동완성이 가능하고 타입도 알아서 추론한다.

참고자료

Typed ConfigService in NestJS (opens in a new tab)