NestJS-config module

jaegeunsong97·2024년 2월 4일
0

NestJS

목록 보기
23/37

🖊️ENV 파일 작성

  • auth/const/auth.const.ts
export const PROTOCOL = 'http';
export const HOST = 'localhost:3000';

지금 우리는 환경변수를 다음과 같이 작성을 했습니다. 매우 중요한 정보이기 때문에 노출이 안되어야 합니다. 따라서 깃허브와 같은 곳에서는 깃허브에 올릴 때, gitignore를 해줘야 합니다.

그리고 nest.js에서는 이런 환경 변수를 잘 다룰 수 있도록 config 모듈을 제공해주고 있습니다. 이 모듈은 기본으로 설치되어 있는 모듈이 아니기 때문에 설치를 해줘야합니다.

yarn add @nestjs/config

일단 환경변수를 어떤 식으로 선언하는지 보도록 하겠습니다. 최상단에 .env파일을 생성합니다.

그리고 .env파일을 생성하면 가장 먼저 해야할 것이 gitignore에 등록을 해줘야 합니다. 깃에서 관리를 하지 못하게 만드는 것입니다. auth/common/const/env.const.ts에 있는 내용을 .env파일에 작성을 해줍니다.

또한 JWT관련 내용도 추가하겠습니다.

  • .env
JWT_SECRET=codefactory
HASH_ROUNDS=10
PROTOCOL=http
HOST=localhost:3000

그리고 app.module.ts를 보면 데이터베이스 내용들도 전부 하드코딩으로 박혀있습니다.

TypeOrmModule.forRoot({
    type: 'postgres',
    host: '127.0.0.1',
    port: 5433,
    username: 'postgresql',
    password: 'postgresql',
    database: 'postgresql',
    entities: [
        PostsModel,
        UsersModel,
    ],
    synchronize: true,
}),

이런 정보들도 PROD가 실행되면 보안을 위해서 숨겨야합니다. 이 정보 또한 env 파일로 옮기겠습니다.

  • .env
JWT_SECRET=codefactory
HASH_ROUNDS=10

PROTOCOL=http
HOST=localhost:3000

DB_HOST=localhost
DB_PORT=5433
DB_USERNAME=postgresql
DB_PASSWORD=postgresql
DB_DATABASE=postgresql

🖊️환경변수 적용

이제부터는 env파일에 적힌 환경변수를 사용해보겠습니다. 일단 app.module.ts에 Config 모듈을 등록해야합니다.

  • app.module.ts
@Module({
    imports: [
      	ConfigModule.forRoot({
            envFilePath: '.env', // nest.js에서 사용될 env 닉네임
            isGlobal: true, // AppModule에 설정해주면 어디서든 사용 가능
        }),
        TypeOrmModule.forRoot({
            type: 'postgres',
            host: '127.0.0.1',
            port: 5433,
            username: 'postgresql',
            password: 'postgresql',
            database: 'postgresql',
            entities: [
                PostsModel,
                UsersModel,
            ],
            synchronize: true,
        }),
        PostsModule,
        UsersModule,
        AuthModule,
        CommonModule,
    ],
    controllers: [AppController],
    providers: [AppService, {
        provide: APP_INTERCEPTOR,
        useClass: ClassSerializerInterceptor,
    }],
})
export class AppModule {}

그리고 환경변수들이 사용되는 부분을 전부 바꿔줍니다. 일단 auth.const.ts 파일을 삭제합니다. 그리고 에러가 나오는 부분을 전부 바꾸겠습니다.

  • auth.service.ts
@Injectable()
export class AuthService {

     constructor(
          private readonly jwtService: JwtService,
          private readonly usersService: UsersService,
          private readonly configService: ConfigService, // 추가
     ) {}
.
.
verifyToken(token: string) {
    try {
        return this.jwtService.verify(token, { 
          	secret: this.configService.get<string>('JWT_SECRET'), // 키값을 넣기 + 어떤 type을 가져올지 명시
        }); 
    } catch (error) {
      	throw new UnauthorizedException('토큰이 만료됐거나 잘못된 토큰입니다. ');
    }
}

근데 지금처럼 JWT_SECRET하는 것이 기분이 나쁩니다. 왜냐하면 이전에 작성한 것과 사실상 별 다른게 없기 때문입니다. 오타의 가능성도 있고 키값의 이름이 변경될 수 있기 때문입니다.

따라서 key값들을 정리하도록 하겠습니다. common/const에 새로운 파일을 생성하겠습니다.

  • common/const/env-keys.const.ts
// 서버 프로토콜 -> http / https
export const ENV_PROTOCOL_KEY = 'PROTOCOL';
// 서버 호스트 -> localhost:3000
export const ENV_HOST_KEY = 'HOST';
// JWT 토큰 시크릿 -> codefactory
export const ENV_JWT_SECRET_KEY = 'JWT_SECRET';
// JWT 토큰 해시 라운드 수 -> 10
export const ENV_HASH_ROUNDS_KEY = 'HASH_ROUNDS';
// 데이터베이스 호스트 -> localhost
export const ENV_DB_HOST_KEY = 'DB_HOST';
// 데이터베이스 포트 -> 5433
export const ENV_DB_PORT_KEY = 'DB_PORT';
// 데이터베이스 사용자 이름 -> postgresql
export const ENV_DB_USERNAME_KEY = 'DB_USERNAME';
// 데이터베이스 사용자 비밀번호 -> postgresql
export const ENV_DB_PASSWORD_KEY = 'DB_PASSWORD';
// 데이터베이스 이름
export const ENV_DB_DATABASE_KEY = 'DB_DATABASE';

이제 auth.service.ts를 바꿔줍니다.

  • auth.service.ts
verifyToken(token: string) {
    try {
        return this.jwtService.verify(token, { 
          	secret: this.configService.get<string>(ENV_JWT_SECRET_KEY), // 적용
        }); 
    } catch (error) {
      	throw new UnauthorizedException('토큰이 만료됐거나 잘못된 토큰입니다. ');
    }
}
.
.
rotateToken(token: string, isRefreshToken: boolean) {
    const decoded = this.jwtService.verify(token, {
      	secret: this.configService.get<string>(ENV_JWT_SECRET_KEY), // 적용
    });

    if (decoded.type !== 'refresh') throw new UnauthorizedException('토큰 재발급은 refresh 토큰으로만 가능합니다.');
    return this.signToken({
      ...decoded,
    }, isRefreshToken);
}
.
.
signToken(user: Pick<UsersModel, 'email' | 'id'>, isRefreshToken: boolean) {
    const payload = {
        email: user.email,
        sub: user.id,
        type: isRefreshToken ? 'refresh' : 'access',
    }
    return this.jwtService.sign(payload, {
        secret: this.configService.get<string>(ENV_JWT_SECRET_KEY), // 변경
        expiresIn: isRefreshToken ? 3600 : 300,
    });
}
.
.
async registerWithEmail(user: RegisterUserDto) {
    const hash = await bcrypt.hash(
        user.password,
        parseInt(this.configService.get<string>(ENV_HASH_ROUNDS_KEY)), // 변경
    );
    const newUser = await this.usersService.createUser({
        ...user,
        password: hash,
    });
    return this.loginUser(newUser);
}

이번에는 env.const.ts를 삭제하겠습니다.

  • common.service.ts
@Injectable()
export class CommonService {

  	// 추가
    constructor (
    	private readonly configService: ConfigService, 
    ) {}
.
.
private async cursorPaginate<T extends BaseModel>(
    dto: BasePaginationDto,
    repository: Repository<T>,
    overrideFindOptions: FindManyOptions<T> = {},
    path: string,
) {

    const findOptions = this.composeFindOptions<T>(dto);
    const results = await repository.find({
        ...findOptions,
        ...overrideFindOptions,
    });

    const lastItem = results.length > 0 && results.length === dto.take ? results[results.length - 1] : null;
	
  	// 변경
    const protocol = this.configService.get<string>(ENV_PROTOCOL_KEY);
    const host = this.configService.get<string>(ENV_HOST_KEY);
    const nextUrl = lastItem && new URL(`${protocol}://${host}/${path}`);
  	// 여기까지
  • posts.service.ts
@Injectable()
export class PostsService {

    constructor(
    @InjectRepository(PostsModel)
       private readonly postsRepository: Repository<PostsModel>,
       private readonly commonService: CommonService,
       private readonly configService: ConfigService, // 추가
    ) {}
.
.
async cursorPaginatePosts(dto: PaginatePostDto) {
    const where: FindOptionsWhere<PostsModel> = {};
    if (dto.where__id__less_than) {
      	where.id = LessThan(dto.where__id__less_than);
    } else if(dto.where__id__more_than) {
      	where.id = MoreThan(dto.where__id__more_than);
    }

    const posts = await this.postsRepository.find({
        where,
        order: {
          	createdAt: dto.order__createdAt,
        },
        take: dto.take,
    });

    const lastItem = posts.length > 0 && posts.length === dto.take ? posts[posts.length - 1] : null;
	
  	// 변경
    const protocol = this.configService.get<string>(ENV_PROTOCOL_KEY);
    const host = this.configService.get<string>(ENV_HOST_KEY);
    const nextUrl = lastItem && new URL(`${protocol}://${host}/posts`);
  	// 여기까지

잘 나오는지 포스트맨으로 테스트를 하겠습니다.

{
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImNvZGVmYWN0b3J5QGNvZGVmYWN0b3J5LmFpIiwic3ViIjoxLCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNzA3MDMzNTgyLCJleHAiOjE3MDcwMzM4ODJ9.28YvVZL4M2jfzIWnFyBvCOLMtvSCujZmRqsIac74YjM",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImNvZGVmYWN0b3J5QGNvZGVmYWN0b3J5LmFpIiwic3ViIjoxLCJ0eXBlIjoicmVmcmVzaCIsImlhdCI6MTcwNzAzMzU4MiwiZXhwIjoxNzA3MDM3MTgyfQ.uwOTeY3b70GQwSzPOBaEbH0Ch8Nxpn-u0gdWoHOjrN4"
}

{
    "data": [
        {
            "id": 101,
            "updatedAt": "2024-01-28T02:12:54.520Z",
            "createdAt": "2024-01-28T02:12:54.520Z",
            "title": "임의로 생성된 92",
            "content": "임의로 생성된 포수트 내용 92",
            "likeCount": 0,
            "commentCount": 0,
            "author": {
                "id": 1,
                "updatedAt": "2024-01-26T05:58:10.800Z",
                "createdAt": "2024-01-26T05:58:10.800Z",
                "nickname": "codefactory",
                "email": "codefactory@codefactory.ai",
                "role": "USER"
            }
        },
        .
        .
        {
            "id": 29,
            "updatedAt": "2024-01-28T02:12:53.979Z",
            "createdAt": "2024-01-28T02:12:53.979Z",
            "title": "임의로 생성된 20",
            "content": "임의로 생성된 포수트 내용 20",
            "likeCount": 0,
            "commentCount": 0,
            "author": {
                "id": 1,
                "updatedAt": "2024-01-26T05:58:10.800Z",
                "createdAt": "2024-01-26T05:58:10.800Z",
                "nickname": "codefactory",
                "email": "codefactory@codefactory.ai",
                "role": "USER"
            }
        }
    ],
    "total": 17
}

env 파일을 사용하고도 요청이 잘 오는 것을 확인할 수 있습니다.


🖊️process 객체를 이용한 환경변수 불러오기

이번에는 DB관련 환경변수를 사용하도록 하겠습니다. app.module.ts에서는 configService로 주입을 받아 가져올 수 가 없습니다. 따라서 process라는 글로벌 변수를 사용해서 process.env를 하면 환경변수를 가져올 수 있습니다.

  • app.module.ts
@Module({
    imports: [
        ConfigModule.forRoot({
            envFilePath: '.env',
            isGlobal: true,
        }),
        TypeOrmModule.forRoot({
            type: 'postgres',
          	// 변경
            host: process.env[ENV_DB_HOST_KEY],
            port: parseInt(process.env[ENV_DB_PORT_KEY]),
            username: process.env[ENV_DB_USERNAME_KEY],
            password: process.env[ENV_DB_PASSWORD_KEY],
            database: process.env[ENV_DB_DATABASE_KEY],
          	// 여기까지
            entities: [
                PostsModel,
                UsersModel,
            ],
            synchronize: true,
        }),
        PostsModule,
        UsersModule,
        AuthModule,
        CommonModule,
        ConfigModule,
    ],
    controllers: [AppController],
    providers: [AppService, {
        provide: APP_INTERCEPTOR,
        useClass: ClassSerializerInterceptor,
    }],
})
export class AppModule {}

저장하고 실행하면 터미널이 잘 실행되는 것을 알 수 있습니다. 하지만 잘 못 입력이 된 경우, 에러를 확인하기 위해 password를 빈값으로 해보겠습니다.

@Module({
    imports: [
        ConfigModule.forRoot({
            envFilePath: '.env',
            isGlobal: true,
        }),
        TypeOrmModule.forRoot({
            type: 'postgres',
          	// 변경
            host: process.env[ENV_DB_HOST_KEY],
            port: parseInt(process.env[ENV_DB_PORT_KEY]),
            username: process.env[ENV_DB_USERNAME_KEY],
            password: '', // 에러~~~~~~~~~~~~~~~~~~~~
            database: process.env[ENV_DB_DATABASE_KEY],
          	// 여기까지
            entities: [
                PostsModel,
                UsersModel,
            ],
            synchronize: true,
        }),
        PostsModule,
        UsersModule,
        AuthModule,
        CommonModule,
        ConfigModule,
    ],
    controllers: [AppController],
    providers: [AppService, {
        provide: APP_INTERCEPTOR,
        useClass: ClassSerializerInterceptor,
    }],
})
export class AppModule {}

페이지네이션 요청도 확인해보겠습니다.

{
    "data": [
        {
            "id": 108,
            "updatedAt": "2024-01-28T02:12:54.578Z",
            "createdAt": "2024-01-28T02:12:54.578Z",
            "title": "임의로 생성된 99",
            "content": "임의로 생성된 포수트 내용 99",
            "likeCount": 0,
            "commentCount": 0,
            "author": {
                "id": 1,
                "updatedAt": "2024-01-26T05:58:10.800Z",
                "createdAt": "2024-01-26T05:58:10.800Z",
                "nickname": "codefactory",
                "email": "codefactory@codefactory.ai",
                "role": "USER"
            }
        },
        .
        .
        {
            "id": 89,
            "updatedAt": "2024-01-28T02:12:54.431Z",
            "createdAt": "2024-01-28T02:12:54.431Z",
            "title": "임의로 생성된 80",
            "content": "임의로 생성된 포수트 내용 80",
            "likeCount": 0,
            "commentCount": 0,
            "author": {
                "id": 1,
                "updatedAt": "2024-01-26T05:58:10.800Z",
                "createdAt": "2024-01-26T05:58:10.800Z",
                "nickname": "codefactory",
                "email": "codefactory@codefactory.ai",
                "role": "USER"
            }
        }
    ],
    "cursor": {
        "after": 89
    },
    "count": 20,
    "next": "http://localhost:3000/posts?order__createdAt=DESC&take=20&where__id__less_than=89"
}

환경변수를 불러오는 방법은 2가지가 있습니다. process를 사용하는 방법이 있고, 이 방법보다 좋은 방법인 configService를 주입받아 get함수를 실행해서 가져오는 방법입니다.

profile
블로그 이전 : https://medium.com/@jaegeunsong97

0개의 댓글