Front-End/TypeScript

타입스크립트[TypeScript] | 클래스 상속(extend), 추상 클래스(abstrct), 정적 메서드(static)

jaeyeong 2023. 4. 18. 11:50

🔍 글을 시작하기 전에

 

해당 글은 타입스크립트 프로그래밍 책과 유데미 타입스크립트 강의를 공부하여 정리한 글입니다.

수정해야 할 부분이 있다면 언제든지 알려주세요!

 

Class(클래스)

클래스는 같은 구조로 이루어진 객체를 쉽게 생성할 수 있도록 해준다.

즉, 클래스를 사용하면 동일한 구조, 동일한 클래스를 기반으로 하는 동일한 메서드로 여러 객체를 빠르게 복제할 수 있는 것이 장점이다.

 

객체클래스의 인스턴스이며 클래스는 객체의 형태, 포함해야 할 속성과 메소드를 정의하는 데 도움을 준다.
따라서 클래스는 객체의 생성 속도를 높여주는, 객체 리터럴 표기법을 사용하는 것에 대한 대안 이다.

타입스크립트에서 클래스를 정의하는 방법은 다음과 같다.

  1. 클래스 내에서 사용할 변수(클래스의 속성)를 상단에 타입과 함께 정의한다. 이때 변수를 필드라고 부른다.
  2. 이 때 외부에서 수정되지 않아야 할 속성은 필드 앞에 private를 붙여준다.
    따로 붙이지 않으면 자동으로 public 속성이 부여된다.
  3. 클래스를 사용해 객체를 생성할 때 전달받은 인자로 초기 구성을 하려면 constructor 메서드를 사용한다.
class Department {
	public name: string; // public은 생략 가능
	private employees: string[] = [];
	
	constructor(n: string) {
            this.name = n;
	}
}

const accounting = new Department('Accounting');
console.log(accounting); // {name: 'Accounting'}

 

위 코드는 필드가 하나여서 복잡해 보이지 않지만, 만약 name, age, address 등등 필드가 많아지면

constructor에서도 값을 할당해줘야 하고, 상단에 필드 정의도 해야 해서 복잡해질 수 있다.

이럴 경우에는 아래 코드처럼 constructor 인자에 public, private 속성과 타입 정의를 해주면 된다.

class Department {
	private employees: string[] = [];
	
	constructor(public name: string, private id: string) {}

	printDepartmentInfo() {
            console.log(this.name, this.id);
	}
}

const accounting = new Department('Accounting', 'p1');
accounting.printDepartmentInfo(); // Accounting p1


위에 printDepartmentInfo 함수에서 this를 사용하는데, 만약 메서드를 복사할 경우엔 어떻게 될까?

class Department {
	private employees: string[] = [];
	
	constructor(public name: string, private id: string) {}

	printDepartmentInfo() {
            console.log(this.name, this.id);
	}
}

const accounting = new Department('Accounting', 'p1');
const copy = { print: accounting.printDepartmentInfo };
copy.print(); // Uncaught TypeError: Cannot read properties of undefined


copy에는 name, id 속성이 없기 때문에 당연히 실패한다.
(컴파일 시에 오류가 발생하진 않고 런타임에 오류가 발생하게 된다.)
다음과 같이 수정하면 값이 잘 출력된다.

class Department {
	private employees: string[] = [];
	
	constructor(public name: string, private id: string) {}

	printDepartmentInfo() {
            console.log(this.name, this.id);
	}
}

const accounting = new Department('Accounting', 'p1');
const copy = { name:'jaeyeong', id:'p2', print: accounting.printDepartmentInfo };
copy.print(); // jaeyeong p2


이런 런타임 오류를 방지하기 위해서 this에 타입스크립트를 적용하는 방법은 다음과 같다.

class Department {
	private employees: string[] = [];
	
	constructor(public name: string, private id: string) {}

	printDepartmentInfo(this: Department) {
            console.log(this.name, this.id);
	}
}

const accounting = new Department('Accounting', 'p1');
const copy = { print: accounting.printDepartmentInfo };
copy.print(); // Cannot assign context 'this' of type print to method Department' type 'this'.

 

printDepartmentInfo 함수에 thisthis의 타입을 함께 작성하였다.

이렇게 this의 타입을 전달하면 컴파일 단계에서 오류를 잡아낼 수 있기 때문에 this를 사용할 땐 타입을 명시해 주는 것이 좋다.

 

클래스 상속 (extends)

상속이란 미리 만들어둔 클래스를 확장시켜 다른 클래스를 만드는 것이다.
미리 만들어둔 클래스의 속성 외에 따로 필요한 필드가 있다면 만들어둔 클래스의 속성을 복사하여 새로운 클래스를 만들고,

추가할 필드를 작성한다.

기본 클래스를 설정한 다음 이를 상속하는 특수화된 클래스를 생성함으로써

일부 공통된 기능, 속성을 사용하고 특수화된 구조로 이루어진 클래스를 만들 수 있다.

위에서 정의한 클래스인 Department를 상속한다면 다음과 같다.

class ITDepartment extends Department {
    admins: string[] = [];
    constructor(id: string, admins: string[]) {
        super(id, 'IT');
        this.admins = admins;
    }
}


이때 가장 중요한 건 super()
확장하기 위해 가져온 원본 클래스(부모 클래스)의 constructor 메서드를 실행시키는 요소가 바로 super()이다
super()를 이용해서 부모 클래스에서 가져온 기본 정보들을 생성한 다음 작업을 하면 된다.

상속받을 때 부모 클래스에서 private로 작성된 필드가 있다면, 내용을 수정할 수 없다.
만약 외부로부터 보호하고 싶은데 확장하는 모든 클래스에서는 접근할 수 있도록 설정하고 싶다면

private대신 protected를 사용하면 된다.

 

private 사용

class Department {
    private employees: string[] = [];
	
    constructor(public name: string, private id: string) {}

    printDepartmentInfo(this: Department) {
        console.log(this.name, this.id);
    }
}

class AccountingDepartment extends Department {
    constructor(id: string, private reports: string[]) {
        super(id, 'Accounting');
    }
	
    addEmployee(name: string) {
        if(name === 'Jae') return;
        this.employees.push(name); // Error: 'employees' 속성은 private이며'Department' 클래스 내에서만 액세스할 수 있습니다.
    }
	
    addReports(text: string) {
        this.reports.push(text);
    }
	
    printReports() {
        console.log(this.reports);
    }
}

 

protected 사용

class Department {
    protected employees: string[] = [];
	
    constructor(public name: string, private id: string) {}

    printDepartmentInfo(this: Department) {
        console.log(this.name, this.id);
    }
}

class AccountingDepartment extends Department {
    constructor(id: string, private reports: string[]) {
        super(id, 'Accounting');
    }
	
    addEmployee(name: string) {
        if(name === 'Jae') return;
        this.employees.push(name);
    }
	
    addReports(text: string) {
        this.reports.push(text);
    }
	
    printReports() {
        console.log(this.reports);
    }
}


정의한 addEmployee는 부모 클래스와 동일한 메서드이다.
자식 클래스에서도 정의한다면 오버라이딩되어 부모 클래스에 있는 메서드가 무시되고 다시 정의된다.

 

추상 클래스

클래스를 확장하면서, 부모 클래스에서 정의한 메서드를 자식 클래스가 오버라이딩하여 새로 정의할 수 있다.
이 경우에는 기존 부모 클래스에 있던 메서드를 재정의 하는 것인데,

자식 클래스에 정의 단계를 위임(?)하는 방식으로도 메서드를 정의할 수 있다.

추상 클래스는 인스턴스화를 할 수 없고 무조건 extends(확장)되어야 한다.
(직접 자식 클래스에서 메서드를 정의해야 하니깐)

부모 클래스를 추상 클래스로 만들고(abstract 붙이기), 자식 클래스에서 구현하도록 위임하고 싶은 메서드를 void 형태로 정의하는 것이다.

abstract class Department {
    protected employees: string[] = [];
    constructor(public name: string, protected id: string) {}
    abstract describe(): void;
}

class ITDepartment extends Department {
    constructor() {
        super('IT', 'it_1');
    }
	
    describe() {
        console.log(`${this.name}'s ID: ${this.id}`);
    }
}


위 코드에서 만약 describe를 정의하지 않으면 오류가 발생한다.
추상 클래스의 abstract 메서드는 꼭 자식 클래스에서 정의해야 한다.
이렇게 확장된 클래스에서 각각 정의했으면 하는 메서드는 추상 메서드로 만들면 된다.

 

static 속성 (정적 메서드 만들기)

지금까지는 new를 통해 인스턴스를 만들고, 인스턴스를 통해 메서드나 필드에 접근했다.


우리는 보통 자바스크립트에서 Math API에 접근할 때 new Math를 하는 것이 아니라,

Math.pow() 이런 식으로 바로 점으로 접근한다.


이렇게 만들기 위해서는 우리가 생성한 클래스의 메서드나 필드를 정적(static)으로 만들면 된다.

class ITDepartment extends Department {
    static year = 2023;
	
    admins: string[] = [];
	
    constructor(id: string, admins: string[]) {
        super(id, 'IT');
        this.admins = admins;
    }

    static createEmployee(name: string) {
        return {name}
    }
}

const year = ITDepartment.year;
console.log(year); // 2023

const employee = Department.createEmployee('JaeYeong');
console.log(employee); // {name: 'JaeYeong'}


정적 메서드와 정적 속성은 인스턴스와 분리되어 있기 때문에 constructor나 다른 곳에서 this로 참조할 수 없다.
내부에서 this.year로 사용하는 것이 아니라, ITDepartment.year로 접근해야 한다.

싱글톤 클래스는 “new”로 생성하지 않고 메서드를 호출하여 구성하므로 특정 시점에 반드시 단 하나의 클래스 인스턴스가 존재한다.