타입스크립트 Decorator로 프로퍼티에 유효성 설정하기

Yuno·2022년 10월 30일
2
post-thumbnail

데코레이터는 클래스 로직을 추가하거나 대체하여 다양한 작업을 할 수 있습니다.
link - Typescript: Decorators

입력 데이터를 Validate할 때

예외를 자주 일으키는 값들이 있습니다.

웹 리소스로 부터 fetch 한 결과나, 사용자 입력 데이터를 사용할 때 그렇습니다.
간단한 입력에도 예상치 못한 데이터가 들어옵니다.

  • html
<form>
  <input type="text" id="title" placeholder="Input Title" />
  <input type="text" id="price" placeholder="Input Price" />

  <button type="submit">Add</button>
</form>
  • app
document.querySelector('form')!.addEventListener('submit', e => {
  e.preventDefault();
  
  const titleEl = document.getElementById('title') as HTMLInputElement;
  const priceEl = document.getElementById('price') as HTMLInputElement;

  const title = titleEl.value;
  const price = +priceEl.value;

  const createdCourse = new Course(title, price);
  console.log(createdCourse);
});

사용자 입력을 인자로 할 때, 원하지 않는 값을 넣는 것을 막기 위해
보통 조건문을 통해 인자로 넣는 것을 막습니다.

  • app - validate
document.querySelector('form')!.addEventListener('submit', e => {
	// 생략 
  
  	const title = titleEl.value;
  	const price = +priceEl.value;
 
	// validate 확인
  	if (title === '' || isNaN(price)) {
  		return;
  	}


  	const createdCourse = new Course(title, price);
  	console.log(createdCourse);
});

이 때, 매번 모든 검증 조건을 확인해야 하는 문제가 생깁니다.
클래스 프로퍼티 자체에 검증을 추가한다면 더욱 괜찮을 것 같습니다.

목표 🏁

클래스 프로퍼티에 어떤 검증이 필요한지 직접 추가하고,
모든 인스턴스를 검증하는 validate 함수를 만듭니다.

class Course {
  @Required
  title: string;
  @Positive // positive number
  price: number;

  constructor(title: string, price: number) {
    this.title = title;
    this.price = price;
  }
}

데코레이터를 통해 프로퍼티가 어떤 조건이 필요한지 간단히 추가합니다.

{
  // ...생략
  
  const createdCourse = new Course(title, price);
  
  // 간략화 된 validate
  if (!validate(createdCourse)) {
    alert('Invalid input, please try again');
    return;
  }

  console.log(createdCourse);
}

함수를 통해 생성한 인스턴스를 검증합니다.

클래스 프로퍼티 데코레이터

프로퍼티 데코레이터는,
프로토타입프로퍼티 이름을 파라미터로 합니다.

function Required(target: any, propName: string | Symbol) {
	console.log(target, propName)
}

// target: 클래스의 프로토타입 객체

이들을 활용하여 검증을 위한 설정을 만듭니다.

interface ValidateConfig{ 
	[name: string] : { // 규칙을 추가할 클래스명
    	[propName: string]: string[] // 내부 프로퍼티 명: 규칙
    }
}

// Validate 설정을 저장
const generatedValidateConfig: ValidateConfig = {}

Validate 설정을 저장할 객체를 만들고,
데코레이터를 사용하여 클래스가 정의될 때 Validate 설정을 완성합니다.

프로토타입

프로토타입의 세부적인 설명은 하지 않겠습니다.

자바스크립트에서 상속은 프로토타입의 체인으로 구현되어 있고,
값이 할당 된 모든 변수는 프로토타입을 가집니다.

궁금하다면 f12를 눌러 콘솔을 열고
x = 12와 같은 변수를 만들고 x.__proto__를 확인해 보십시오
우리가 사용하는 변수가 어떤 체인을 가지고 있는지 알 수 있습니다.

원시 값을 가진 변수를 선언했는데 그 뒤에 메소드를 붙혀 사용할 수 있는 이유이기도 합니다.

let x = 12
x.toString()

클래스 프로토타입

클래스는 프로토타입이 있는데, (class) A -> A.prototype
이는 인스턴스의 프로토타입 체인이 될 객체입니다.

메소드가 없는 빈 클래스이더라도 프로토타입은 존재합니다.
또한, 모든 프로토타입은 constructor이라는 프로퍼티를 가집니다.

constructor 프로퍼티는 함수(생성자 함수) 자신을 가르킵니다.

class A {}
const a1 = new A()

//true
a1.constructor === A 

// 혹은
const a2 = new a1.consturctor()

Validate Config 저장하기

데코레이터를 이용해 정의할 때,
validateConfig에 설정을 추가하겠습니다.

const generatedValidateConfig: ValidateConfig = {}

프로토타입의 구성에 대해 알았으니,
다음처럼 타입을 지정할 수 있습니다.

interface Constructorable {
	constructor: {name: string}
}

데코레이터에서 config를 추가합니다.

function Required(target: Constuctorable, propName: string) {
	const name = target.constructor.name;
  
  	generatedValidateConfig[name] = {
      	...generatedValidateConfig[name],
    	[propName] = ['required']
    }
}

function Positive(target: Constuctorable, propName: string) {
	const name = target.constructor.name;
  
  	generatedValidateConfig[name] = {
      	...generatedValidateConfig[name],
    	[propName] = ['positive']
    }
}

프로퍼티에 여러 validate를 추가할 수 있게 수정합니다.

generatedValidateConfig[name] = {
  ...generatedValidateConfig[name],
  [propName] = [
  	...generatedValidateConfig[name]?.[proName] ?? [],
    'validateName',
  ]
}

이제 앞서 본 목표에서 처럼 데코레이터를 적용하면

class Course {
  @Required
  title: string;
  @Positive // positive number
  @Required
  price: number;

  constructor(title: string, price: number) {
    this.title = title;
    this.price = price;
  }
}

이런 Course 클래스를 정의할 때
genereatedValidateConfig는 다음 값이 될겁니다.

{
	"Course": {
      	"title": ["required"]
      	"price": ["required", "Positive"]
    }
}

Validate 함수 만들기

만들어진 config를 통해 주어진 인스턴스가 유효한지 확인하는 함수를 만듭니다.

const validate = (obj: any) => {
  let isValide = true;
  
  return isValide
}

인스턴스인 파라미터 obj는 유연하면서 어떤 타입인지 알 수 없으므로 any가 적절합니다

또한, obj 인스턴스는
프로토타입 체인을 통해 obj.constructor로 생성한 클래스를 얻을 수 있습니다.

const validate = (obj: any) => {
  const currentConfig = generatedValidateConfig[obj.constuctor.name]
  
  let isValide = true;
  return isValide
}

이제 인스턴스가 설정한 규칙에 부합하는지 확인합니다.

const validate = (obj: any) => {
  const currentConfig = genertedValidateConfig[obj.constuctor.name];
  
  let isValide = true;
  for (const propName in currentConfig) { // title, price
  	for (const validateRule of currentConfig[propName]) { // required, positive
    	switch(validateRule) {
          	case "required":
            	isValide = isValide && Boolean(obj[propName])
            	break;
          	case "positive":
            	isValide = isValide && obj[propName] > 0
            	break;
        }
    }
  }
  
  return isValide
}

isValide = isValide && boolean
isValide &&= boolean으로 간략화 할 수 있습니다.

이제 이 함수를 통해 목표했던 것 처럼 한번에 validate 할 수 있습니다.

const createdCourse = new Course(title, price);

if (!validate(createdCourse)) {
  alert('Invalid input, please try again');
  return;
}

console.log(createdCourse);


직접 결과를 확인할 수 있습니다.

유연한 Validate 데코레이터 만들기

validate 확인을 위한 코드를 작성할 때, 여러 규칙을 미리 정의하기도 하지만
유효성 확인을 위한 validate 함수를 정의하기도 합니다.

한개의 프로퍼티가 복잡한 규칙을 지닌다면, 이런 유연한 방식이 좋아보입니다.

이전 데코레이터와 비슷하지만 데코레이터 팩토리를 통해 validate 함수를 사용합니다.
link - 데코레이터 팩토리란?

type ValidateFn = (value: any) => boolean;

interface ValidateConfig{ 
	[name: string] : { 
    	[propName: string]: (string | ValidateFn)[]
    }
}

// decorator factory
function WithValidate(validate: ValidateFn) {
	return function(target: Constructorable, propName: string) {
      	generatedValidateConfig[name] = {
          ...generatedValidateConfig[name],
          [propName] = [
            ...generatedValidateConfig[name]?.[proName] ?? [],
            validate, // Rule String 대신, validate 함수를 추가
          ]
        }
    }
}

WithValidate 데코레이터를 적용합니다.

class Course {
  	@WithValidate(v => v.trim().length > 5)
	title: string;
  	@Positive
  	price: number;
  
  	// 생략
}

validate 함수를 수정하여, 함수형 규칙도 동작하도록 합니다.


const validate = (instance: any) => {
  const validateConfig = registeredValidators[instance.constructor.name];

  if (!validateConfig) {
    return true;
  }

  let isValid = true;
  for (const prop in validateConfig) {
    for (const valid of validateConfig[prop]) {
      
      // ====== 추가된 부분 ======
      if (typeof valid === 'function') {
        isValid &&= valid(instance[prop]);
      }
      // =======================

      switch (valid) {
        case ValidateEnums.Required:
          isValid = isValid && !!instance[prop];
          break;
        case ValidateEnums.Positive:
          isValid = isValid && instance[prop] > 0;
          break;
      }
    }
  }

  return isValid;
};

결과

활용 예시

이런 방법을 사용한, 보다 정교한 패키지를 찾을 수 있습니다.

맺음말

데코레이터를 이해하면 이를 주로 사용하는 Angular나 Nest.js를 사용하는데 도움이 될 수 있습니다.

지금 개발하는데 얼마나 실용적일지 알 수 없지만,
클래스 프로퍼티에 직접 validate를 추가할 수 있는게 멋있다고 생각하고
열심히 정리해보았다.

profile
web frontend developer

0개의 댓글