개발-사용기/serverless 사용기

3. Typescript & Serverless 사용기 - class-validator 적용

mouuaw 2020. 7. 8. 22:29

class-validator

 

링크: https://github.com/typestack/class-validator

 

typestack/class-validator

Validation made easy using TypeScript decorators. Contribute to typestack/class-validator development by creating an account on GitHub.

github.com

class-validator는 model을 선언하면서 해당 객체의 validation 기능도 사용할 수 있기 때문에 api를 구성할 때 더욱 안전한 서버를 구축할 수 있게 한다.

 

class-validator를 사용하는 용도는 다양하겠지만 여기선 요청에 대한 validation 수행 작업으로 사용해보겠다.

 

1. class-validator 설치

 

설치는 간단히 다음 명령어를 사용하면 된다.

 

npm i --save class-validator

 

설치 후 typescript의 데코레이션을 사용하기 위해 tsconfig.json을 다음과 같이 변경해주자

 

{
  "compilerOptions": {
    "lib": [
      "es2017"
    ],
    "removeComments": true,
    "moduleResolution": "node",
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "sourceMap": true,
    "target": "es2017",
    "outDir": "lib",
    "baseUrl": ".",
    
    // 추가된부분 START
    "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
    "emitDecoratorMetadata": true,  /* Enables experimental support for emitting type metadata for decorators. */
    // 추가된부분 END
    
    "paths": {
      "@model/*": [
        "models/*"
      ],
      "@api/*": [
        "api/*"
      ],
      "@service/*": [
        "service/*"
      ],
      "@util/*": [
        "util/*"
      ],
      "@config/*": [
        "config/*"
      ],
      "@test/*": [
        "test/*"
      ]
    }
  },
  "include": [
    "./**/*.ts"
  ],
  "exclude": [
    "node_modules/**/*",
    ".serverless/**/*",
    ".webpack/**/*",
    "_warmup/**/*",
    ".vscode/**/*"
  ]
}

 

2. 예제용 Model 생성

 

예제에서 사용할 Model을 생성해보자

 

model
    request-model
        api-example.model.ts

 

request용 모델을 만들기 위해 request-model이라는 폴더를 만들고 하위에 api-example.model.ts 파일을 생성했다. 

api_example.model.ts에 Model Class를 생성해보자

 

// api-example.model.ts

import { IsOptional, IsString, IsNumber } from "class-validator";

export class ApiPostRequest {
    @IsNumber() id: number

    @IsOptional()
    @IsString() name: string
}

 

간단하다! 어떤 의미인지도 파악하기 쉽다. @IsNumber는 id라는 변수가 number 타입이어야 한다는 뜻이고 @IsString은 name변수가 string 타입이라는 것, 그리고 @IsOptional 은 변수가 필수 값이 아니라는 뜻이다. 

사용 가능한 타입 선언 목록은 공식문서를 통해 알아볼 수 있다.

 

사용 가능한 validation 목록: https://github.com/typestack/class-validator#validation-decorators

 

typestack/class-validator

Validation made easy using TypeScript decorators. Contribute to typestack/class-validator development by creating an account on GitHub.

github.com

3. api선언 및 validation 사용

 

api에 validation 구현을 해보자.

 

import { APIGatewayProxyHandler } from 'aws-lambda';
import { validateOrReject } from 'class-validator';
import { ApiPostRequest } from '@model/request-model/api-example.model';


export const api_example_post: APIGatewayProxyHandler = async (event, _context) => {
    try {
        let { body } = <any>event
        body = JSON.parse(body) ----------------------- 1

        const apiPostReqest = new ApiPostRequest() ---- 2
        apiPostReqest.id = body.id
        apiPostReqest.name = body.name

        await validateOrReject(apiPostReqest) --------- 3

        return {
            statusCode: 200,
            body: JSON.stringify({ message: `success` })
        }
    } catch (error) {
        console.log("api_example_post:APIGatewayProxyHandler -> error", error)

        return {
            statusCode: 500,
            body: JSON.stringify({ message: error })
        }
    }
}

 

우선 위에서 구현한 ApiPostRequest를 불러온다. 그리고 class-validator에서 제공하는 validation 유틸 함수인 validateOrReject를 통해 validation을 구현한다. 

 

1. request로 받은 body는 string형태이기 때문에 JSON.parse를 통해 body Object를 생성해준다. 

 

2. new ApiPostRequest()를 통해 모델 인스턴스를 생성해주고 모델에 맞는 프로퍼티를 대입해준다.

 

3. validateOrReject는 Promise를 리턴하기 때문에 await를 사용해주고 있다. class-validator에서 제공하는 검증 함수에는 validate라는 함수가 하나 더 있는데, 이 함수는 validation error를 값으로 반환해주지만 우리가 사용하는 validateOrReject는 validation에 실패할 경우 throw error를 발생시키기 때문에 try-catch 구문에서 사용하기 더 좋다.

 

api를 등록시키기 위해 serverless.yml 파일의 functions에 등록해준다.

 

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          method: get
          path: hello

  api_example_post:
    handler: api/api_example.api_example_post
    events:
      - http:
          method: post
          path: api_example_post

 

handler를 보면 api 경로를 통해 접근할 수 있는 걸 알 수 있다.  events - http의 항목에 method는 get, post, put, delete 등의 http 요청 방법을 정의해주고, path 부분이 api를 요청하는 주소가 입력되는 부분이다.

 

이제 validation이 제대로 동작하는지 알아보기 위해 api 요청을 해보자! 

 

4. Postman을 이용한 api 요청

 

서버를 로컬에 구동하기 전에 serverless.yml 파일에 다음과 같이 serverless-offline 설정을 추가해주자.

 

custom:
  webpack:
    webpackConfig: ./webpack.config.js
    includeModules: true

// 추가된 부분
  serverless-offline:
    useChildProcesses: true

 

serverless-offline을 구동시키고 작업을 하다 보면 변경된 코드 내용이 제대로 반영되지 않을 때가 있는데 useChildProcesses옵션을 활성화시키면 해결 가능하다.

 

sls offline 명령을 통해 서버를 구동시키고 postman으로 요청을 해보자.

 

 

POST: http://localhost:3000/dev/api_example_post에 요청을 보냈고 body는 일부러 validation error 가 발생하도록 요청을 보냈다. 

 

요청 결과를 보면 validation 에러가 제대로 발생했으며 어떤 항목에서 문제가 있는지도 알 수 있는 메시지를 받았다. 

 

그럼 제대로 된 요청을 보낸 결과를 확인해보자.

 

제대로 된 타입으로 요청을 할 경우 success 응답을 받을 수 있다.

 

5. Refactoring

 

코드를 작성하다 보면 일일이 model 인스턴스에 값을 입력하는 부분이나 validation 하는 부분이 반복 작업이 된다. 또한 response를 보내는 부분도 일반적인 경우엔 body와 statusCode 만 설정해서 보낼 수 있도록 몇 가지 util 함수를 만들어서 코드를 개선해보려 한다.

 

http.util.ts 파일을 만들어 다음과 같은 함수를 작성해보자.

 

// util/http.util.ts

import { validateOrReject, ValidationError } from "class-validator"


export const responseCorsHeader = {
    "Access-Control-Allow-Origin": "*",
    'Access-Control-Allow-Credentials': false,
    "Content-Type": "application/json",
    "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT,DELETE"
}

export const requestSuccess = (body, statusCode = 200, header?) => {

    if (typeof body === 'object') {
        body = JSON.stringify(body)
    }

    header = { ...responseCorsHeader, ...header }

    return { statusCode, body }
}

export const requestFail = (body, statusCode = 500, header?) => {

    if (typeof body === 'object') {
        body = JSON.stringify(body)
    }

    header = { ...responseCorsHeader, ...header }

    return { statusCode, body }
}

export const createModelAndValidation = async (classModel, body, paramList) => {
    try {
        paramList.map(param => classModel[param] = body[param])
        await validateOrReject(classModel)

        return classModel

    } catch (error) {

        if (error[0] instanceof ValidationError) {
            const errorMessageList = error.reduce((origin, ele) => {
                Object.keys(ele.constraints).map(key => {
                    origin.push(`${ele.constraints[key]}`)
                })
                return origin
            }, [])
            throw errorMessageList
        } else {
            throw error
        }
    }
}

 

requestSuccess, requestFail 함수는 요청 응답에 공통적으로 사용할 함수이다. 여기서 responseCorsHeader를 넣어주고 있는데 serverless는 생각보다 CORS 문제가 까다롭기 때문에 앞으로 차츰 다뤄볼 예정이다. error response 에도 꼭 CORS header를 넣어주자!

 

createModelAndValidation 함수는 class-validator를 쉽게 쓰기 위해 만든 함수이다. 인스턴스에 값을 넣어주고 validation 결과 문제가 있으면 throw error를 발생시켜준다. 

 

util 함수를 사용해서 코드를 다음과 같이 개선시켜주자

 

import { APIGatewayProxyHandler } from 'aws-lambda';
import { ApiPostRequest } from '@model/request-model/api-example.model';
import { createModelAndValidation, requestSuccess, requestFail } from '@util/http.util';


export const api_example_post: APIGatewayProxyHandler = async (event, _context) => {
    try {
        let { body } = <any>event
        body = JSON.parse(body)

        const optionalParamList = ['name']
        const requireParamList = ['id']
        const paramList = [...requireParamList, ...optionalParamList]

        const apiPostReqest = await createModelAndValidation(new ApiPostRequest(), body, paramList)

        return requestSuccess({ message: `success` })
    } catch (error) {
        console.log("api_example_post:APIGatewayProxyHandler -> error", error)

        return requestFail({ message: error })
    }
}

 

paramList에는 model 인스턴스에 넣어줄 파라미터 이름을 넣어준다. 여기서 파라메터 성격을 조금 더 알기 쉽게 하려고 optional, require 파라미터를 나눠줬다. 

 

이제 validation이 제대로 작동하는지 다시 요청을 해보자.

 

 

에러 메시지 응답이 이전보다 더 보기 쉬워졌다. 

 

요약

 

class-validator를 통해 요청 검증을 더 쉽게 하는 작업을 했고, 몇 가지 유틸 함수를 추가시켜서 리펙토링 작업까지 완료했다. 다음번에는 class-validator와 class-validator-jsonschema를 사용해서 swagger를 자동?으로 만들어주는 작업을 해보겠다.