믹스인(mixin)
JavaScript와 TypeScript는 trait이나 mixin 키워드를 제공하지 않지만 쉽게 직접 구현할 수 있습니다.
두 키워드 모두 둘 이상의 클래스를 상속받는 다중 상속(multiple inheritance)과 관련된 기능을 제공하며, 역할 지향 프로그래밍(role-oriented programming)을 제공합니다.
역할 지향 프로그래밍에선 "이것은 Shape이에요"라고 표현하는 대신 "측정할 수 있어요", "반지름을 가지고 있어요"처럼 속성을 묘사하는 방식을 사용합니다.
즉, "is-a" 관계 대한 'can', 'has-a' 관계를 사용합니다.
믹스인을 구현해 보겠습니다.
믹스인이란 동작과 프로퍼티를 클래스로 혼합(mix)할 수 있게 해주는 패턴으로, 다음 규칙을 따릅니다.
- 상태를 가질 수 있습니다.(예: 인스턴스 프로퍼티)
- 구체 메서드만 제공할 수 있습니다.(추상 메서드 X)
- 생성자를 가질 수 있습니다.(클래스가 혼합된 순서와 같은 순서로 호출됩니다.)
TypeScript는 믹스인을 내장 기능으로 제공하지 않지만 비교적 쉽게 구현할 수 있습니다.
예를 들어 TypeScript 클래스의 디버깅 라이브러리를 설계한다고 가정하고 이를 EZDebug라 부르도록 하겠습니다.
이 라이브러리를 이용해 라이브러리를 사용하는 모든 클래스의 정보를 출력해서 런타임 클래스를 검사할 수 있습니다.
이 라이브러리는 아래처럼 사용할 수 있습니다.
class User {
// ...
}
User.debug() // 'User({"id": 1, "name": "Cozy Linda"})'로 평가
사용자는 표준 .debug 인터페이스를 이용해 모든 것을 디버그 할 수 있습니다.
이제 실제로 만들어보겠습니다.
withEZDebug라는 믹스인을 이용해 이 기능을 구현합니다.
믹스인은 단순하게 클래스 생성자를 인수로 받아 클래스 생성자를 반환하는 함수이므로 withEZDebug는 아래처럼 구현할 수 있습니다.
type CalssConstructor = new (...args: any[]) => {} // ①
function withEZDebug<C extends CalssConstructor>(Class: C){ // ②
return class extends Class { // ③
constructor(...args: any[]){ // ④
super(...args) // ⑤
}
}
}
- 먼저 모든 생성자를 표현하는 ClassConstructor 타입을 선언합니다. TypeScript는 전적으로 구조를 기준으로 타입을 판단하므로(구조 기반 타입화) new로 만들 수 있는 모든 것을 생성자라고 규정합니다. 또한 생성자에 어떤 타입의 매개변수가 올지 알 수 없으므로 임의의 개수의 any 타입 인수를 받을 수 있게 지정했습니다.
- 한 개의 타입 매개변수 C만 받도록 withEZDebug 믹스인을 선언했습니다. extends로 강제했듯이 C는 최소한 클래스 생성자여야 합니다. withEZDebug의 반환 타입은 C와 새로운 익명 클래스의 교집합이며 TypeScript가 이를 추론하도록 했습니다.
- 믹스인은 생성자를 인수로 받아 생성자를 반환하는 함수이므로 익명 클래스 생성자를 반환했습니다.
- 이 생성자는 최소한 우리가 전달한 클래스가 받는 인수를 받을 수 있어야 합니다. 하지만 어떤 클래스를 전달할지 아직 알 수 없으므로 Class Constructor와 마찬가지로 임의의 개수의 any 타입을 받도록 구현했습니다.
- 마지막으로 이 익명 클래스는 다른 클래스를 상속받으므로 Class의 생성자를 호출해야 한다는 사실을 기억해야 합니다.
일반 JavaScript 클래스처럼 constructor에 아무런 로직이 없으면 4, 5의 코드를 생략할 수 있습니다.
withEZDebug 예에서도 생성자에 아무런 로직을 넣지 않을 것이므로 이를 생략하겠습니다.
필요한 코드를 준비했으니 디버깅이 실제 동작하도록 만들 차례입니다. .debug를 호출하면 클래스의 생성자명과 인스턴스 값을 출력해야 합니다.
type CalssConstructor = new (...args: any[]) => {}
function withEZDebug<C extends CalssConstructor>(Class: C){
return class extends Class {
debug() {
let Name = this.constructor.name
let value = this.getDebugValue()
return Name + '(' + JSON.stringify(value) + ')'
}
}
}
디버깅에 사용할 .getDebugValue 메서드를 반드시 구현하도록 강제하려면 제네릭 타입을 이용하면 됩니다.
type ClassConstructor<T> = new (...args: any[]) => T // ①
function withEZDebug<C extends ClassConstructor<{
getDebugValue(): object // ②
}>>(Class: C){
// ...
}
- ClassConstructor에 제네릭 타입 매개변수를 추가했습니다.
- 형태 타입 C를 ClassConstructor에 연결함으로써 withEZDebug로 전달한 생성자가 .getDebugValue 메서드를 정의하도록 강제했습니다.
위의 디버깅 라이브러리는 아래처럼 사용할 수 있습니다.
type ClassConstructor<T> = new (...args: any[]) => T // ①
function withEZDebug<C extends ClassConstructor<{
getDebugValue(): object // ②
}>>(Class: C){
// ...
}
class HardToDebugUser {
constructor(
private id: number,
private firstName: string,
private lastName: string
){}
getDebugValue() {
return {
id: this.id,
name: this.firstName + ' ' + this.lastName
}
}
}
let User = withEZDebug(HardToDebugUser)
let user = new User(1, 'Cozy', 'Linda')
user.debug()
필요한 수의 믹스인을 클래스에 제공함으로 더 풍부한 동작을 제공할 수 있으며 타입 안정성도 보장됩니다.
믹스인은 동작을 캡슐화할 뿐 아니라 동작을 재사용할 수 있도록 도와줍니다.
'👶 TypeScript' 카테고리의 다른 글
final 클래스 흉내 내기 (0) | 2023.01.17 |
---|---|
데코레이터(decorator) (0) | 2023.01.16 |
다형성 (0) | 2023.01.16 |
클래스는 값과 타입을 모두 선언한다 (0) | 2023.01.16 |
클래스는 구조 기반 타입을 지원한다 (0) | 2023.01.16 |