reflection and decorator

홍범선·2023년 11월 1일
0

타입스크립트

목록 보기
30/34

reflect과 decorator

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함수로 입력한 값들이 유효성검사를 하여 에러를 발생시킨다.

profile
알고리즘 정리 블로그입니다.

0개의 댓글