@Expose() 데코레이터도 @Exclude 데코레이터와 같이 특정 프로퍼티를 제외시키는 상황에서 쓰이는데, 동작하는 것이 그 반대이다.
이게 무슨 말이냐, 쉽게 말해 @Exclude()가 붙은 프로퍼티만 제외되었다면 @Expose()는 이 데코레이터가 붙지 않은 데코레이터를 제외시킨다.
아래의 코드는 위에서 @Exclude() 데코레이터 예제와 같은 동작을 한다.
import { Exclude, Expose } from 'class-transformer'@Exclude()class User { @Expose() public id: number @Expose() public username: string public email: string constructor(id: number, username: string, email: string) { this.id = id this.username = username this.email = email } is_admin(): boolean { return this.id === 1 } is_gmail(): boolean { return this.email.endsWith('@gmail.com') }}
다만 클래스 자체에 @Exclude() 데코레이터를 붙여줘야 하고, @Expose()의 경우 제외시킬 항목이 많은 경우 사용하면 유용하다.
다만 클래스 상속에서 두 클래스가 각각 @Exclude() 방식과 @Expose() 방식으로 다르게 사용하고 있다면 상속에서 조심해야 한다.
예를 들어 부모 클래스는 @Exclude() 방식을 사용하여 제외할 프로퍼티 외엔 아무런 데코레이터가 붙어있지 않은데, 자식 클래스가 @Expose() 방식을 사용하여 부모의 프로퍼티를 상속받았다면 데코레이터가 붙어있지 않은 프로퍼티는 자동으로 제외되기 때문이다.
아무튼 이러한 기능이 있는게 class-transformer이고, 그 외에 직렬화 시 이름 변경 등의 여러 기능이 있으나 따로 설명하진 않겠다. 이번 포스팅에서 다룰 내용은 @Exclude() 데코레이터면 충분하다.
그런데 저렇게 @Exclude()나 @Expose() 데코레이터를 써서 제외시킬 프로퍼티를 명시하고, 이걸 제외시켜 클라이언트에 보내주기 위해 instanceToPlain() 함수를 호출하게 된다. 즉 아래와 같이 사용한다는 의미이다.
async findByUsername(username: string) { const user = await this.repo.findOne({ where: { username } }) if (!user) { throw new NotFoundException('User not found') } return instanceToPlain(user)}
그런데 실수로 instanceToPlain() 함수 호출을 까먹었다고 치자, 그럼 영문도 모른채 제외되야할 데이터가 그대로 남아있는 상태로 반환된다.
그리고 매번 저렇게 호출을 하는 것은 귀찮기 때문에 이걸 자동으로 해주는 기능이 필요하다.
2. Interceptor
NestJS의 인터셉터(Interceptor)는 간단히 말해 요청이나 응답의 흐름에서 가로채서 데이터를 조작하는 역할을 한다.
이걸 자세히 설명하려면 NestJS의 기본적인 생명 주기의 미들웨어, 가드, 파이프, 예외 필터, 그리고 RxJS의 Observer 패턴 등에 대해 설명해야 하는데 그럼 포스트의 양이 늘어나고 주제와는 큰 관련이 없기 때문에 나중에 따로 다뤄보는걸로 하고, 코드 부터 보자.
// src/common/interceptors/transform.interceptor.tsimport { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'import { instanceToPlain } from 'class-transformer'import { Observable } from 'rxjs'import { map } from 'rxjs/operators'@Injectable()export class TransformInterceptor implements NestInterceptor { intercept(_: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe(map((data) => instanceToPlain(data))) }}
위와 같이 간단히 구현하였는데, next 핸들러를 통해 컨트롤러의 반환 값인(응답) Observable 데이터 스트림을 가져온다. 그리고 pipe와 map을 통해 데이터에 대해 instanceToPlain 함수를 실행한다.
그러면 자동으로 응답에 대해 instanceToPlain()를 호출하는 인터셉터를 만들었다. 그리고 이 인터셉터를 NestJS 앱에 등록해주면 된다.