nest.js는 제가 백엔드 개발을 시작하고 가장 처음 접한 프레임워크이자 가장 많이 사용한 프레임워크입니다.
지금으로서는 이 nest.js를 통해 서버를 만드는데 익숙해졌지만, nest.js를 왜 사용하고 어떤 점이 장점인지 고민해본 기억이 적은 것 같습니다
따라서 nest.js의 특성과 장단점을 적어보고 이 프레임워크를 사용한 이유와 사용했던 경험에 대해 리뷰해볼 생각입니다
nest.js는 node.js를 이용한 서버 개발에 있어서 아키텍처의 문제를 해결하기 위한 프레임워크입니다
node.js 진여의 서버 개발에 있어서 이미 사용하기 쉽고 성능이 뛰어난 Express.js가 존재합니다. 그러나 지나치게 자유롭다는 단점이 있습니다
아래는 yukina1418의 벨로그에 정리가 너무 잘되어있어서 발췌한 내용입니다
@Controller
데코레이터를 통해 다음과 같이 경로를 설정합니다@Controller({ path: 'feed', version: 'v1' })
export class FeedController {
constructor(private readonly feedService: FeedService) {}
}
@Get
,@Post
와 같은 핸들러의 인수에 따라nestjs의 모든 데이터 처리 및 비즈니스 로직을 담당합니다
계층형 구조(레이어드 아키텍쳐)라는 기법을 사용합니다
간단히 말해 작업을 종류별로 나누어서 역량을 집중하게 하는것이라고 보면 됩니다
제어 역전(IoC)과 의존성 주입(DI)
제어 역전(Inversion of Control)
const sweetBeanBoong = new boong(new sweetBean())
const creamBoong = new boong(new cream())
의존성 주입 (Dependency Injection)
nestjs 프로바이더의 주요 개념은 의존성을 주입할 수 있다는 것입니다
이 뜻은 서로가 다양한 관계를 만들 수 있다는 것을 의미하며 이 연결 기능을 nest 시스템이 담당해준다고 할 수 있습니다
컨트롤러가 http 요청을 처리한다면 이보다 복잡한 일은 프로바이더에게 위임하는 것입니다
위의 sweetBean()
와 같이 주입당하는 객체를 바로 프로바이더라고 할 수 있습니다
프로바이더는 역할에 따라 그 이름이 다음과 같이 달라집니다
처리 과정은 아래 이미지와 같습니다
1. 미들웨어
2. 가드에서 요청이 해당 api에 접근 가능한 것인지 확인
3. pipe에서 클라이언트에서 요청과 함께 보낸 데이터를 원하는 형태로 가공
4. 핸들러 혹은 서비스에서 필요한 작업(비즈니스 로직)을 수행
5. 이 과정에서 발생한 예외처리를 Exception filter가 담당
이렇게 과정이 나뉘게 됩니다
위와 같은 이름을 가진 provider들을 활용한 경험을 기록해보겠습니다
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
/*
- jwt 인증이 유효한지 확인한다.
*/
@Injectable()
export class JwtAccessGuard extends AuthGuard('access') {}
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtAccessStrategy extends PassportStrategy(Strategy, 'access') {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// 헤더에서 액세스 토큰을 가져와 검증
secretOrKey: process.env.JWT_ACCESS_KEY,
passReqToCallback: true,
});
}
async validate(req, payload) {
const accessToken = await req.headers.authorization.split(' ')[1];
if (!accessToken) throw new UnauthorizedException('액세스 토큰이 없습니다');
return { email: payload.email, createdAt: payload.createdAt };
}
}
전략패턴이란?
- 전략 패턴은 객체의 행위를 바꾸고 싶은 경우 이를 '직접' 수정하지 않고 '전략'이라고 부르는 '캡슐화한 알고리즘'을 컨텍스트 내에서 바꿔주는 패턴을 말합니다
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-jwt';
@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'refresh') {
constructor() {
super({
jwtFromRequest: (req) => req.headers.cookie.replace('refreshToken=', ''),
// 쿠키에서 리프레시 토큰을 가져와 검증
secretOrKey: process.env.JWT_REFRESH_KEY,
passReqToCallback: true,
});
}
async validate(req, payload) {
const refreshToken = await req.headers.cookie.replace('refreshToken=', '');
if (!refreshToken)
throw new UnauthorizedException('리프레시 토큰이 없습니다');
return { email: payload.email, createdAt: payload.createdAt };
}
}
@Module({
imports: [TypeOrmModule.forFeature([Feed, FeedLike, User]), CqrsModule],
controllers: [FeedController],
providers: [FeedService, UserService, FetchFeedQueryHandler, JwtAccessGuard],
})
export class FeedModule {}
// 피드 생성 API
//@UseGuards로 JwtAccessGuard 사용한 사례
@Post()
@UseGuards(JwtAccessGuard)
createFeed(
@CurrentUser() currentUser: ICurrentUser,
@Body(ValidationPipe) createFeedInput: CreateFeedInput,
): Promise<Feed> {
return this.feedService.create({ currentUser, createFeedInput });
}
// 액세스 토큰 복구 API
// @UseGuards로 JwtRefreshGuard 사용한 사례
@Post('restoreAccessToken')
@UseGuards(JwtRefreshGuard)
restoreAccessToken(
@CurrentUser() currentUser: ICurrentUser, //
): Promise<string> {
return this.authService.getAccessToken({ user: currentUser });
}
}
가드가 끝났으면 다음 차례는 파이프입니다
파이프는 클라이언트로부터 온 데이터를 처리해주는 역할입니다
문제가 생기면 Error를 뱉고 통과한다면 처리된 데이터가 비즈니스 로직으로 들어가게 됩니다
파이프는 data-transformation과 data-validation을 위해 사용됩니다.
- Data Transformation
입력 데이터를 원하는 형식으로 변환하는 것을 말한다. 가령 문자열에서 정수로 바꾸는 것을 의미한다.- Data Validation
유효성 체크로서, 입력 데이터를 평가하고 유효한 경우 변경되지 않은 상태로 전달된다. 그렇지 않으면 데이터가 올바르지 않을 때 예외를 발생시킨다.
제가 사용한 기능은 이중에서 data-validation, 즉 올바르지 않은 데이터가 들어왓을 때 예외처리를 해주는 기능을 적용했습니다
다만 provider에서 사용하진 않고 다음과 같이 main.ts에 useGlobalPipes()를 활용해 global로 적용했습니다
app.useGlobalPipes(
new ValidationPipe({
transform: true,
enableDebugMessages: true,
exceptionFactory(errors) {
const message = Object.values(errors[0].constraints);
throw new BadRequestException(message[0]);
// 예외처리 : 이렇게 해두면 어떤 인풋의 타입에러가 발생했는지를
// 에러 메시지를 통해 보여줍니다
},
}),
);
사용한 파이프는 ValidationPipe로 빌트인 파이프를 사용했습니다
이 파이프를 사용하기 위해선 class-validator
, class-transformer
모듈을 설치해야 합니다
이렇게 글로벌로 적용한 파이프가 작동되기 위해서는 해당 dto에 다음과 같이 class-validator
의 모듈에서 임포트한 데코레이터를 추가해줘야합니다
export class CreateFeedInput {
@IsString() // class-validator 데코레이터
@IsNotEmpty()
@ApiProperty({
description: '게시글 제목',
example: '서울 맛집 추천!!',
})
title: string;
@IsString()
@IsNotEmpty()
@ApiProperty({
description: '게시글 내용',
example: '종로구 부대찌개 맛집 추천드려요!',
})
content: string;
@IsString()
@ApiProperty({
description: '게시글 해시태그',
example: '#종로구,#부대찌개,#주말,#맛집',
})
hashTags: string;
}
@IsString
데코레이터가 원하는 문자열 타입이 아니기 때문에 타입에러를 리턴합니다.서비스와 핸들러는 비즈니스 로직을 수행하는 프로바이더입니다
서비스
Injectable
데코레이터를 이용해 다른 컴포넌트에서 해당 비즈니스 로직을 사용할 수 있게 만듭니다@Injectable
export class UserService {
constructor(@InjectRepository(User) private readonly userRepository: Repository<User>) {}
async fetch({ email }) {
const user = await this.userRepository.findOne({ where: { email } });
if (!user) throw new NotFoundException('유저 정보가 존재하지 않습니다');
return user;
}
}
```
constructor
에 넣어서 사용합니다@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
}
```
핸들러
@Get
, @Post
, @Delete
등과 같은 데코레이터로 장식된 컨트롤러 클래스 내의 단순한 메서드입니다.@Get() // 핸들러
fetchUser(@Query('email') email: string): Promise<User> {
return this.userService.fetch({ email });
}
@Get('list') // 핸들러
fetchUsers(): Promise<User[]> {
return this.userService.fetchAll();
}
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status =
exception instanceof HttpException //
? Number(exception.getStatus())
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception.message;
console.log('==========');
console.log('에러 발생!');
console.log('에러코드: ', status);
console.log('에러내용: ', message);
console.log('==========');
response.status(status).json({
statusCode: status,
message,
timestamp: new Date(),
});
}
}
app.useGlobalFilters(new HttpExceptionFilter());
if (!feed) throw new NotFoundException(ErrorType.feed.notFound.msg);
@Catch
데코레이터를 이용한 상속 클래스를 만들어 해당 모듈의 프로바이더로 설정해주면 됩니다provider를 정리하면서 IoC,DI등의 개념을 정리하고 provider들을 다시 복습하니 코드를 짜면서 찝찝했던 부분이 많이 해소가 되는 것 같습니다
사실 모듈을 사용하면서도 provider들을 명확하게 알지 못했는데, provider를 제대로 복습하고 리뷰한 점이 꽤나 도움이 된 것 같습니다
nest.js가 단순히 라이브러리 하나가 아닌 엔터프라이즈급의 서버개발 프레임워크고 포스팅할게 너무 많기 때문에 리뷰를 앞으로도 많이 해야 할 것 같습니다 추가로 포스팅해야 할 개념들을 적어보고 마무리 지을게요
https://www.wisewiredbooks.com/nestjs/
https://nemne.tistory.com/m/26
https://velog.io/@chappi/Nestjs%EB%A5%BC-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90-6%EC%9D%BC%EC%B0%A8-Pipe-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC-John-ahn%EB%8B%98-%EA%B0%95%EC%9D%98
https://velog.io/@kimdlzp/%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90
https://velog.io/@yukina1418?tag=NestJS