포도가게의 개발일지

[Nest] 정리 본문

Nest

[Nest] 정리

grape.store 2022. 3. 27. 21:04
반응형

Nest

- Nest는 node.js를 효율적으로 서버측 어플리케이션을 확장 가능하게 빌드해주는 프레임워크이다.

 

code snippet

- 모듈처럼 일부 코드의 한 부분

 

현재 Nest Node 최소버전 >= 10.13.0, except for v13

Setup

$ npm i -g @nestjs/cli
$ nest new project-name

Yarn vs Npm

속도

npm은 pakage 설치시 순차적으로 pakage를 설치하는 반면에 yarn은 병렬적으로 pakage를 설치하여 좀 더 빠르게 설치할 수 있다.

보안이슈도 있었지만 npm도 pakage-lock.json이 생기면서 이 부분에 대해서는 해결된 것 같다.

https://developer0809.tistory.com/128

우리회사는 yarn으로 pakage관리를 하기 때문에 yarn command 사이트를 추가

https://classic.yarnpkg.com/en/docs/cli/config

 

Yarn

Fast, reliable, and secure dependency management.

classic.yarnpkg.com

Nest Js Life Cycle

1. OnModuleInit() : 호스트 모듈이 초기화되면 호출

2. OnApplicationBootstrap() : 어플리케이션이 완전히 시작되고 부트 스트랩되면 호출

3. OnModuleDestroy : Nest가 호스트 모듈을 파괴하기 직전에 정리

4. beforeApplicationShutdown() : onModuleDestroy() 핸들러가 완료된 후에 호출

5. OnApplicationShutdown() : 시스템 신호에 응답한다.

 

Request Life cycle

  1. Incoming request (1)
  2. Globally bound middleware
  3. Module bound middleware
  4. Global guards (2)
  5. Controller guards (2)
  6. Route guards (2)
  7. Global interceptors (pre-controller) (3)
  8. Controller interceptors (pre-controller) (3)
  9. Route interceptors (pre-controller) (3)
  10. Global pipes (4)
  11. Controller pipes (4)
  12. Route pipes (4)
  13. Route parameter pipes (4)
  14. Controller (method handler) (5)
  15. Service (if exists)
  16. Route interceptor (post-request) (6)
  17. Controller interceptor (post-request) (6)
  18. Global interceptor (post-request) (6)
  19. Exception filters (route, then controller, then global) -> 이벤트 발생시 즉발
  20. Server response

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

NestFacotry.create(AppModule) 메소드는 INestApplcation interface를 return 해준다.

 

Request handler

- Nest request handler는 JS 원시타입(primitive)인 경우 그대로 return 되면 object나 array인 경우 Json 형태로 serialize(변환)된 후 return 된다.

@Request(), @Req() req
@Response(), @Res()* res
@Next() next
@Session() req.session
@Param(key?: string) req.params / req.params[key]
@Body(key?: string) req.body / req.body[key]
@Query(key?: string) req.query / req.query[key]
@Headers(name?: string) req.headers / req.headers[name]
@Ip() req.ip
@HostParam() req.hosts

 

Nest에서는 default status code가 200, Post method는 201로 설정 되어있다. 만약 바꾸고 싶으면 @Httpcode(number)을 사용하여 쉽게 변환 시킬 수 있다.

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

 

@Res, @Req 데코레이터를 이용하기위해 (/@types/express)를 이용가능하다

 

Nest는 @Res, @Next를 감지하여 Library-specific option을 선택한지 알 수 있다. 때문에 Nest standard response handling을 동시에 사용하기 위해서는 @Res({passthrough:true})설정이 필요하다.

 

@Header 데코레이터를 이용하여 header를 custom 할 수 있음 하지만 res.header를 수정하기 위해서는 library-specific 방식을 채택해야 한다.

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

@Redirect 데코레이터를 이용하여 response를 특정 url에 redirect 시킬 수 있다.

@Get()
@Redirect('https://nestjs.com', 301)

// 특정조건에 redirect url을 변경할 수 있다.
return { url: 'link' };

 

Error handling

Exception filters

- 응용 프로그램 코드에서 예외를 처리하지 않았으먄 exception layer에 의해 예외가 탐지되어 사용자에게 적절한 응답을 자동으로 보냅니다.

- 인식하지 못하는 exception이 발생하였을 때, exceptioin-filter에 내장된 default json type을 응답한다.

//custom

@Get()
async findAll() {
  throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  }, HttpStatus.FORBIDDEN);
}

HttpException 클래스의 인스턴스인 예외를 포착하면 사용자가 정의한 응답을 구현하는 역할(log or console과 같은) 하는 예외 필터를 만들어 봅시다.

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    console.log(`status : ${status}`);
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

exception filter bind

  @Post('/test-exception')
  @UseFilters(new HttpExceptionFilter())
  occurException(@Body() req: UserDto) {
    if (req.userId === undefined) throw new BadRequestException();
    return req;
  }

위와 같은 new HttpExceptionFilter()로 bind된 경우 new 객체를 만들어서 사용 되지만, HttpExceptionFilter로 class를 넘겨주는 경우 framework에서 Ioc DI가 적용되어서 사용된다.

 

UseFilter는 method scope, controller scope, global scope이 있으며 controller는 controller위에 global은 아래와 같이 사용된다.

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

userGlobalFilters는 module밖에서 생성되기 때문에 DI가 일어나지 않는다.(frame work 밖이라 그런가?) 모듈에 넣어주면 DI가능

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

exception type과 관계없이 모든 exception을 잡기 위해 @Catch() 데코레이터를 사용해라

마지막으로 간단한 custom exception filter를 만들고 싶을때 BaseExceptionFilter을 상속하고 @catch를 사용해라.

*주의 baseesceptionfilter를 상속하는경우 new instance사용하지말고 framework한테 맡겨라

import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    super.catch(exception, host);
  }
}

 

Nest는 dto를 class로 만드는 것을 추천한다. typescript에서 interface로 만들 수 있지만 ts -> js로 translation 과정중 interface제거 된다. 따라서 Nest에서 추적이 불가. This is important because features such as Pipes enable additional possibilities when they have access to the metatype of the variable at runtime

-> 추가된 이유중 하나가 nest에서는 class에 decorator를 붙일 수 있어 validation. check가 용이하지만 intreface에는 decorator를 붙일 수 없어 불편한이유도 있다. (만든사람이 이게 주된 이유래)

https://github.com/nestjs/nest/issues/1228

 

Interface vs DTO · Issue #1228 · nestjs/nest

[ ] Regression [ ] Bug report [ ] Feature request [x] Documentation issue or request [ ] Support request => Please do not submit support request here, instead post your question on Stack Overflo...

github.com

 

ValidationPipes

- whitelist config 설정으로 임의로 추가된 property는 제거 가능하다 forbidNonWhitelisted 을 같이 쓰면 추가로 들어온 property가 있을경우 error 반환

 

set cookies or headers depending on certain conditions는 Library-specific에 의존할 수 밖에 없어서 below passthrought: true 필요

@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}

 

Nest DI

- nest는 데코레이터와 module를 통해 metadata를 얻는것으로 보임. 데코레이터만 쓰면 네스트가 모르고 module에 정보 추가까지 해줘야함

 

@Injectable()

injectable 데코레이터를 통해 Nest Ioc에 관리 대상이 되야하는 class라는 metadata를 알려준다.

 

Nest DI

- aplication이 bootstrap을 싱핼할때 모든 provider는 instance화 되어있어야 한다. 그리고 apl이 종료될때 모든 provider는 제거될것이다.

- request로 인해 controller가 인스턴스 될때 dependency를 확인한다 주입되어야 할게 있다면 ioc에서 주입될 인스턴스를 확인후 없으면 이때 provider를 인스턴스화하고 return 및 cache하며 이미 존재하면 그냥 return 만 한다.

- api가 bootstrap될때 dependency graph를 생성하는데 이건 복잡하다. 그래프는 아무래도 bottom-up방식을 채택.

providers: [CatsService]는 좀 펼쳐서 보면 아래와 같음 어떤 class를 사용하는지 명시되어있음

providers: [
  {
    provide: CatsService,
    useClass: CatsService,
  },
];

Provider scope

default : api가 bootstrap 될때 모든 싱글톤 pro는 인스턴스화 된다. -> 한번 생성 된 후 cache 됨

request : 요청이 있을때 새로운 인스턴스가 생기는 경우. 이 스코프는 리퀘스타가 끝난후 garbage-collecting 된다.

transient : consumer?(provider를 가져다 쓰는애들)에게 공유 안됨

-> 싱글톤 디폴트 스코프가 권장된다. 아래에 scope 설정 법

@Injectable({
  provide: 'CACHE_MANAGER',
  useClass: CacheManager,
  scope: Scope.TRANSIENT,
})

controller scope

-> controller scope는 provider와 달리 Request가 들어오면 new 인스턴스 생성 후 req가 완료되면 가비지컬렉팅 진행한다.

 

순환참조(cicular dependency) 두개의 클래스가 서로 의존하는 경우

-> 안쓰는게 낫지만 이 문제를 nest에서 해결하는 2가지 방법 제공 -> 대신 쓸려면 의존 순서에 영향이 없게 먼저 코드를 작성해야 한다.

-> forward referenec, moduleRef -> 그냥 어떤식으로든 끊어내야한다...

@Injectable()
export class CatsService {
  constructor(
    @Inject(forwardRef(() => CommonService))
    private commonService: CommonService,
  ) {}
}
@Module({
  imports: [forwardRef(() => CatsModule)],
})
export class CommonModule {}

@Guard

- 특정 상황에 따라서, request가 route handle될 지 말지를 결정한다.

- guard는 CanActivate interface를 반드시 implement해야한다.

- guard는 모든 middleware 다음에 실행되고, interceptor나 pipe 이전에 실행된다.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
/**
     * @param context Current execution context. Provides access to details about
     * the current request pipeline.
     *
     * @returns Value indicating whether or not the current request is allowed to
     * proceed.
     */
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

@RolesGuard : 특정 role을 가진 user만 라우트해줌

//roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  
  constructor(private reflector: Reflector) {}
  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return false;
    }
    const request = context.switchToHttp().getRequest();
    
    // node.js에서는 승인된 사용자를 request 객체에 연결하는 것이 일반적
    const user = request.user;
    
    //return matchRoles(roles, user.roles);
    
    // 간단하게 구현한 matchRoles 로직
    return roles.includes(user.role);
  }
}

 

Execution Context

- 현재 실행 프로세스에 대한 추가 세부 정보를 받아옴

- 실행컨텍스트이기에 현재 프로세스의 next스텝이라던가 여러 정보를 가져올 수 있음.

 

- contextType

export declare type ContextType = 'http' | 'ws' | 'rpc';
 

ArgumentsHost(HttpArgumentsHost, WsArgumentsHost, RpcArgumentsHost)

- switch메소드를 통해 contextType을 변경할수있음

- getType 메서드를 통해 현재 contextType return

 

ExecutionContext extend ArgumentsHost

 

ArgumentsHost class구성

export interface HttpArgumentsHost {
    /**
     * Returns the in-flight `request` object.
     */
    getRequest<T = any>(): T;
    /**
     * Returns the in-flight `response` object.
     */
    getResponse<T = any>(): T;
    getNext<T = any>(): T;
}

For example, in an HTTP context, if the currently processed request is a POST request, bound to the create() method on the CatsController, getHandler() returns a reference to the create() method and getClass() returns the CatsControllertype (not instance).

getHandler는 현재 context의 function을 getClass는 현재 context의 controller를 반환한다.

const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"

 

Reflection

- 반영은 컴퓨터 프로그램에서 런타임 시점에 사용되는 자신의 구조와 행위를 관리하고 수정할 수 있는 프로세스

- 즉 객체를 통해 런타임 시점에 클래스의 정보를 알 수 있는 것

- nest에서는 guard를 사용하게 되면 guard에서 execution context에 접근하여 해당 request정보를 확인 후 해당 route(handler)에 접근을 허가할지 불허할지 결정해야 하는 경우가 있습니다.

- 이때 @Guard는 수행 될 contoller(컨트롤러는 기본적으로 request마다 인스턴스가 생성된다.)의 handler 타입을 알 수 없습니다. 어떤 요청이 들어올지 모르고 어떤 handler가 실행될지 컴파일 시점에서 알 수 없기때문이다. 
-> 때문에 우리는 reflection을 이용하여 이를 수행해야한다.(런타임 시점에 정보를 알 기위해)

 

Nest는 Setmetadata를 이용하여 reflection한다.


  @Get()
  @Roles('admin')
  get() {
    this.testService.test();
  }

///////////////////////////////////////////////////////////////////////////////////////////

import { SetMetadata } from "@nestjs/common";

export const Roles = (...roles: string[]) => {
  const ret = SetMetadata("roles", roles);
  console.log("META:", ret);
  return ret;
};

export const SetMetadata = <K = any, V = any>(
// 현재 K는 string type, V는 string[] type으로 설정된다.
// metadataKey는 'roles', metadataValue = ['admin'] 이 들어온다.
  metadataKey: K,
  metadataValue: V
) => (target: object, key?: any, descriptor?: any) => {
  if (descriptor) {
  // 이때 get() method에 키가 'roles'이이고 ['admin'] metadata가 등록됩니다.
  // 이로인해 Guard에서 'roles'키를 이용하여 request에 metadata와 비교 할 수 있습니다.
    Reflect.defineMetadata(metadataKey, metadataValue, descriptor.value);
    return descriptor;
  }
  Reflect.defineMetadata(metadataKey, metadataValue, target);
  return target;
};
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";

@Injectable()
export class AuthorizationGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
  
  // 현재 실행되고 있는 context의 method에서 'roles'key의 value를 가져옵니다.
    const roles = this.reflector.get<string[]>("roles", context.getHandler());
    console.log(roles); // ['admin']
    console.log(context.getHandler()); // [Function: get]
  // 현재는 true만 뱉지만 코드수정을 통해 비교해주어야 합니다.
    return true;
    
    ==> 로 수정 가능
    if(!roles){
    return true
  	}
  	const { user } = context.switchToHttp().getRequest();
    return roles.some((role) => user?.role === role)
}

이 방법 외에도 Nest에서는 간편하게 applyDecorator를 사용하여 metadata를 추가 할 수 있습니다.

import { applyDecorators } from '@nestjs/common';

export function Auth(...roles: Role[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(AuthGuard, RolesGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({ description: 'Unauthorized' }),
  );
}

Response 객체에 property 추가 decorator

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);
@Get()
async findOne(@User() user: UserEntity) {
  console.log(user);
}

Authentication

 

- @nestjs/passport 모듈을 통헤 Nest 어플리케이션과 이 라이브러리를 직관적으로 통합할 수 있다.

  • username/password, jwt, 혹은 identity provider에 의해 제공된 token identity 로 유저를 인증한다.
  • 인증 상태를 관리한다. (JWT, Session)
  • 인증된 user의 정보를 Request 오브젝트에 붙여서 route handler들에서 사용이 가능하게 한다

Passport가 뭘까?

- passport는 Node를 위한 express호환 인증 미들웨어이다.

- passport의 유일한 목적은 어떠한 목적을 위해 확장 가능한 플러그인을 통해 수행되는 요청을 인증하는것이다.

 

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
//passport-local strategy는 기본적으로 username과 password를 request body에서 얻어온다.
  constructor(private authService: AuthService) {
    super();
  }
//하지만 만약 username이 아닌 다른 property인 email에서 가져오고 싶다면 아래와 같이 작성
    constructor(private authService: AuthService) {
        super({usernameField: 'email'});
      }
  
// 생성 이후 validate 함수를 실행하게 된다.
// 각 strategy의 validate를 어떻게 작성하냐에 따라 유효 결정 방식이 바뀌게 된다.
// 예를 들어 jwt전략에서는 token을 decode하여 userId를 추출해 db를 확인하게 된다.
  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

Passport가 validate() 메서드를 통해 자동으로 user object를 생성해주고, request object에 이를 할당해준다.(찾아봐도 어떻게 주입되는지 모르겠다..  ㅠ)

Validation

과정

Interceptors

- 반드시 NestInterceptor 인터페이스를 구현해야한다.

- 인터셉터가 할 수 있는것

  • 메소드 실행 전/후에 다른 logic을 bind
  • 함수에서 리턴된 값을 변형
  • 함수에서 throw 된 예외를 변형
  • 기본 함수의 기능을 extend
  • 특정 조건 하에서 함수를 완전히 override

intercept() 메서드는 req/res를 wrap 할 수 있다.

 

Dynamic Module

  • 모듈은 근본적으로 Dynamic과 Static으로 나누어 진다. dynamic module은 커스터마이징이 가능한 모듈을 만들 수 있는 모듈이다. 옵션을 주어 또 다른 모듈을 반환하여 동적이다.
  • Module에 옵션을 주게 되면 Dnynamic Module로 타이핑 된다.
  • 일반적으로 static method를 만들어 동적으로 변화를 주는것으로 보인다.
  • 개발자를 위한 데이터베이스, 준비 / 테스트 환경을 위한 다양한 배포 환경에서 응용 프로그램 설정을 쉽게 변경할 수 있습니다.
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    UsersModule,
    PassportModule,
//JwtModule에 등록되어있는 static method인 register에 Option을 넣어줌
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '60s' },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}


////////////////////////////////////////////////////////////////////////

@Module({
  providers: [JwtService],
  exports: [JwtService]
})
export class JwtModule {
// option에는 = { secret: jwtConstants.secret, signOptions: { expiresIn: '60s' }, }
	static register(options: JwtModuleOptions): DynamicModule {
	    return {
	      module: JwtModule,
	      providers: createJwtProvider(options)
          -> return된다. useValue를 통해 JWT_MODULE_OPTION을 대체한다.
          [{
          	provide: JWT_MODULE_OPTIONS,
            useValue: { secret: jwtConstants.secret, signOptions: { expiresIn: '60s' }, }
          }]
	    };
	  }
}
	
export function createJwtProvider(options: JwtModuleOptions): any[] {
  return [{ provide: JWT_MODULE_OPTIONS, useValue: options || {} }];
}

export const JWT_MODULE_OPTIONS = 'JWT_MODULE_OPTIONS';

 

class-validator

1. @ValidateIf()

- 제공된 함수가 false를 반환할 때 validation check를 무시할 수 있다.

import { ValidateIf, IsNotEmpty } from 'class-validator';

export class Post {
  otherProperty: string;

  @ValidateIf(o => o.otherProperty === 'value')
  @IsNotEmpty()
  example: string;
}

'Nest' 카테고리의 다른 글

Angular DI의 이해  (0) 2022.09.22
Comments