nest의 공식문서를 토대로 작성합니다.
Nest는 여러 애플리케이션 컨텍스트(예: Nest HTTP 서버 기반, 마이크로서비스 및 웹소켓 애플리케이션 컨텍스트)에서 작동하는 애플리케이션을 쉽게 작성할 수 있도록 도와주는 여러 유틸리티 클래스를 제공합니다. 이러한 유틸리티는 현재 실행 컨텍스트에 대한 정보를 제공하여 광범위환 컨트롤러, 메소드 및 실행 컨텍스트에서 작동할 수 있는 일반 가드, 필터 및 인터셉터를 빌드하는데 사용할 수 있습니다.
이러한 클래스 두 가지를 다룹니다. ArgumentsHost
와 ExecutionContext
입니다.
ArgumentsHost
클래스는 핸들러에 전달되는 인수를 검색하는 메소드를 제공합니다. 이 클래스를 사용하면 인수를 검색할 적절한 컨텍스트(예: HTTP, RPC, WebSockets)를 선택할 수 있습니다. 프레임워크는 일반적으로 호스트 매개변수로 참조되는 ArgumentsHost
의 인스턴스를 사용자가 접근하려는 위치에 제공합니다. 예를 들어 필터의 catch()
메소드는 ArgumentsHost
인스턴스를 사용하여 호출됩니다.
ArgumentsHost
는 단순히 핸들러의 인수를 추상화하는 역할을 합니다. 예를 들어 HTTP 서버 애플리케이션(@nestjs/platform-express
를 사용하는 경우)의 경우 host
객체는 Express의 [request, response, next]
배열을 캡슐화하며 여기서 request
는 요청 객체, response
는 응답 객체, next
는 애플리케이션의 요청-응답 사이클을 제어하는 함수입니다. 반면 GraphQL 애플리케이션의 경우 host
객체에는 [root, args, context, info]
배열이 포함됩니다.
여러 애플리케이션 컨텍스트에서 실행되는 일반 가드, 필터 및 인터셉터를 빌드할 때는 메소드가 현재 실행 중인 애플리케이션 유형을 확인할 수 있는 방법이 필요합니다. 이 작업은 ArgumentsHost
의 getType()
메소드를 사용하여 수행합니다:
if (host.getType() === 'http') {
// do something that is only important in the context of regular HTTP requests (REST)
} else if (host.getType() === 'rpc') {
// do something that is only important in the context of Microservice requests
} else if (host.getType<GqlContextType>() === 'graphql') {
// do something that is only important in the context of GraphQL requests
}
HINT
GqlContextType
은@nestjs/graphql
패키지에서 import.
애플리케이션의 유형을 확인할 수 있게 되면 아래와 같이 보다 일반적인 컴포넌트를 작성할 수 있습니다.
핸들러에 전달되는 인수의 배열을 검색하려면 호스트 객체의 getArgs()
메소드를 사용할 수 있습니다.
const [req, res, next] = host.getArgs();
인덱스별로 특정 인수를 추출하려면 getArgByIndex()
메소드를 사용하면 됩니다:
const request = host.getArgByIndex(0);
const response = host.getArgByIndex(1);
이 예제에서는 인덱스로 요청 및 응답 객체를 검색했는데 이는 애플리케이션을 특정 실행 컨텍스트에 연결하기 때문에 일반적으로 권장되지 않습니다. 대신 호스트 객체의 유틸리티 메소드 중 하나를 사용하여 애플리케이션에 적합한 애플리케이션 컨텍스트로 전환함으로써 코드를 보다 강력하고 재사용 가능하게 만들 수 있습니다. 컨텍스트 전환 유틸리티 메소드는 다음과 같습니다.
/**
* Switch context to RPC.
*/
switchToRpc(): RpcArgumentsHost;
/**
* Switch context to HTTP.
*/
switchToHttp(): HttpArgumentsHost;
/**
* Switch context to WebSockets.
*/
switchToWs(): WsArgumentsHost;
switchToHttp()
메소드를 사용해서 이전 예제를 다시 작성해봅시다. host.switchToHttp()
헬퍼 호출은 HTTP 애플리케이션 컨텍스트에 적합한 HttpArgumentsHost
객체를 반환합니다. HttpArgumentsHost
객체에는 원하는 객체를 추출하는 데 사용할 수 있는 두 가지 유용한 메소드가 있습니다. 또한 이 경우 Express 타입 assertions를 사용하여 기본 Express 타입 객체를 반환합니다:
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
마찬가지로 WsArgumentsHost
와 RpcArgumentsHost
에는 마이크로서비스와 웹소켓 컨텍스트에서 적절한 객체를 반환하는 메소드가 있습니다. 다음은 WsArgumentsHost
에 대한 메소드입니다:
export interface WsArgumentsHost {
/**
* Returns the data object.
*/
getData<T>(): T;
/**
* Returns the client object.
*/
getClient<T>(): T;
}
다음은 RpcArgumentsHost
에 대한 메소드입니다:
export interface RpcArgumentsHost {
/**
* Returns the data object.
*/
getData<T>(): T;
/**
* Returns the context object.
*/
getContext<T>(): T;
}
ExecutionContext
는 ArgumentsHost
를 확장하여 현재 실행 프로세스에 대한 추가 세부 정보를 제공합니다. ArgumentsHost
와 마찬가지로 Nest는 가드의 canActivate()
메소드나 인터셉터의 intercept()
메소드 등 필요할 수 있는 곳에 ExecutionContext
의 인스턴스를 제공합니다.
다음과 같은 메소드를 제공합니다:
export interface ExecutionContext extends ArgumentsHost {
/**
* Returns the type of the controller class which the current handler belongs to.
*/
getClass<T>(): Type<T>;
/**
* Returns a reference to the handler (method) that will be invoked next in the
* request pipeline.
*/
getHandler(): Function;
}
getHandler()
메소드는 호출하려는 핸들러에 대한 참조를 반환합니다. getClass()
메소드는 이 특정 핸들러가 속한 Controller
클래스의 타입을 반환합니다. 예를 들어 HTTP 컨텍스트에서 현재 처리된 요청이 CatsController
의 create()
메소드에 바인딩된 POST
요청인 경우, getHandler()
는 create()
메소드에 대한 참조를 반환하고 getClass()
는 인스턴스가 아닌 CatsController
타입을 반환합니다.
const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"
현재 클래스와 핸들러 메소드 모두에 대한 참조에 접근할 수 있는 기능은 뛰어난 유연성을 제공합니다. 가장 중요한 것은 가드 또는 인터셉터 내에서 Reflector#createDecorator
를 통해 생성된 데코레이터 또는 내장된 @SetMetadata()
데코레이터를 통해 메타데이터 세트에 접근할 수 있다는 점입니다. 이 사용 사례는 다음에서 배웁니다.
Nest는 Reflector#createDecorator
메소드를 통해 생성된 데코레이터와 내장된 @SetMetadata()
데코레이터를 통해 라우트 핸들러에 사용자 정의 메타데이터를 첨부할 수 있는 기능을 제공합니다. 이 섹션에서는 두 가지 접근 방식을 비교하고 가드 또는 인터셉터 내에서 메타데이터에 접근하는 방법을 살펴봅니다.
Reflector#createDecorator
를 사용하여 강력한 타입의 데코레이터를 만들려면 타입 인수를 지정해야 합니다. 예를 들어 문자열 배열을 인수로 받는 Roles
데코레이터를 만들어 보겠습니다.
# roles.decorator.ts
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
여기서 Roles
데코레이터는 string[]
타입의 단일 인수를 받는 함수입니다.
여기서 오류가 날 수 있는데 이는 nest의 버전 때문일 수 있습니다.
$ npm i -g npm-check-updates $ ncu
해보시면
이렇게 뜹니다.
맨 아래에 나온 것처럼$ ncu -u $ npm i
명령어를 입력해주면 버전이 업데이트되어 오류가 없어질 것입니다.
이제 이 데코레이터를 사용하려면 핸들러에 주석을 달기만 하면 됩니다.
# cats.controller.ts
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
여기에서는 admin
역할이 있는 사용자만 이 경로에 접근할 수 있어야 함을 나타내는 Roles
데코레이터 메타데이터를 create()
메소드에 첨부했습니다.
경로의 역할(사용자 정의 메타데이터)에 접근하려면 Reflector
헬퍼 클래스를 다시 사용합니다. Reflector
는 일반적인 방법으로 클래스에 삽입할 수 있습니다.
# roles.guard.ts
@Injectable()
export class RolesGuard {
constructor(private reflector: Reflector) {}
}
이제 핸들러의 메타데이터를 읽기 위해 get()
메소드를 사용합니다.
const roles = this.reflector.get(Roles, context.getHandler());
여기서 갑자기 context가 어디서 나오는가 하면
https://github.com/nestjs/nest/blob/v10.2.7/sample/01-cats-app/src/common/guards/roles.guard.ts
여기 nest 깃허브 확인해보면 다르게 구현되어 있는 걸 확인하실 수 있습니다..
Reflector#get
메소드를 사용하면 데코레이터 참조와 메타데이터를 검색할 컨텍스트의 두 가지 인수를 전달하여 메타데이터에 쉽게 접근할 수 있습니다. 이 예에서 지정된 데코레이터는 Roles
입니다. 컨텍스트는 context.getHandler()
를 호출하여 제공되며 그 결과 현재 처리된 라우트 핸들러의 메타데이터를 추출합니다. getHandler()
는 라우트 핸들러 함수에 대한 참조를 제공합니다.
또는 컨트롤러 수준에서 메타데이터를 적용하여 컨트롤러 클래스의 모든 경로에 적용하여 컨트롤러를 구성할 수도 있습니다.
# cats.controller.ts
@Roles(['admin'])
@Controller('cats')
export class CatsController {}
이 경우 컨트롤러 메타데이터를 추출하기 위해 context.getHandler()
대신 context.getClass()
를 두 번째 인수로 전달합니다(메타데이터 추출을 위한 컨텍스트로 컨트롤러 클래스를 제공하기 위해):
# roles.guard.ts
const roles = this.reflector.get(Roles, context.getClass());
여러 레벨에서 메타데이터를 제공할 수 있으므로 여러 컨텍스트에서 메타데이터를 추출하고 병합해야 할 수도 있습니다. Reflector
클래스는 이를 지원하는 데 사용되는 두 가지 유틸리티 메소드를 제공합니다. 이 메소드들은 컨트롤러와 메소드 메타데이터를 한 번에 추출하고 서로 다른 방식으로 결합합니다.
두 레벨 모두에서 Roles
메타데이터를 제공한 다음 시나리오를 생각해봅시다.
# cats.controller.ts
@Roles(['user'])
@Controller('cats')
export class CatsController {
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
}
기본 role로 user
를 지정하고 특정 메소드에 대해 선택적으로 재정의하려는 경우 getAllAndOverride()
메소드를 사용할 수 있습니다.
const roles = this.reflector.getAllAndOverride(Roles, [context.getHandler(), context.getClass()]);
위의 메타데이터를 사용하여 create()
메소드의 컨텍스트에서 실행되는 이 코드가 포함된 가드는 ['admin']
을 포함하는 역할을 생성합니다.
두 메타데이터를 모두 가져와 병합하려면(이 메소드는 배열과 객체를 모두 병합합니다) getAllAndMerge()
메소드를 사용합니다:
const roles = this.reflector.getAllAndMerge(Roles, [context.getHandler(), context.getClass()]);
이렇게 하면 ['user', 'admin']
을 포함하는 rolse
가 생성됩니다.
이 두 병합 메소드 모두 첫 번째 인자로 메타데이터 키를 전달하고 두 번째 인자로 메타데이터 대상 컨텍스트 배열(즉 getHandler()
및 getClass()
메소드에 대한 호출)을 전달합니다.
앞서 언급했듯이 Reflector#createDecorator
를 사용하는 대신 내장된 @SetMetadata()
데코레이터를 사용하여 핸들러에 메타데이터를 첨부할 수도 있습니다.
# cats.controller.ts
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
위의 구조에서는 roles
메타데이터(roles
는 메타데이터 키이고 ['admin']
은 연관된 값)를 create()
메소드에 첨부했습니다. 이렇게 해도 작동하지만 @SetMetadata()
를 경로에 직접 사용하는 것은 좋은 방법이 아닙니다. 대신 아래와 같이 자체 데코레이터를 만들 수 있습니다.
# roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
이 접근 방식은 훨씬 더 깔끔하고 가독성이 높으며 Reflector#createDecorator
접근 방식과 다소 유사합니다. 차이점은 @SetMetadata
를 사용하면 메타데이터 키와 값을 더 많이 제어할 수 있고 둘 이상의 인수를 받는 데코레이터를 만들 수도 있다는 점입니다.
이제 사용자 정의 @Roles()
데코레이터가 생겼으므로 이를 사용하여 create()
메소드를 데코레이션할 수 있습니다.
# cats.controller.ts
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
경로의 role(사용자 지정 메타데이터)에 접근하려면 Reflector
헬퍼 클래스를 다시 사용합니다:
# roles.guard.ts
@Injectable()
export class RolesGuard {
constructor(private reflector: Reflector) {}
}
이제 핸들러 메타데이터를 읽으려면 get()
메소드를 사용합니다.
const roles = this.reflector.get<string[]>('roles', context.getHandler());
여기서는 데코레이터 참조를 전달하는 대신 메타데이터 키를 첫 번째 인수로 전달합니다(위 경우 'roles
'). 다른 모든 것은 Reflector#createDecorator
예시와 동일하게 유지됩니다.
고생하셨습니다!
다음 글에서 만나요~~😀
저도 아직 배우는 단계입니다. 지적 감사히 받겠습니다. 함께 열심히 공부해요!!