엄격한 단일 상속
Mixin에 앞서서 class의 엄격한 단일 상속에 대하여 알아둬야 합니다.
(클래스가 수행할 수 없는 상속의 형태를 알게 되면, 그에 맞는 함수를 구상하는 데 도움이 됩니다.)
클래스
Typescript 그리고 Javascript 클래스는 엄격하게 단일 상속만 지원합니다. 그래서 아래와 같이 할 수 없습니다.
class User extends Tagged, Timestamped {// 🚨오류 : 다중 상속 불가
}

interface와 class를 헷갈리지 말자.
interface는 multiple extends가 가능합니다.
아래 코드에서 interface IUser는 Tagged와 Timestamped를 만족시켜야 합니다.
class User는 IUser를 만족시켜야 합니다.(implements)
class Tagged {
constructor(public tag: string) {}
}
class Timestamped {
constructor(public timestamp: Date) {}
}
interface IUser extends Tagged, Timestamped {} //
class User implements IUser {
constructor(public name: string, public tag: string, public timestamp: Date) {}
}
Mixin 개요
재사용 가능한 구성요소로 클래스를 구성하는 방법
재사용 가능한 구성요소로 클래스를 구성하는 다른 방법은 **믹스인(mixin)**이라 부르는 간단한 부분 클래스들로 구성/조립하는 것입니다.
Mixin 개념
믹스인은 다음과 같은 함수입니다.
- 생성자(constructor)를 받음.
- 생성자 확장하여 새 기능을 추가한 클래스 생성
- 새 클래스 반환
Mixin 사용 의의 ⭐
Mixin은 유연하고 분리된 방식으로 class간에 행동을 공유할 수 있는 강력한 방법입니다.
상속의 필요성을 피하면서 여러 소스의 동작을 혼합하고 일치시키는 데 사용할 수 있습니다.
재사용 가능한 구성요소로 클래스를 만드는 첫 번째 방법
전체 코드
// typescript Mixin
// https://typescript-kr.github.io/pages/mixins.html
// 소개(introduction)
// 전통적인 객체지향 계층과 함께, 재사용 가능한 컴포넌트로 부터 클래스를 빌드하는 또 다른 일반적인 방법으로, 간단한 부분클래스를 결합하여 빌드하는 것입니다.
// 예시 코드(Code Sample)
// Disposable Mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}
}
// Activatable Mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
class SmartObject {
constructor() {
setInterval(() => console.log(this.isActive + ' : ' + this.isDisposed), 500);
}
interact() {
this.activate();
}
}
interface SmartObject extends Disposable, Activatable {}
applyMixins(SmartObject, [Disposable, Activatable]);
let smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);
type Constructor<T = {}> = new (...args: any[]) => T;
////////////////////////////////////////
// In your runtime library somewhere
////////////////////////////////////////
function applyMixins(derivedCtor: Constructor, baseCtors: Constructor[]) {
baseCtors.forEach(baseCtor => {
// 첫 번째 loop 때 Disposable 사용.
Object.getOwnPropertyNames(baseCtor.prototype) // 첫 번째 loop 때 ---> [isDisposed, dispose]
.forEach(name => {
// SmartObject.prototype에 isDisposed, dispose를 Descriptor까지 완벽하게 추가한다.
Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name)!);
});
});
}
Mixin의 결합을 처리할 클래스
// Disposable Mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}
}
// Activatable Mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
class SmartObject {
constructor() {
setInterval(() => console.log(this.isActive + ' : ' + this.isDisposed), 500);
}
interact() {
this.activate();
}
}
interface SmartObject extends Disposable, Activatable {}
첫 번째 사항은 SmartObject 클래스에서 Disposable과 Activatable을 확장하는 대신 SmartObject인터페이스에서 확장한다는 것입니다. Declaration merging으로 인해 SmartObject 인터페이스가 SmartObject 클래스에 혼합됩니다.
클래스를 인터페이스로 취급하고 Disposable 및 Activatable 뒤에 있는 유형만 구현이 아닌 SmartObject 유형으로 혼합합니다. 이것은 우리가 클래스 구현에서 mixin을 제공해야 한다는 것을 의미합니다. (그 외에는 mixin의 사용을 피할 수 있습니다.)
따라서 아래와 같이 클래스 SmartObject에서 메서드가 자동 추천됨을 볼 수 있습니다.

하지만 이는 런타임에서 에러를 발생시킬 것입니다. 왜냐하면 실제로는 아직 SmartObject가 해당 메서드들을 갖고 있는 상태가 아니기 때문입니다.
인터페이스가 Declaration merging으로 사기를 치고 있다고 생각하시면 됩니다.🤣
그러므로 마지막으로 클래스 구현에서 실제로 mixin을 혼합(mix)]해야 합니다.
applyMixins(SmartObject, [Disposable, Activatable]);
마지막으로, 우리를 위해 mixin을 수행 할 도우미 함수를 만듭니다. 그러면 각 mixin의 속성이 실행되고 mixin의 대상으로 복사되어 독립형 속성을 구현합니다.
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name));
});
});
}
재사용 가능한 구성요소로 클래스를 만드는 두 번째 방법
첫 번째는 interface(사실상 위 코드에서는 인터페이스의 Declaration merging으로 가짜 클래스 역할을 수행했습니다. SmartObject는 헬퍼 함수 없이는 실제로는 비어있음.)에 이어서 실제로 런타임에 실행될 클래스 구현을 위한 혼합 함수(헬퍼 함수)를 만들어야 했기에 곧바로 이해하기는 어려웠습니다.
그러나 두 번째 방법은 첫 번째보다 훨씬 이해하기 쉽습니다. 어떤 식으로 확장이 되는지 눈에 바로 보일 것입니다…
// typescript Mixin
// https://radlohead.gitbook.io/typescript-deep-dive/type-system/mixins
// 모든 믹스인에 필요
type Constructor<T = {}> = new (...args: any[]) => T;
////////////////////
// 예제 믹스인
////////////////////
// 속성을 추가하는 믹스인
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
timestamp = Date.now();
};
}
// 속성과 메소드를 추가하는 믹스인
function Activatable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
isActivated = false;
activate() {
this.isActivated = true;
}
deactivate() {
this.isActivated = false;
}
};
}
////////////////////
// 클래스를 조립하는 예제
////////////////////
// 간단한 클래스(Root Base)
class User {
name = '';
}
// Timestamped 적용된 User
const TimestampedUser = Timestamped(User);
// Timestamped와 Activatable이 적용된 User
const TimestampedActivatableUser = Timestamped(Activatable(User));
////////////////////
// 조립된 클래스 사용
////////////////////
const timestampedUserExample = new TimestampedUser();
console.log(timestampedUserExample.timestamp);
console.log(timestampedUserExample.name);
const timestampedActivatableUserExample = new TimestampedActivatableUser();
console.log(timestampedActivatableUserExample.timestamp);
console.log(timestampedActivatableUserExample.isActivated);
console.log(timestampedActivatableUserExample.name);
export {};
두 방법 중 무엇을 사용할까?🤔❓
첫 번째 방식을 사용하게 되면 Mixin클래스 자체를 따로 분리해서 사용할 수도 있습니다. 반면 두 번째 방식은 항상 어떠한 (Root)Base가 되는 클래스를 받아야 합니다.
물론 이게 의도한 것이라면 두 번째 방식을 사용해도 좋습니다.
여기서 옳고 그른 방식은 없습니다. 취향에 맞게 사용하면 됩니다.
레퍼런스
- https://typescript-kr.github.io/pages/mixins.html
- https://radlohead.gitbook.io/typescript-deep-dive/type-system/mixins
이 글은 옵시디언(Obsidian)에서 작성되었습니다. 티스토리에서 상대경로 링크는 작동하지 않습니다. 큰 이미지 파일은 업로드되지 않을 수 있습니다.