sogno
작성일
2023. 6. 11. 00:57
작성자
sognociel

뭔가 이것저것 짬뽕인 느낌이지만 길이도 그렇게 길지 않을 테고.. 복습 겸 한꺼번에 모아두는 게 좋을 것 같았다.

나는 차트 데이터에 관해서 코드를 짜보았는데, class-validator는 로그인 등 다양한 곳에서 사용하는 것 같더라..!

요약 : class-validator로 DTO타입 체크하는 여정기.

 

class-validator

class-validator란 joi의 Typescript 버전으로, 데코레이터를 이용하여 편리하게 검증할 수 있는 라이브러리이다. 서버로 들어오는 Json 데이터의 검증을 할 때 유용하게 사용할 수 있다.

joi란 무엇이냐? 이것 또한 validation 라이브러리라고 보면 된다.

 

DTO (Data Transfer Object)
DTO란 말 그대로 해석하면 '데이터 전송 객체'가 된다고 한다. 데이터의 전송을 담당하는 클래스라고 할 수 있으며 웹 서비스의 클라이언트와 서버의 서비스 계층 사이에서 교환되는 데이터를 담는 그릇이라고 할 수 있다. 쉽게? 말하자면 계층 간 데이터 교환을 위한 객체..! 인 듯?
DAO(Data Access Object) 패턴에서 유래된 단어로 DAO에서 DB처리 로직을 숨기고 DTO라는 결괏값을 내보내는 용도로 활용했다고 한다.

 

차트는 react-chartjs-2를 사용했는데, 기본적으로 필요한 데이터는 아래와 같이 설정했다.

차트의 범례가 될 labels와 값이 될 data, 그리고 그려질 차트의 색이 될 backgroundColor를 필수 값으로 설정하고 나머지는 선택으로!

export interface ChartProps {
  labels: string[];
  datasets: {
    data: number[];
    backgroundColor: string[];
    label?: string;
    borderColor?: string[];
    borderWidth?: number;
  }[];
}

const chartDataBase: ChartProps = {
  labels: [],
  datasets: [
    {
      data: [],
      backgroundColor: [],
    },
  ],
};

이 차트의 데이터는 하드코딩이 아니라 API를 통해 받아오게 되는데, 이 받아오는 과정에서 그 값들이 맞게 들어왔는지 확인...? 하기 위해 class-validator를 사용하는 것 같다.

class-validator가 데이터를 검증하는 것은 알겠어! DTO가 데이터 전송 객체라는 것도 대충은 알겠어..! 그런데 둘 사이의 관계가 도대체 어떻게 되는데? -> 정말 헷갈렸는데 찾다 보니 이런 글이 있더라

 

어떠한 값을 검증하려면 그 값에 대한 정의가 필요하겠죠?
그 정의를 dto라는 곳에서 하게 됩니다

 

오마이갓... 그제야 개안한 느낌이었다. DTO로 데이터의 형식을 정의하고, 그 정의를 기반으로 인스턴스를 만들어서 값을 집어넣은 다음 집어넣은 데이터들이 정의에 맞는지 아닌지를 class-validator로 확인한다는... 그런 것이었다. (맞나? 여튼...)

 

그리고 chat-GPT의 도움을 받아(ㅎ..) 아래와 같이 chart 데이터에 대한 DTO를 작성해 주었다.

import {
  IsArray,
  IsString,
  ValidateNested,
  IsNumber,
  IsOptional,
  IsInt,
  Min,
} from 'class-validator';

export class DatasetDTO {
  @IsArray()
  @IsNumber({}, {each: true})
  data: number[];

  @IsArray()
  @IsString({each: true})
  backgroundColor: string[];

  @IsOptional()
  @IsString()
  label?: string;

  @IsOptional()
  @IsArray()
  @IsString({each: true})
  borderColor?: string[];

  @IsOptional()
  @IsNumber()
  @IsInt()
  @Min(0)
  borderWidth?: number;
}

export class ChartDataPropsDTO {
  @IsArray()
  @IsString({each: true})
  labels: string[];

  @ValidateNested({each: true})
  @IsArray()
  datasets: DatasetDTO[];
}

tsconfig.json 에서 strictPropertyInitialization을 true로 해주었다면 아마 속성 ㅇㅇ은 이니셜라이저가 없고 생성자에 할당되어 있지 않습니다.라는 오류가 뜰 것이다. 아마 데코레이터에 지정되어 있지 않은 속성(내가 만든 것)이라 그런 것 같은데 그럴 때는! 를 붙여주면 된다.

@IsArray()
@IsNumber({}, {each: true})
data!: number[];

 

이제는 API로 전송된 값을 DTO를 통해 만들어주고, validate를 통해 검증해 보겠다!

그래서...! 여기서 알고 가야 하는 게 직렬화/역직렬화이다. 클래스, 객체, 인스턴스 세 가지의 개념이 있는데 간단하게 설명해 보자면

클래스(Class) | 객체를 만들어 내기 위한 설계도 혹은 툴.
객체(Object) | 소프트웨어 세계에 구현할 대상. 클래스에 선언된 모양 그대로 생성된 실체.

인스턴스(Instance) | 설계도인 클래스를 바탕으로 소프트웨어 세계에 구현된 구체적인 실체.

 

직렬화 (객체 -> 인스턴스)

객체를 직렬화하여 전송 가능한 형태로 만드는 것. 객체들의 데이터를 연속적인 데이터로 변형하여 stream을 통해 데이터를 읽도록 해준다
역직렬화 (인스턴스 -> 객체)
직렬화된 파일 등을 역으로 직렬화하여 다시 객체의 형태로 만드는 것을 의미한다. 저장된 파일을 읽거나 전송된 스트림 데이터를 읽어 원래 객체의 형태로 복원한다.

 

그리고 이런 직렬화, 역직렬화를 해주는 라이브러리가 class-transformer 이다.

validate를 이행하기 위해 DTO로 새로운 인스턴스를 만들어주는데, 내가 설정한 차트 데이터에서 dataset은 또 인스턴스로 설정이 되어있다. 따라서 값을 그대로 넣을 수 있는 labels와는 다르게 그 안에 값을 집어넣기 위해서는 객체형태가 아닌 인스턴스 형태로 넣어주어야 한다. 이때 class-transformer를 사용하는 것.

 

사용법은 아래와 같은데, 버전? 때문인지 plainToClass가 아니라 plainToInstance를 사용하는 듯하다.

// 직렬화 예시
let users = plainToInstance(User, userJson)

처음에 봤을 땐 뭐.,..User에 뭘 넣어야 하고 userJson에는 뭘 넣어야 하는 건데... 했는데 알고 보니 User에는 변형할 인스턴스의 형태를 가진 것(DTO라고 하자...)이고 userJson에는 객체의 값이 들어가면 되는 것이었다.

나는 plainToInstance만 사용했으니.. 여기서는 이것만 설명하겠다.

 

그리고 완성된 코드..! 😭

const chartDataBase: ChartProps = {
  labels: [],
  datasets: [
    {
      data: [],
      backgroundColor: [],
    },
  ],
};

const [data, setData] = useState<ChartDataPropsDTO>(chartDataBase);

useEffect(() => {
const setChartData = (data: ChartProps) => { // data는 객체의 형태로 들어온다
  const chartData = new ChartDataPropsDTO();
  chartData.labels = data.labels;
  chartData.datasets = plainToInstance(DatasetDTO, data.datasets); // DatasetDTO에 맞게 직렬화
  return chartData; // return 되는 chartData는 ChartDataPropsDTO형태의 인스턴스
};

const fetchData = async () => {
  const resolvedChartData = setChartData({ // 객체 들어가욧
    labels: ['Label 1', 'Label 2', 'Label 3'],
    datasets: [
      {
        data: [20, 30, 50],
        backgroundColor: ['red', 'green', 'blue'],
      },
    ],
  });

  const errors = await validate(resolvedChartData); // 그리고 인스턴스를 validate 한다
  if (errors.length > 0) throw new Error(JSON.stringify(errors));
  setData(resolvedChartData); // validate가 통과하면 차트 데이터를 set!
};

fetchData();
}, []);

 

처음 코드를 작성하고 나서는 이게 뭔지.. 맞는지... 잘 모르겠었는데 그래도 이렇게 정리해 보니까 쪼끔... 알 것 같기도?

사실 fetchData에서 resolvedChartData는 인스턴스의 형태일 텐데 차트에 넣어주는 data는 객체... 일 텐데.. 화면에 그려지긴 해서 상관이 없..나..? 싶으면ㅅㅓ도 이거 되면 안 되는 거 아냐..? 의 무한반복...

에러가 난다면 아..! instanceToPlain으로 변형해서 넣어주면 되겠구나! 하겠는데 오류 없이 잘.. 그려져서...

나중에 transformer로 한번 변형해서 넣어봐야겠다. ^^)9

'일하면서 공부해욧' 카테고리의 다른 글

Delete `␍` eslint (prettier/prettier)  (0) 2023.07.01
custom class validator  (0) 2023.06.25
node.js와 express  (0) 2023.06.18
React, TypeScript 그리고 Jest  (0) 2023.05.26
Jest  (0) 2023.05.26