reflection과 decorator는 주로 입력값 유효성 검사할 때 사용한다.
class Idol{
name: string;
age: number;
constructor(name:string, age: number){
this.name = name;
this.age = age;
}
// style에는 '신나게' | '열정적으로'
sing(style : '신나게' | '열정적으로'){ //이렇게 할 수 있지만 JS에서는 아무값이나 가능해진다.
if (style !== '신나게' && style !== '열정적으로') throw Error('안됨');
//이렇게도 가능하지만 복잡해진다.
// 파라미터가 계속 추가될 수록 if문은 증가한다.
return `${this.name}이 ${style}노래를 부릅니다.`
}
// 여기서 데코레이팅 하면 target은 실제 idol타입의 프로토타입가 된다.
}
그냥 sing입력받을 때 '신나게' '열정적으로'를 받도록 할 수 있다. 하지만 TS에서는 2개만 입력받을 수 있지만 JS에서는 아무값이든 다 받을 수 있다.
그래서 if문으로 입력값을 체크해야 하지만 만약 파라미터가 여러개면 상당히 복잡해진다.
데코레이터를 사용해서 유효성 검사를 하자
class Idol{
name: string;
age: number;
constructor(name:string, age: number){
this.name = name;
this.age = age;
}
@validateMethod
sing(@RestrictParamValue(['신나게', '열정적으로']) style : string,
@RestrictParamValue([1,2,3]) ranking:number){ //이렇게 할 수 있지만 JS에서는 아무값이나 가능해진다. 원래라면 style:string보단 style : '신나게' | '열정적으로'가 좋다. style은 사용자가 입력한 값 타입
// if (style !== '신나게' && style !== '열정적으로') throw Error('안됨');
return `${this.name}이 ${style}노래를 부릅니다.`
}
}
일단 sing메서드에 파라미터가 2개 들어와야 한다.
style변수에 string타입, ranking라는 변수에 number타입이 와야한다.
보면 RestrictParamValue라고 파라미터 데코레이터를 style, ranking 이렇게 2번 사용한 것을 볼 수 있다.
또한 메서드 데코레이터로 validateMethod를 볼 수 있다. 즉 클래스 선언과 동시에 이 데코레이터를 가지고 함수를 데코레이터?(확장)을 해보자
const restrictParamValueKey = Symbol('restrictParamValue'); // symbol을 쓰면 어떤게 들어가든 선언하는 순간 프로그램이 실행하는 동안 이 값과 절대로 똑같은 값은 존재하지 않도록 할 수 있다.
interface RestrictionInfo<T>{
index:number;
restrictedValues: T[];
}
function RestrictParamValue<T>(restrictedValues: T[]){ //여기서 T는 string이다. restrictedValues = ['신나게' ,'열정적으로']
return (target: any, propertyKey: string, index: number) => {
///메타데이터 안에다가 모든 이 restriction 값들을 전부 다 관리를 할 수 있게 한다.
//메서드에다가 메타데이터를 저장하게 되면 다른 메서드랑 겹칠일 이 없다.
// 메타데이터를 먼저 가져오는 게 기존에 저장된 값들이 있으면 그 값들을 불러오고 그 값들에다가 추가해야 함
// target은 클래스, proprotyKey는 메서드 이름
const prevMeta = Reflect.getOwnMetadata(restrictParamValueKey, target, propertyKey) ?? []; // 저장된 것이 없으면 그냥 리스트
// ?? => null이거나 undefined이거나 false에 해당하는지 체크
const info: RestrictionInfo<T> = { //T는 string
index,
restrictedValues,
}
Reflect.defineMetadata(restrictParamValueKey, [
...prevMeta,
info,
], target, propertyKey); //리스트에 계속 쌓이게 됨 target에 propertyKey에다 저장한다.
console.log(Reflect.getOwnMetadata(restrictParamValueKey, target, propertyKey));
}
}
@validateMethod
sing(@RestrictParamValue(['신나게', '열정적으로']) style : string, @RestrictParamValue([1,2,3]) ranking:number){ //이렇게 할 수 있지만 JS에서는 아무값이나 가능해진다. 원래라면 style:string보단 style : '신나게' | '열정적으로'가 좋다. style은 사용자가 입력한 값 타입
// if (style !== '신나게' && style !== '열정적으로') throw Error('안됨');
return `${this.name}이 ${style}노래를 부릅니다.`
}
RestrictParamValue 데코레이터 메서드이다.
제너릭으로 받은 T는 string타입인 것을 알 수 있다. '신나게', '열정적으로'는 string타입이기 때문이다.
그리고 파라미터 데코레이터 스타일은 (target: any, propertyKey: string, index: number)이다.
이제 파라미터데코레이터로 전달받은 ['신나게', '열정적으로']를 Reflection으로 해당 메서드에 저장할 것이다.
일단 이전의 데이터를 가져와야 한다. Reflect.getOwnMetadata로 이전데이터를 가져오고
현재 파라미터 index = 0일 때 info = {0, ['신나게', '열정적으로']}
index = 1일 때 info {1, [1,2,3]}이 된다.
이제 이전 데이터와 현재 데이터를 합친 것을 다시 method에다가 메타데이터를 저장한다.
여기서 target은 Idol클래스를 propertKey는 method명인 sing이 된다.
즉 symbol로 저장한 restrictParamValueKey키에 ...prev + info값을 합친 value를 target(Idol)클래스에서 sing프로퍼티에 저장하겠다라는 의미가 된다.
이렇게 저장하는 것은 되었다.
이제 validate하는 함수를 정의해보자
function validateMethod(target: any, propertyKey:string, descriptor: PropertyDescriptor){
const metas : RestrictionInfo<any>[] = Reflect.getOwnMetadata(restrictParamValueKey, target,propertyKey) ?? []; // 메타데이터에 저장한 값이 나옴
const original = descriptor.value;
descriptor.value = function(...args: any){ //sing메서드에 입력한 파라미터들이 any타입으로 들어온다.
const invalids = metas.filter(
(x) => !x.restrictedValues.includes(args[x.index])
);
if(invalids.length > 0) throw Error(`잘못된 값입니다. ${invalids.map(x => args[x.index]).join(', ')}`)
console.log("QWeqwew");
return original.apply(this, args)
}
}
메서드 데코레이터 파라미터 시그니처로는 (target: any, propertyKey:string, descriptor: PropertyDescriptor)이다.
metas변수에 메타데이터에 저장한 값들 [{0, ['신나게', '열정적으로'], {1, [1,2,3]}]을 가져온다.
original에는 현재 함수(descriptor.value)를 가져온다.
그리고 현재 함수를 데코레이터를 하자.
...args는 sing에 입력한 파라미터들이 any타입으로 들어오게 된다.
그래서 metas를 filter함수로 입력한 값들이 유효성검사를 하여 에러를 발생시킨다.