타입스크립트의 클래스

타입스크립트에서의 클래스 알아보기

Intro


Class


만일 객체를 활용해 다른 부서의 데이터를 관리하고 각 객체에 메소드를 추가해 해당 부서 정보를 표출하려면 어떤 방법이 좋을까. 이때는 Class를 활용하는것이 좋을것이다. 클래스(부서 설계도)를 인스턴스화(실체화)해 객체(실체화된 부서 객체)로 만들고, 해당 부서 객체의 메소드로 해당 부서의 데이터를 표출할 수 있기 때문이다.

Class 알아보기


다음과 같이 관례상 클래스임을 명시하기 위해 첫 문자는 대문자로 입력한다. ▶Department
클래스는 일종의 틀이며 이를 실체화하는 것을 인스턴스화 실체를 객체라고 이해하면된다.

ts
// app.ts

class Department {
    name: string; // 필드의 타입을 정의한다. 키-값이 아닌, 키를 정의한다.

    // 인스턴스화가 진행될때 하나의 string타입을 인자로 받아 호출되는 생성자 구문
    constructor(n: string) {
        this.name = n;
    }
}

const accounting = new Department('Accounting');
console.log(accounting);
> ▶ Department {name: "Accounting"} // 키-값 쌍을 가진 객체를 출력한다.

위 소스를 처음본다면 객체네? 라고 생각할 수 있을것이다. 하지만 객체가 아니며 클래스의 필드의 타입을 명시한다. 키-값쌍이 아닌 이름과 값의 타입을 정의할 뿐이다.

컴파일 후 자바스크립트


Class 알아보기단계의 컴파일된 js코드를 보자. 참고로 컴파일이 진행된 자바스크립트 버전은 es6이다.

js
// app.js

"use strict";
class Department {
    constructor(n) {
        this.name = n;
    }
}
const accounting = new Department('Accounting');
console.log(accounting);
//# sourceMappingURL=app.js.map

코드는 비슷하지만 Department 클래스에 field가 존재하지 않는다. es6에서는 지원되지 않기 때문이다. 생성자 로직이 있지만 객체의 속성을 바닐라 자바스크립트로 추가할 뿐이다. es5로 변경한다면 해당 구문이 많이 뒤바뀌게 될 것이다. 이처럼 타입스크립트는 강력한 컴파일 기능을 갖춘것을 볼 수 있다.

생성자(constructor) 및 this 키워드


실체화된 즉, 인스턴스화된 객체에서 호출할 수 있는 몇 가지 함수나 메소드를 추가해보자.

ts
// app.ts

class Department {
    name: string; // 필드의 타입을 정의한다. 키-값이 아닌, 키를 정의한다.

    // 인스턴스화가 진행될때 하나의 string타입을 인자로 받아 호출되는 생성자 구문
    constructor(n: string) {
        this.name = n;
    }

    // 메소드 이름 다음 쌍점이나 등호 대신 괄호 () 를 입력한다. 괄호에는 매개변수를 넣을 수 있다.
    describe() {
        console.log('Department: ' + this.name);
    }
}

const accounting = new Department('Accounting');
accounting.describe();
>> Department: Accounting

13번 라인에서 this는, 보통 클래스의 필드를 가리킨다.

만일 다음과 같이 accountingCopy객체를 만들고 describe키에 accounting.describe를 정의하면 어떻게 될까.

ts
// app.ts

// 사실상 describe 함수 자체를 전달하기 때문에 this는 가리키지 않는다.
const accountingCopy = { describe: accounting.describe };
// 만일 accountingCopy 객체내에 { name: 'Lim' describe: accounting.describe }; 과 같이 입력한다면 정상적으로 출력될 것이다.

// accountingCopy 를 참조하는데, this.name 이라는 필드가 존재하지 않기 때문에 undefined를 출력한다.
accountingCopy.describe();
>> Department: undefined

하지만 타입스크립트에서 이는 에러로 판단하지 않는다. 이를 해결하려면 다음과 같이 메소드에 참조할 대상을 정의해야한다.

ts
// app.ts

class Department {
    name: string;.

    constructor(n: string) {
        this.name = n;
    }

    // Department 클래스에 기반한 인스턴스를 참조해야한다.
    describe(this : Department) {
        console.log('Department: ' + this.name);
    }
}

const accounting = new Department('Accounting');

const accountingCopy = { describe: accounting.describe };
// Department 클래스에 기반한 인스턴스를 참조하지않으므로 에러가 발생한다.
accountingCopy.describe();

개인 및 공용 엑세스 수정자


직원필드를 정의한 후 직원을 추가할 수 있도록 코드를 수정해보자.

ts
// app.ts

class Department {
    name: string;
    employees = string[] = [];

    constructor(n: string) {
        this.name = n;
    }

    // Department 클래스에 기반한 인스턴스를 참조해야한다.
    describe(this : Department) {
        console.log('Department: ' + this.name);
    }

    // 직원 추가 메서드
    addEmployee(employee: string){
        this.employees.push(employee);
    }

    // 직원 수 및 직원 출력 메서드
    printEmployeeInformation() {
        console.log(this.employees.length);
        console.log(this,employees);
    }
}

const accounting = new Department('Accounting');

accounting.addEmployee('Max');
accounting.addEmployee('Menu');

accounting.printEmployeeInformation();
>> 2
>> (2) ["Max", "Manu"]

클래스내 addEmployee() 메서드를 포함하여 나중에 객체로 생성한 경우 해당 메서드로 직원을 추가할 수 있다. 하지만 다음과 같은 문제도 있다. 필드에 직접 접근하여 값을 변경하는 경우다.

ts
accounting.employees[2] = 'Anna';

accounting.printEmployeeInformation();
>> 3
>> (2) ["Max", "Manu", "Anna"]

이와 같은 경우는 사용하지 못하도록 변경해야한다. 작업중 다른 동료간 추가하는 방식이 다르다면 문제가 발생할 수 있다. 클래스 외부에서 필드에 접근하지 못하도록 private 키워드를 추가해야 한다. 이와 같은 키워드를 접근 제어자라고 한다. JAVA를 사용해봤다면 익숙할 것이다. 필드 접근 제어자의 기본값은 public이다.

이는 자바스크립트는 없지만 타입스크립트에서 추가되었다. 컴파일시 구문이 변경될 것이다.

ts
class Department {
    name: string;
    // employees.  객체 내부에서만 사용가능하다.
    private employees = string[] = [];
}
const accounting = new Department('Accounting');

accounting.employees[2] = 'Anna'; // 에러 발생

약식의 초기화


클래스 사용시 인스턴스화 과정에서 보통 생성자에서 모든 필드를 초기화해 사용한다. 이때 아래처럼 필드를 추가하고 생성자에서 초기화하는 방식이 아닌, 생성자에서 초기화와 동시에 필드를 생성할 수 있다.

ts
// app.ts
class Department {
    // private id: string;
    // public name: string;
    private employees: string[] = [];

    constructor(private id: string, public name: string) {
        // private id = 인스턴스화 과정에서 전달된 매개변수 값;
        // public name = 인스턴스화 과정에서 전달된 매개변수 값;
    }

    // Department 클래스에 기반한 인스턴스를 참조해야한다.
    describe(this : Department) {
        console.log(`Department (${this.id}): ${this.name}`);
    }
}

const accounting = new Department('d1', 'Accounting');

accounting.describe();
>> Department (d1): Accounting

인스턴스화 과정에서 매개변수를 지정하는데, 이 매개변수 값이 생성된 필드가 저장된다.

읽기 전용 속성


private, public 이면 안되고, 초기화 후 변경 되어서도 안 되는 특정 필드의 경우 접근 제어자뒤에 readonly를 입력한다.
private, public, readonly 는 타입스크립트에서 추가된 기능이다.

ts
// app.ts
class Department {
    // private readonly id: string;
    // public name: string;
    private employees: string[] = [];

    constructor(private readonly id: string, public name: string) {

    }

    // Department 클래스에 기반한 인스턴스를 참조해야한다.
    describe(this : Department) {
        console.log(`Department (${this.id}): ${this.name}`);
    }

    addEmployee(employee: string){
        this.id = 'd2'; // 에러 발생!
        this.employees.push(employee);
    }
}

더욱 안정적인 협업이 가능하도록 도와준다.

Class 활용하기


보다 실무적으로 활용할 방법들을 보자.

상속


부서 클래스에서 보다 특정 유형 부서에 대한 정의도 필요할 것이다. 구문을 보자. 기존의 Department는 바뀐 점이 없다.

ts
// app ts
class Department {
    private employees: string[] = [];

    constructor(private readonly id: string, public name: string) {

    }

    describe(this : Department) {
        console.log(`Department (${this.id}): ${this.name}`);
    }

    addEmployee(employee: string){
        this.employees.push(employee);
    }
}

이때 상속을 이용하면 효율적으로 정의할 수 있다. extends키워드 다음 클래스를 상속받으며 여러 클래스 상속은 불가능하다.

ts
// app.ts
class ITDepartment extends Department {
    // 기본 생성자가 없는 경우 부모 생성자를 포함한다
    // 기존의 Department가 가진 모든 것을 상속받는다.
}

const accounting = new ITDepartment('d1', 'Accounting')

accounting.describe();
>> Department (d1): Accounting

기본 생성자를 생성하여 ITDepartment 호출시 초기화 하는 블록을 생성해보자. 기존에 id부서 필드의 초기화는 부모 생성자를 호출한다.
이때 사용할 수 있는 키워드는 super이다. 이 키워드는 부모를 가리키며 super()는 부모의 기본생성자를 가리킨다. 또한 ITDepartment만 사용할 임원 필드를 추가해보도록 하자.

ts
// app.ts
class ITDepartment extends Department {
    admins: string[];

    constructor(id: string, admins: string[]){
        super(id, 'IT'); // 부모의 생성자를 호출한다.
        this.admins = admins; // 하위 클래스에서 this를 사용하려면 super 먼저 호출해야한다.
    }
}

const it = new ITDepartment('d1', ['Max'])

it.addEmployee('Max');
it.addEmployee('Manu');

it.describe();
it.name = 'NEW NAME';
it.printEmployeeInformation();

console.log(it);

>> ITDepartment {id: "d1", name: "NEW NAME", employees: Array(2), admins: Array(1)}
▶ admins: ["MAX"]
▶ employees: (2) ["MAX", "MANU"]
   id: "d1"
   name: "NEW NAME"

아래와 같이 추가 메서드도 생성할 수 있다.

ts
// app.ts

class AccountingDepartment extends Department {
    constructor(id: string, private reports: string[]){
        super(id, 'Accounting');
    }

    addReport(text: string){
        this.reports.push(text);
    }

    printReports() {
        console.log(this.reports);
    }
}

상위 클래스의 속성 및 메서드 재정의


상속받은 클래스의 속성 및 메서드를 재정의할 수 있다. 단, private로 선언된 필드는 수정할 수 없다. 상속받은 하위 클래스에서 접근이 가능하도록 변경하려면 private가 아닌 protected로 선언하여 제어한다.

ts
// app.ts
class Department {
    // private employees: string[]; Department 클래스에서만 사용 가능
    protected employees: string[]; // 이를 상속받은 클래스에서 사용 가능
}

class AccountingDepartment extends Department {
    constructor(id: string, private reports: string[]){
        super(id, 'Accounting');
    }

    // Department 상위 클래스의 메서드 재정의
    addEmployee(name: string){
        if(name === 'Max'){
            return;
        }
        this.employees.push(name);
    }

    addReport(text: string){
        this.reports.push(text);
    }

    printReports() {
        console.log(this.reports);
    }
}

Getter와 Setter

Getter는 필드의 값을 가져오며 get키워드를 사용한다.
Setter는 필드의 값을 지정하며 set키워드를 사용한다. 또한 인자가 필요하다.
외부에서 사용시 이 두 기능을 메서드처럼 사용하지 않고 필드와 같이 접근해야한다.

ts
// app.ts

class AccountingDepartment extends Department {
    private lastReport: string;

    get mostRecentReport() {
        if(this.lastReport){
            return this.lastReport;
        }
        throw Error('No report found');
    }

    set mostRecentReport(value: string) {
        if(!value){
            throw new Error('Please pass in a valid value')
        }
        this.addReport(value);
    }

    addReport(text: string){
        this.reports.push(text);
    }
}

const accounting = new AccountingDepartment('d1', ['Max'])
// accounting.mostRecentReport('Your End Report'); X
accounting.mostRecentReport = 'Your End Report';

// 속성을 참조하듯이 ()는 뒤에 입력하지 않는다.
console.log(accounting.mostRecentReport);
>> ['Your End Report']

정적 메서드와 속성


Math메서드의 경우 인스턴스화 하지 않고 바로 접근이 가능한데, static으로 설정되어있기 때문이다. 같은 곳의 메모리 주소만을 참조하기 때문에
인스턴스화 하지 않고 접근할 수 있다.

ts
// app.ts
class Department {
    // private employees: string[]; Department 클래스에서만 사용 가능
    protected employees: string[]; // 이를 상속받은 클래스에서 사용 
    
    static createEmployee(name: string) {
        return {name: name}
    }

}

// 인스턴스화하지 않고 클래스에 있는 정적 메소드에 직접 진입할 수 있다.
const employee1 = Department.createEmployee('Hex');
console.log(employee1)
>> {name: "Max"}

정적 메소드가 아닌 정적인 필드 추가도 가능하다. 단 같은 클래스내에서 접근은 불가능하다.
this는 클래스를 기반으로, 생성된 인스턴스를 참조하기 때문이다. 정적 속성은 인스턴스에 유효하지 않다. 정적 속성, 메서드는 인스턴스와 분리되어 있다.

ts
// app.ts
class Department {
    static fiscalYear = 2022;
    // private employees: string[]; Department 클래스에서만 사용 가능
    protected employees: string[]; // 이를 상속받은 클래스에서 사용 
    
    constructor(private readonly id: string, public name: string){
        console.log(this.fiscalYear); // Error! Property 'fiscalYear' is a static member of type
        console.log(Department.fiscalYear); // 2022
    }

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

}

// 인스턴스화하지 않고 클래스에 있는 정적 메소드에 직접 진입할 수 있다.
const employee1 = Department.createEmployee('Hex');
console.log(employee1, Department.fiscalYear)
>> {name: "Max"} 2022

추상 클래스


클래스를 확장해감에 따라 하위 클래스들이 반드시 재정의하거나 구현해야할 메서드도 필요할것이다. 이러한 행동은 부모클래스에서 지정할 수 있다. 바로 abstract추상 클래스의 사용이다. 다음과 같이 Department클래스를 추상 클래스로 변경해보자.
하나의 추상 메서드라도 가지고 있는 경우 해당 클래스는 반드시 추상클래스로 정의해야하며. 추상 메서드는 선언한 클래스에서 구현할 수 없고 이 클래스를 상속 받은 하위 클래스에서 구현해야한다. 만일 하위 클래스에서 이를 구현하지 않은 경우 해당 추상 메서드를 구현하라는 에러가 발생한다.

또한 추상 클래스는 인스턴스화가 불가능하다. 기본적으로 상속되어야할 클래스이다.

ts
// app.ts
abstract class Department {

    // 추상 메서드는 반환할 타입, 매개변수의 타입을 지정해야한다.
    abstract describe(this: Department): void;
}

// 상속 받은 하위 클래스 
class ITDepartment extends Department {
    admins: string[];

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

    describe() {
        console.log('IT Department - ID: ', + this.id)
    }
}

싱글톤 및 private 생성자


private 생성자는 어떤 용도로 사용할까. 객체 지향 프로그래밍에는 싱글톤 패턴이 있는데, 특정 클래스의 인스턴스를 정확히 하나만 갖는 패턴이다. 회사에서 회계부서는 하나일 것이다. 이때 싱글톤 패턴에 적합할 것이다.

우선 회계 부서 클래스 생성자에 private키워드를 입력한다. 생성자는 해당 클래스에서만 사용가능하므로 이 클래스는 new 생성자 사용이 즉 인스턴스화가 불가능하다.

ts
// app.ts
class AccountingDepartment extends Department {
    private lastReport: string;
    private static instance: AccountingDepartment;

    get mostRecentReport() {
        if(this.lastReport){
            return this.lastReport;
        }
        throw Error('No report found');
    }

    set mostRecentReport(value: string) {
        if(!value){
            throw new Error('Please pass in a valid value')
        }
        this.addReport(value);
    }

    // private로 지정됐다면 이 클래스는 new 생성자 사용이 즉 인스턴스화가 불가능하다.
    private constructor(id: string, private reports: string[]) {
        super(id, 'Accounting');
        this.lastReport = reports[0];
    }

    static getInstance() {
        if(AccountingDepartment.instance) {
            return this.instance
        }

        this.instance = new AccountingDepartment('d2', []);
        return this.instance;
    }

    addReport(text: string){
        this.reports.push(text);
    }
}

const accounting = AccountingDepartment.getInstance();
const accounting2 = AccountingDepartment.getInstance();

consolg.log(accounting, accounting2);

  • 21-24 우선 회계 부서 클래스 생성자에 private키워드를 입력한다. 생성자는 해당 클래스에서만 사용가능하므로 이 클래스는 new 생성자 사용이 즉. 인스턴스화가 불가능하다.

  • 4 인스턴스화한 객체를 저장할 정적 필드를 생성한다. 타입은 현재 클래스로 설정한다. 이 클래스 내에서만 접근가능하도록 private접근 제어자를 사용한다.

  • 26-33 클래스의 인스턴스화가 불가능하므로 접근을 위해 클래스내에 정적 메소드를 생성한다. 인스턴스가 있다면 반환하고 없다면 생성하여 반환한다.

  • 40-43 두 변수가 참조하는 인스턴스는 같은 객체를 출력한다. 즉 싱글턴 패턴은 클래스를 최초에 한 번만 메모리에 적재하여 사용한다.