class-transformer 로 더 편리하게 작업하기

화솔·2023년 11월 22일
0
post-thumbnail

express 환경에서 여러 HTTP API 라이브러리를 사용하며 불편했던 부분이 여러가지가 있었다.

  • 응답받은 값을 통해 리터럴 객체를 추출하고 클래스 인스턴스를 직접 만들어야 되는 부분
  • 반대로 변환한 클래스 인스턴스를 JSON으로 직렬화 하는 과정
  • 응답 값 중 케이스 컨벤션에 맞지 않다면 값이 있다면 변환해주는 과정
  • 응답 값 중 사용하지 않는 값은 제외하고 싶다

결론적으로 위의 문제상황에 도움을 주는 라이브러리를 활용하면 쉽게 작업을 할 수 있을 것 같다는 생각을 해 class-transformer 모듈을 찾아보게 되었다.

사전 준비

먼저 모듈을 설치해야 한다.

npm install class-transformer --save

그 후 reflect-metadata 모듈을 추가로 설치해줘야 한다.
코드의 메타 정보 (메소드/필드/변수 등)에 접근하기 위한 위함 (리플렉션)

npm install reflect-metadata --save

마지막으로 글로벌한 위치(app.ts)에 import를 해주면 된다.

import 'reflect-metadata';

활용하기

외부 API 호출을 통해 아래의 응답을 받은 상황이라고 해보자

{
  id: '1',
  first_name: 'hwa',
  last_name: 'sowl',
  location: 'ko'
};

서론에서 말한 불편함을 해결하기 위해서는 4가지의 문제 해결이 필요하다.

  1. 응답받은 JSON 객체를 클래스 인스턴스로 변환하고 싶다.
  2. 그 반대로 클래스 인스턴스를 JSON 객체로 직렬화 하고싶다.
  3. 케이스 컨벤션 호환 (스네이크 -> 카멜)
  4. 사용하지 않는 특정 값 제외

그렇다면 class-transformer 모듈을 활용해 위 4가지 문제를 차근히 해결해보자.

1. 응답받은 JSON 객체를 클래스 인스턴스로 변환


먼저 클래스 인스턴스로 변환하기 위해 유저 클래스를 만든 후

class User {
  id: number;
  first_name: string;
  last_name: string;
  location: string;
  
  getName() {
    return this.first_name + ' ' + this.last_name;
  }
}

클래스 인스턴스로 변환하기 위해 plainToInstance 메서드를 활용해보자.

해당 메서드 설명을 보면 리터럴 객체를 클래스로 변환해준다고 되어있다. 배열도 작업이 가능하다고 하니 참고하면 좋겠다.

const response = await request() // 응답받은 JSON 객체
const user = plainToInstance(User, response) // 유저 클래스

console.log(user.getName())

이렇게 편리하게 클래스와 리터럴 객체값만 넘겨줌으로써 클래스 인스턴스를 생성할 수 있었다.

번외

매번 plainToInstance 메서드를 호출하기 귀찮다면 제네릭 함수를 만들어 편리하게 처리할 수 있다.

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 plainToInstance<T, AxiosResponse['data']>(classType, response.data)
}
return request<GoogleUser>(
    {
        method: 'get',
        url: ENV.GOOGLE.USERINFO_URL,
        headers: bearerHeader(accessToken),
    },
    GoogleUser,
)

2. 클래스 인스턴스 직렬화


위에서 plainToInstance 메서드를 통해 리터럴 객체를 클래스 인스턴스로 변환했었다.

하지만 현실에서는 부득이하게 리터럴 객체로 값을 꺼내 사용해야 되는 경우가 존재한다. 이러한 경우에는 instanceToPlain 메서드를 사용하면 된다.

설명을 보면 클래스 객체를 리터럴 객체로 변환해준다고 설명되어있다. 마찬가지로 배열로도 작업이 가능하다고 한다. 응답 값은 레코드로 반환해준다.

const instance = plainToInstance(User, response) // 클래스 인스턴스

const literal = instanceToPlain(instance)
console.log(literal)

3. 케이스 컨벤션 호환 (스네이크 -> 카멜)


// 응답받은 리터럴 객체
{
  id: '1',
  first_name: 'hwa',
  last_name: 'sowl',
  location: 'ko'
};

위와 같이 컨벤션에 맞지 않는 스네이크 케이스의 값을 받은 상황이고, 프로젝트 컨벤션에 맞게 케이스 변환을 원한다면 Expose 데코레이터를 활용하면 된다.

해당 데코레이터의 문서를 보면 특정 객체의 값을 다른 이름으로 변경하고 싶을 때 @Expose 데코레이터를 활용하면 된다고 한다.

// 적용 전
class User {
  id: number;
  first_name: string;
  last_name: string;
  location: string;
  
  getName() {
    return this.first_name + ' ' + this.last_name;
  }
}

// 적용 후
class User {
  id: number;
  
  @Expose({ name: 'first_name' })
  firstName: string;
  
  @Expose({ name: 'last_name' })
  lastName: string;
  
  location: string;
  
  getName() {
    return this.firstName + ' ' + this.lastName;
  }
}

4. 특정 값 제외


// 응답받은 리터럴 객체
{
  id: '1',
  first_name: 'hwa',
  last_name: 'sowl',
  location: 'ko'
};

특정 값(location)을 제외하고 싶은 상황이라면 @Exclude 데코레이터를 활용하면 된다.

class User {
  id: number;
  
  @Expose({ name: 'first_name' })
  firstName: string;
  
  @Expose({ name: 'last_name' })
  lastName: string;
  
  @Exclude()
  location: string;
  
  getName() {
    return this.firstName + ' ' + this.lastName;
  }
}

출처

https://github.com/typestack/class-transformer
https://jojoldu.tistory.com/617

0개의 댓글