티스토리 뷰

 

데코레이터(Decorator) 패턴, 혹시 한 번씩은 들어보시거나 기억나시나요? 해당 패턴은 기존 객체의 기능을 동적으로 추가하거나 덮는 식으로 전체적인 기능에 변화를 주거나 하는데 널리 사용되는 패턴 중 하나입니다.

 

더보기

해당 패턴에 대해 더 자세히 알고 싶다면 https://mttankkeo.tistory.com/19 참고해주세요.

 

앞서 데코레이터 패턴을 언급한 이유는 바로 여러분에게 소개할 믹스인과 그 사용 목적이 매우 유사하기 때문입니다.

 

물론 데코레이터 패턴과 믹스인 개념에 이미 익숙한 분들도 계시겠지만, 믹스인은 주로 단일 상속만을 지원하는 Dart나 Javascript 같은 언어에서 사용되는 개념입니다. 다중 상속을 지원하는 C++ 같은 언어에서는 믹스인의 필요성이 크지 않습니다. 또 한 가지 차이점은 믹스인은 주로 런타임에 확장되는 개념이 아니라, 구현(선언) 단계에서 확장을 목표로 한다는 것입니다. 다시 말해, 런타임에서의 유연성은 데코레이터 패턴이 좀 더 뛰어나다고 볼 수 있습니다.

 

물론 믹스인의 유연성이 낮다고 해서 무조건 단점만 있는 것은 아닙니다. 데코레이터 패턴은 여러 겹으로 객체를 감싸는 경우 코드가 복잡해지고 가독성이 떨어질 수 있는 반면, 믹스인은 마치 레고 블록을 조립하듯 작고 독립적인 코드 조각들을 조합하듯 사용할 수 있기 때문에 코드 구조가 비교적 간결하고 후임 개발자 또는 기여자에게 코드의 의도가 명확하게 전달될 수 있다는 장점이 있습니다. (물론 이 부분은 개인적인 프로그래밍 스타일이나 프로젝트 특성에 따라 다르게 느껴질 수 있습니다.)

 

기존에 믹스인의 존재를 몰랐던 분들은 어때요? 믹스인에 조금은 흥미가 돋나요? 잔말 말고 바로 타입스크립트에서 믹스인을 구현하는 방법을 알아보도록 하겠습니다.

// 해당 타입은 주어진 제네릭 클래스의 생성자 함수를 정의합니다.
type Constructor<T extends {}> = new (...args: any) => T;

먼저 믹스인을 구현하기 이전에 타입스크립트에서 생성자 함수의 타입을 어떻게 나타낼 수 있는지에 대한 방법을 알아야 합니다.

 

new 키워드는 클래스의 생성자(constructor)을 의미하고 인자들은 생성자의 인자들을 의미합니다, 반환하는 타입은 생성자가 뱉는 인스턴스의 타입을 의미합니다.

function Mixin(Base: Constructor<{}>) {
    return class Mixin extends Base {
        print() {
            console.log("hello world");
        }
    }
}

// 이러한 방식으로 선언하는 경우도 있습니다.
// function Mixin<T extends Constructor<{}>>(Base: T)

해당 코드의 경우 믹스인을 구현하는 예시들 중 하나입니다. Mixin 함수를 호출하면 주어진 생성자를 상속한 클래스를 반환하는 것을 볼 수 있습니다. 여기서 다른 언어를 사용하다 JavaScript를 접한 사람들이, 함수와 클래스가 모두 1급 객체로 다뤄진다는 사실에 놀라곤 합니다. 따라서 JavaScript에서는 함수와 클래스를 변수에 담거나, 다른 함수의 인자로 넘기거나, 함수의 반환값으로 사용할 수 있습니다.

 

그렇다면 특정 클래스만을 대상으로 확장하는 믹스인은 어떻게 구현할 수 있을까요?

class Example {
    say() {
        console.log("hello world 1")
    }
}

function ExampleMixin(Base: Constructor<Example>) {
    return class ExampleMixin extends Base {
        say() {
            super.say();
            console.log("hello world 2");
        }
    }
}

해당 코드에서는 Example 클래스를 대상으로 확장하는 ExampleMixin을 볼 수 있습니다, 이전 코드 예시와 달리 Example의 생성자만을 인자로 받고 있으며 이로 인해서 생성자 타입은 Example 그 자체이거나 하위 타입으로 제한된다는 사실을 알 수 있습니다.


이러한 생성자 인자의 타입 선언을 통해서 ExampleMixin은 Example 클래스의 기존 인터페이스에 접근하고 수정할 수 있는 권한을 갖게 됩니다.

 

이번엔 해당 믹스인을 통해서 어떻게 클래스를 확장할 수 있는지 알아보도록 하겠습니다.

class ExpandedExample extends ExampleMixin(Example) {
    ...
}

해당 코드처럼 기존 클래스 상속과 유사한 방식으로 ExampleMixin을 사용할 수 있습니다. 즉, 확장하고자 하는 클래스를 ExampleMixin 함수로 감싸기만 하면 손쉽게 클래스를 확장할 수 있습니다.

const ExpandedMixin = ExampleMixin(Example);

물론 위와 같이 ExampleMixin(Example)을 새로운 변수(또는 상수)에 할당할 수도 있지만, 이 경우에는 활용도가 제한적이기 때문에 일반적으로는 저는 첫 번째 방식을 선호합니다.

 

솔직히 개인적으로 믹스인은 기존 클래스 확장 방식보다 유연하고 강력한 면모를 보여주는 기능이라고 생각합니다, 기존 단일 상속의 문제점을 쉽게 극복할 수 있으니까요.

 

특히 TypeScript의 제네릭과 결합하면 타입 안정성을 유지하면서도 다양한 클래스에 적용 가능한 확장 클래스를 마구 만들 수 있습니다. 앞으로 코드를 작성할 때 믹스인을 활용하여 더욱 유연하고 효율적인 코드를 작성해 보세요!

 

Happy coding in the another world!