우선 시작하기 전에 NestJS
를 사용해본 많은 개발자들이 컨트롤러 내에서 데코레이터를 활용한 경험이 있거나 본 적이 있을 것이다.
예를 들어, @Post()
와 같은 방식으로 데코레이터를 사용해 특정 작업을 수행하는 방식이 있다.
@Post()
이외에도 @Body()
, @Query()
, @Param()
과 같이 매개 변수에 특정 데코레이터를 적용하는 방법도 있다.
@Body()
@Query()
@Param()
여기에는 @Req()
와 @Res()
같은 데코레이터도 포함이 되어 있는데 가능하면 이 둘의 사용을 지양하는 것이 좋다고 권장된다.
예시로, 다음과 같이 @Req()
를 사용한 코드가 있다.
@ApiOperation({ summary: '내 정보 조회' })
@Get()
getUsers(@Req() req) {
return req.user;
}
이 방식은 특정 플랫폼(Express)에 종속되어 있고 추후 변경하기도 어렵고 테스트하기도 까다롭다는 단점이 있다.
NestJS
에서는 특히 JWT 토큰을 res.locals.jwt
에 저장하는 경우가 많은데 이는 Express에서 미들웨어 간 변수를 공유하는 데 사용된다.
이러한 문제를 해결할 수 있는 방법이 커스텀 데코레이터이다.
커스텀 데코레이터를 통해 req.user
나 res.locals.jwt
와 같은 부분을 @Req()
, @Res()
를 사용하지 않고도 처리할 수 있으며 이를 통해 req.user
의 타입이 추론이 되지 않는 문제도 해결할 수 있다.
몇 가지 예제들을 통해서 커스텀 데코레이터를 만드는 방법을 보자.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
}
)
여기서 createParamDecorator
를 사용하여 커스텀 데코레이터를 사용한다.
이 함수는 익명 함수를 매개 변수로 받으며 data와 ExecutaionContext 타입의 ctx를 매개 변수로 한다.
참고로, 여기서의 ExecutionContext(실행 컨텍스트)는 Javascript
에서의 실행 컨텍스트와는 개념이 다르다.
이를 통해 HTTP 요청을 가져오고, request.user
를 반환하는 형태로 데코레이터가 작성되어 있다.
위와 같이 작성하면 우리는 User라는 데코레이터를 직접 만들어준 셈이다.
이렇게 정의된 User 데코레이터는 다음과 같이 사용할 수 있다.
기존의 @Req()
부분을 @User()
로 변경해 사용한다.
@ApiOperation({ summary: '내 정보 조회' })
@Get()
getUsers(@Req() req) {
return req.user;
}
---------------------------------------------------------------
@ApiOperation({ summary: '내 정보 조회' })
@Get()
getUsers(@User() user) {
return user;
}
이 변경을 통해서 user 변수가 req.user
의 값을 직접 참조할 수 있게 된다.
이렇게 커스텀 데코레이터를 활용하면 코드의 의존성을 줄이고 더욱 유연하고 테스트를 하기 쉬운 구조로 만들 수 있다.
토큰을 다루는 또 다른 예시로, 이번에는 JWT 토큰이 응답 객체에 저장되어 있다고 가정해보자.
이 상황에선 createParamDecorator
를 사용해 커스텀 데코레이터를 만들어 응답 객체에서 직접 토큰을 추출할 수 있다.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const Token = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const response = ctx.switchToHttp().getResponse();
return response.locals.jwt;
}
)
위 코드를 통해 @Token()
데코레이터를 생성하게 되면, 컨트롤러 내에서 @Token() token
과 같이 사용해 JWT 토큰을 손쉽게 가져올 수 있다.
이 방법은 req.user
나 res.locals.jwt
와 같은 방법을 사용하지 않음으로써 발생할 수 있는 문제점들을 피할 수 있는 중요한 접근 방식이다.
특히 만약에, req.user
나 res.locals.jwt
를 컨트롤러에서 많이 사용한다면 나중에 테스트가 필요하거나, Express에서 Fastify나 Koa와 같은 다른 플랫폼으로 전환할 때 많은 수정이 필요할 수 있다.
각각의 req.user
나 res.locals.jwt
가 사용된 부분을 일일이 찾아 수정해야 하는 번거로움이 발생한다.
하지만 커스텀 데코레이터를 사용함으로써 필요한 수정 사항을 훨씬 쉽게 관리할 수 있게 되며, 코드 중복도 줄일 수가 있다.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const Token = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const response = ctx.switchToHttp().getResponse();
return response.locals.jwt;
}
)
그리고 여기서 언급된 ExecutionContext(실행 컨텍스트)는 복잡하게 생각할 필요가 없는 개념이다.
ctx 객체를 통해 switchToHttp
같은 메소드를 사용해 현재 HTTP 서버의 요청과 응답 객체 등을 접근할 수 있다.
NestJS
는 HTTP 서버뿐만 아니라 웹소켓과 RPC 서버도 동시에 운영할 수 있는 유연성을 제공한다.
하나의 실행 컨텍스트 안에서 HTTP, 웹소켓, RPC 서버에 대한 정보를 관리하고 접근할 수 있다는 것은 개발의 편의성을 크게 높여준다.
이러한 구조 덕분에 하나의 서버에서 여러 통신 방식을 효율적으로 관리하고, 서로 다른 서버 유형 간의 소통을 용이하게 하는 것이 가능해진다.
NestJS
가 이런 방식을 채택한 이유는 바로 이런 유연성과 확장성 때문이다.
커스텀 데코레이터를 통한 접근 방식은 이런 아키텍처를 더욱 활용할 수 있게 해준다.
NestJS
에서 커스텀 데코레이터 사용은 req.user
나 res.locals.jwt
와 같은 플랫폼에 종속적인 코드 사용을 피할 수 있게 해주고 코드의 유연성과 테스트 용이성을 향상시킨다.
@User()
와 @Token()
같은 커스텀 데코레이터를 통해 컨트롤러 내에서 필요한 사용자의 정보나 토큰을 직접적이고 깔끔하게 접근할 수 있다.
이 방식은 플랫폼 변경 시 수정해야하는 코드의 양을 줄여주고 코드의 중복을 제거하는 효과를 가진다.
또한, NestJS
의 실행 컨텍스트를 활용해 HTTP, 웹소켓, RPC 서버 간의 통신을 하나의 컨텍스트 내에서 효율적으로 관리할 수 있게 해준다.