class-transformer란? @Expose, @Exclude, @Type

TypeScript 환경에서 Rest API를 호출하거나 받는 경우가 많습니다. 그럴 때 가장 많이 사용되는 것은 JSON 객체를 사용합니다. 다른 환경은 모르겠지만 TypeScript & NodeJS(NestJS) 환경에서는 당황스러운 일이 있곤합니다. 바로 인스턴스 변환을 지원하지 않는다는 점이죠.

이 말의 뜻은 API 중 어느 것도 클래스의 인스턴스를 응답으로 넘겨주지 않습니다. 그렇기 때문에 Json 파일을 전달 받으면 NodeJS 진영에서는 이것을 인스턴스로 변환하지 않습니다.

그렇다면 NodeJS 진영에서는 어떻게 요청 데이터를 인스턴스로 변환하는지 살펴보도록 하겠습니다.

class-transformer 이란?

문제 1. – JavaScript

JavaScript에는 두 가지 유형의 객체가 있습니다.

  • 일반(리터럴) 객체
  • 클래스(생성자) 객체

일반 객체란 클래스의 인스턴스(Object) 객체입니다.

클래스 객체란 자체 정의된 생성자, 속성 및 메서드가 있는 클래스의 인스턴스입니다.

여기서 문제가 있습니다. 바로 일반 자바스크립트 객체를 가지고 있는 ES6부터 지원한 클래스로 변환하고 싶은 경우 입니다.

예를 들어 NodeJS, NestJS로 백엔드를 구성해서 API 중 요청 받은 값 중 json를 가져올 때 클래스 인스턴스가 아닌 일반 자바스크립트 객체가 생성됩니다.

[
  {
    "id": 1,
    "firstName": "Johny",
    "lastName": "Cage",
    "age": 27
  },
  {
    "id": 2,
    "firstName": "Ismoil",
    "lastName": "Somoni",
    "age": 50
  },
  {
    "id": 3,
    "firstName": "Luke",
    "lastName": "Dacascos",
    "age": 12
  }
]

위와 같은 json 데이터가 있습니다. 그리고 아래와 같은 클래스가 있죠.

export class User {
  id: number;
  firstName: string;
  lastName: string;
  age: number;

  getName() {
    return this.firstName + ' ' + this.lastName;
  }

  isAdult() {
    return this.age > 36 && this.age < 60;
  }
}

이것을 사용하기 위해 다음과 같은 코드를 작성해서 실행하려고 합니다.

fetch('users.json').then((users: User[]) => {
  // 여기에 User Class를 통해 다양한 처리를 할 예정입니다만..?
});

이렇게 코드를 작성해서 users를 호출하는 순간 오류가 발생합니다. 에러 내용은 users는 class 객체가 아닌 일반 자바스크립트 객체의 배열로 넘어오기 때문에 사용할 수 없게 됩니다.

해결 방안

이 문제를 해결하기 위해서는 리터럴 객체를 클래스 인스턴스 변환이 꼭 필요합니다. 여기에 필요한 것은 class-transformer 라이브러리 입니다. class-transformer는 단어 그대로 class로 변환해주는 라이브러리입니다.

fetch('users.json').then((users: Object[]) => {
  const realUsers = plainToClass(User, users);
});

위 코드에 있는 plainToClass는 전달받은 리터럴 객체를 클래스객체로 변환해 주는 method 입니다. 이를 통해 문제가 되었단 리터럴 객체를 클래스 객체로 활용할 수 있게 됩니다.

class-transformer이 지원하는 기능

plainToClass

앞선 코드에서 보았던 plainToClass method 입니다. 이것은 자바스크립트 객체를 특정 클래스의 인스턴스로 변환합니다.

import { plainToClass } from 'class-transformer';

let users = plainToClass(User, userJson); // to convert user plain object a single user. also supports arrays

classToPlain

이 method는 클래스를 객체를 일반 자바스크립트 객체로 다시 변환합니다.

import { classToPlain } from 'class-transformer';
let photo = classToPlain(photo);

instanceToInstance

이 method는 클래스 객체를 새로운 인스턴스로 변환합니다. 객체를 전체 복제한다고 보시면 됩니다.

import { instanceToInstance } from 'class-transformer';
let photo = instanceToInstance(photo);

serialize & deserialize

직렬화와 역직렬화는 아래 코드처럼 사용할 수 있습니다. 데이터 크기를 줄여서 주고 받을 때 사용합니다.

import { serialize, deserialize, deserializeArray } from 'class-transformer';
let photo = serialize(photo);

let photo = deserialize(Photo, photo);

let photos = deserializeArray(Photo, photos); // 배열을 역직렬화합니다.

제네릭을 활용한 API 함수

앞서 다룬 plainToClass는 계속해서 호출해야 한다는 단점이 있습니다.

이것이 귀찮다면 별도의 함수를 만들어서 사용할 수 있습니다.

export const instance: AxiosInstance = axios.create({
  responseType: 'json',
  validateStatus(status) {
    return [200].includes(status);
  },
});

export async function request<T>(
  config: AxiosRequestConfig,
  classType: any,
): Promise<T> {
  const response = await instance.request<T>(config);
  return plainToClass<T, AxiosResponse['data']>(classType, response.data);
}

위와 같이 말이죠. Axios를 많이 사용하기 때문에 Axios 객체로 만들었는데요. 비슷하게 다른 곳에서도 제네릭으로 만들 수 있습니다.

export class PaginatedResponseDto<T> {

  @Exclude()
  private type: Function;

  @Expose()
  @ApiProperty()
  @Type(opt => (opt.newObject as PaginatedResponseDto<T>).type)
  data: T[];

  constructor(type: Function) {
    this.type = type;
  }
}

위 코드는 NestJS에서 많이 사용하는 코드입니다. DTO 클래스를 만들 때 제네릭을 통해 리터럴 객체를 클래스 객체로 자동으로 변환합니다.

위와 같이 작성하면 Request 요청 중 Post로 받을 때 body json을 자동으로 클래스 객체로 치환해줍니다. 그리고 데코레이터 설정에 따라 필요한 값을 매핑하거나 제외할 수 있습니다. 그리고 복잡한 Type도 대응해서 매핑이 가능합니다.

중첩 객체 매핑

클래스 내부에는 일반 속성 뿐만 아니라 다른 클래스를 넣어 사용하는 중첩된 객체도 존재합니다. 물론 일반 리터럴 객체에도 중첩된 값들이 들어갈 수 있습니다. 이것을 매핑하기 위해서는 @Type 데코레이터를 사용합니다.

import { Type, plainToClass } from 'class-transformer';

export class Album {
  id: number;

  name: string;

  @Type(() => Photo)
  photos: Photo[];
}

export class Photo {
  id: number;
  filename: string;
}

let album = plainToClass(Album, albumJson);

위와 같이 Type 데코레이터를 통해 중첩된 객체를 클래스로 매핑 할 수 있습니다.

데코레이터 사용법

@Expose

expose 데코레이터는 getter 또는 method에 사용할 수 있으며, 사용하면 반환하는 내용을 노출할 수 있습니다.

import { Expose } from 'class-transformer';

export class User {
	@Expose({ name: 'uid' })
	id: number;
  firstName: string;
  lastName: string;
  password: string;

  @Expose()
  get name() {
    return this.firstName + ' ' + this.lastName;
  }

	

  @Expose()
  getFullName() {
    return this.firstName + ' ' + this.lastName;
  }
}

@Exclude

특정 속성을 제외해서 반환을 막을 수 있습니다.

import { Exclude } from 'class-transformer';

export class User {
  id: number;

  email: string;

  @Exclude({ toPlainOnly: true })
  password: string;
}

@Type

일반 자바스크립트 객체를 다양한 형태로 변환하는 데코레이터입니다.

날짜 문자열을 Date 객체로 반환하고, 배열로 변경하거나, Transform 데코레이터를 활용하여 추가 데이터 변환을 할 수 있습니다.

import { Type } from 'class-transformer';
import * as moment from 'moment';
import { Moment } from 'moment';

export class Skill {
  name: string;
}

export class Weapon {
  name: string;
  range: number;
}

export class Player {
  name: string;

  @Type(() => Date)
  registrationDate: Date;

	@Type(() => Date)
  @Transform(({ value }) => moment(value), { toClassOnly: true })
  date: Moment;

  @Type(() => Skill)
  skills: Set<Skill>;

  @Type(() => Weapon)
  weapons: Map<string, Weapon>;
}

마무리

여기까지가 class-transformer와 많이 사용하는 데코레이터 @Expose에 대한 내용을 다뤘습니다.

공부하면서 javascript는 클래스 객체가 도입된 지 얼마 되지 않았고, TypeScript까지 나오면서 객체지향 프로그래밍으로 많은 시도를 하고 있습니다. 그러다보니 원래 리터럴 객체를 사용했던 JavaScript에서 클래스로 맵핑하는 과정이 순조롭지 않았습니다. 이 부분이 다른 객체지향 언어와의 차이점이 되겠네요

다행히 class-transformer로 어느 정도 문제를 해결할 수 있기 때문에 다행이죠.

참고자료

https://jojoldu.tistory.com/617

https://github.com/typestack/class-transformer#what-is-class-transformer

함께 보면 좋은 글

https://www.centbin.com/decorator%ec%97%90-%eb%8c%80%ed%95%9c-%ea%b8%b0%ec%b4%88%ec%99%80-%ed%99%9c%ec%9a%a9-1-typescript/

Leave a Comment