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관련 내용도 추가하겠습니다.
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 파일로 옮기겠습니다.
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 모듈을 등록해야합니다.
@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
파일을 삭제합니다. 그리고 에러가 나오는 부분을 전부 바꾸겠습니다.
@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에 새로운 파일을 생성하겠습니다.
// 서버 프로토콜 -> 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를 바꿔줍니다.
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를 삭제하겠습니다.
@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}`);
// 여기까지
@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 파일을 사용하고도 요청이 잘 오는 것을 확인할 수 있습니다.
이번에는 DB관련 환경변수를 사용하도록 하겠습니다. app.module.ts에서는 configService로 주입을 받아 가져올 수 가 없습니다. 따라서 process
라는 글로벌 변수를 사용해서 process.env
를 하면 환경변수를 가져올 수 있습니다.
@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함수를 실행해서 가져오는 방법입니다.