개요
ES6이후 class키워드의 추가로 클래스형 타입이 도입된 자바스크립트는 물론 제네릭까지도 지원하면서 정적인 형을 지원하는 타입스크립트도 고려하면 클래스를 기반으로 하는 타입프로그래밍은 이제 프론트엔드에서도 충분히 도입해볼만한 기법입니다.
자바스크립트에서 서브클래스를 어떻게 체크해야 하는지를 간단히 살펴봅니다.
인스턴스의 서브클래스
인스턴스와 클래스의 관계에서는 instanceof 연산자를 사용할 수 있습니다. 이 애매하고 js에 거의 없는 키워드형 중위연산자는 “인스턴스 instanceof 클래스(타입)” 형태로 사용하며 이걸 부정할 때는 귀찮게도
!( target instanceof Class) 처럼 괄호로 감싸서 부정해야 합니다.
타입스크립트나 자바스크립트가 is를 키워드로 사용하지 않는다는 점을 고려하면 편의 함수를 만들어 둘 수 있을 것입니다.
const is = (target:any, type:{new (...arg:any):any})=>target instanceof type; console.log(is(new Date(), Date)); //true
이런 게 있으면 조건문에서 !is(…) 형태로 쓰기도 편하고 아예 isnot(…)을 만들어도 되겠죠.
이러한 인스턴스와 클래스의 관계는 prototype체인이라 불리는 전통적인 자바스크립트의 구조에 기반을 두고 있지만 사실 타입시스템을 이용하려는 입장에서는 큰 관련이 없습니다.
타입사이의 서브클래스 관계 확인하기
근데 인스턴스와 타입 사이가 아니라 타입과 타입 사이에 서브클래스 관계를 평가하려면 어찌해야 할까요?
instanceof는 인스턴스에게만 작동하기 때문에 다음과 같은 형태로는 쓸 수 없죠.
class Parent{} class Child extends Parent{} console.log(new Child() instanceof Parent); //true console.log(Child instanceof Parent); //false
이런 클래스 사이의 서브타입 관계를 확인하려면 어쩔 수 없이 자바스크립트 언어의 프로토타입체인을 이해하고 이를 이용해 직접 구현할 수 밖에 없습니다. 이를 작성하기 위해서는 클래스의 prototype과 그 prototype이 상속한 클래스의 prototype을 어떤 식으로 체이닝 하는지에 대한 지식이 필요합니다.
간단히 구조를 만들어보죠. 참고로 타입스크립트 버전으로 작성하겠습니다.
export const isSubClass = (subClass:{new(...arg:any):any}, superClass:{new(...arg:any):any})=>{ const superPrototype = superClass.prototype; let targetPrototype = subClass.prototype; do{ if(targetPrototype === superPrototype) return true; targetPrototype = Object.getPrototypeOf(targetPrototype); }while(targetPrototype); return false; };
우선 시그니처를 보면 생성자만 받아들이게 되어있습니다. 생성자간의 서브클래스 관계를 확인하기 위한거니 당연하다면 당연한데 타입스크립트가 아니라 나중에 자바스크립트 겸용으로 쓸걸 생각하면 사실 코드 안에 인자검사를 추가해야할 필요도 있습니다만 생략했습니다.
클래스간의 서브타입 확인은 prototype체인을 통해 검사해야 합니다.
만약 바로 부모클래스라면 최초의 subClass.prototype과 superClass.prototype의 비교에서 바로 일치하겠지만 부모가 아닌 조손관계나 더 깊은 상속관계라면 프로토타입체인 시스템으로 거슬러 올라가면서 전부 찾아봐야합니다.
이렇게 현재 prototype의 상위 체인 prototype으로 가려면 반드시 prototype.__proto__를 찾거나 아니면 Object.getPrototypeOf를 이용해 찾아야 합니다.
이를 다시 targetPrototype에 넣어주고 같은 과정을 반복하여 최종적으로 체인 내에서 superClass의 prototype과 일치하는 경우가 없다면 자손관계가 없다는 것을 확정지을 수 있습니다.
캐쉬를 통한 속도 개선
이 과정은 매번 수행할 때마다 루프를 돌게 되므로 클래스의 구조가 확정적이라는 가정 하에 캐쉬를 잡아도 무방합니다. 짜피 객체 간의 관계이므로 WeakMap와 WeakSet을 이용하면 됩니다.
캐쉬는 관계가 서브클래스가 확인된 경우와 서브클래스가 아님이 밝혀진 경우로 두가지가 있는데, 여기서는 확인된 경우만 캐쉬에 잡도록 하겠습니다.
//0 const cache: WeakMap<{ new(): any }, WeakSet<{ new(): any }>> = new WeakMap(); export const isSubClass = (subClass:{new(...arg:any):any}, superClass:{new(...arg:any):any})=>{ //1 if(!cache.has(subClass)) cache.set(subClass, new WeakSet()); if(cache.get(subClass)?.has(superClass) == true) return true; const superPrototype = superClass.prototype; let targetPrototype = subClass.prototype; do{ if(targetPrototype === superPrototype){ //2 cache.get(subClass)?.add(superClass); return true; } targetPrototype = Object.getPrototypeOf(targetPrototype); }while(targetPrototype); return false; };
전체코드에서 변경된 부분을 살펴보면
0번에서 인메모리 스코프의 캐쉬를 설정한 맵을 만듭니다. 키엔 클래스가 들어갈테고, 값에는 검증된 슈퍼클래스를 담을 셋이 들어갈 것입니다.
1번에서 우선 캐쉬에 없는 서브클래스는 맵에 등록해줍니다. 이 때 그 서브클래스와 관계를 저장할 셋도 생성하는거죠. 그러고나면 무조건 서브클래스는 맵에 존재하므로 그 셋에 슈퍼클래스가 있는지 확인하여 있으면 여기서 종결합니다.
2번에는 슈퍼클래스임이 확인된 시점에 캐쉬에도 등록하는 것을 볼 수 있습니다.
결론
정말 정말 오랜만에 포스팅을 하니 좀 생소한 느낌마저 있군요. 앞으로 자주 포스팅을 해야겠습니다.
타입스크립트와 class구문을 이용한 강타입 프로그래밍의 기초가 되는 유틸리티를 제작해봤습니다.
보통의 경우 이런 유틸은 암묵적으로 리스코프치환원칙을 위배하게 됩니다.
자식클래스를 부모클래스로 캐스팅하는 관계만 사용해야하는데 이런 유틸이 그런 행위를 선택적으로 하게 만들 수 있기 때문입니다.
하지만 합타입을 수시로 사용하는 타입스크립트의 경우 확정된 합타입의 서브클래스 런타임에 확인하는 용도로는 나쁘지 않습니다.
recent comment