데코레이터는 클래스 로직을 추가하거나 대체하여 다양한 작업을 할 수 있습니다.
link - Typescript: Decorators
예외를 자주 일으키는 값들이 있습니다.
웹 리소스로 부터 fetch 한 결과나, 사용자 입력 데이터를 사용할 때 그렇습니다.
간단한 입력에도 예상치 못한 데이터가 들어옵니다.
<form>
<input type="text" id="title" placeholder="Input Title" />
<input type="text" id="price" placeholder="Input Price" />
<button type="submit">Add</button>
</form>
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);
});
사용자 입력을 인자로 할 때, 원하지 않는 값을 넣는 것을 막기 위해
보통 조건문을 통해 인자로 넣는 것을 막습니다.
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()
데코레이터를 이용해 정의할 때,
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"]
}
}
만들어진 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 함수
를 사용합니다.
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를 추가할 수 있는게 멋있다고 생각하고
열심히 정리해보았다.