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/

13 thoughts on “class-transformer란? @Expose, @Exclude, @Type”

  1. seleb66
    Hello There. I found your blog using msn. That is an extremely well written article.

    I’ll be sure to bookmark it and come back to learn extra of
    your helpful information. Thank you for the post. I will definitely comeback.

    응답
  2. big777
    Do you mind if I quote a couple of your posts as long as
    I provide credit and sources back to your blog? My blog is in the very same area of interest as yours and my users would
    really benefit from a lot of the information you provide here.
    Please let me know if this alright with you. Appreciate it!

    응답
  3. bromo77 bromo77 bromo77 bromo77
    Howdy very cool blog!! Man .. Excellent ..

    Wonderful .. I will bookmark your blog and take the feeds also?
    I’m glad to search out numerous helpful information here within the post, we need develop extra strategies
    on this regard, thank you for sharing. . . . . .

    응답
  4. yandex semua
    Does your website have a contact page? I’m having problems locating it but, I’d like to
    send you an email. I’ve got some recommendations for your blog you might be interested in hearing.
    Either way, great site and I look forward to seeing it improve over time.

    응답

Leave a Comment