👶 TypeScript

믹스인(mixin)

개발자 린다씨 2023. 1. 16. 16:00
반응형

믹스인(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) // ⑤
        }
    }
}
  1. 먼저 모든 생성자를 표현하는 ClassConstructor 타입을 선언합니다. TypeScript는 전적으로 구조를 기준으로 타입을 판단하므로(구조 기반 타입화) new로 만들 수 있는 모든 것을 생성자라고 규정합니다. 또한 생성자에 어떤 타입의 매개변수가 올지 알 수 없으므로 임의의 개수의 any 타입 인수를 받을 수 있게 지정했습니다.
  2. 한 개의 타입 매개변수 C만 받도록  withEZDebug 믹스인을 선언했습니다. extends로 강제했듯이 C는 최소한 클래스 생성자여야 합니다. withEZDebug의 반환 타입은 C와 새로운 익명 클래스의 교집합이며 TypeScript가 이를 추론하도록 했습니다.
  3. 믹스인은 생성자를 인수로 받아 생성자를 반환하는 함수이므로 익명 클래스 생성자를 반환했습니다.
  4. 이 생성자는 최소한 우리가 전달한 클래스가 받는 인수를 받을 수 있어야 합니다. 하지만 어떤 클래스를 전달할지 아직 알 수 없으므로 Class Constructor와 마찬가지로 임의의 개수의 any 타입을 받도록 구현했습니다.
  5. 마지막으로 이 익명 클래스는 다른 클래스를 상속받으므로 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){
    // ...
}
  1. ClassConstructor에 제네릭 타입 매개변수를 추가했습니다.
  2. 형태 타입 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()

필요한 수의 믹스인을 클래스에 제공함으로 더 풍부한 동작을 제공할 수 있으며 타입 안정성도 보장됩니다.

 

믹스인은 동작을 캡슐화할 뿐 아니라 동작을 재사용할 수 있도록 도와줍니다.

반응형