TypeScrip Class 중

#TypeScript Class

목차

  1. Introduction

  2. Classes -> 생략

  3. Inheritance

  4. Public, private, and protected modifiers

  5. Readonly modifier

  6. Accessor

  7. Static properties

  8. Abstract Classes

  9. Advanced Tecknique

Introduction

Traditional js uses functions and prototype-based inheritance to build up resuable components, but this may feel a bit akward to programmers more comfortable with OOP approach, where classes inherit functionality and objects are built from these classes.
전통적인 js는 재 사용가능한 컴포넌트를 만들기 위해 prototype-base의 함수를 사용했습니다. 하지만 이 방식은 클래스가 기능을 상속받고 객체가 class로 만들어지는데 익숙한 OOP 지향의 프로그래머에게는 이상하게 느껴질 수도 있습니다.

Starting with ECMAScript 2015, also known as ECMAScript 6, js programming will be able to build their applications using this OOP approach.
ECMAScript6 또는 ECMAScript 2015 때부터 js 프로그래밍은 OOP 방식을 지원할 것입니다.

In ts, we allow developers to use these techniques now, and compile them down to js that works across all major browsers and platforms, without having to wait for the next version of js.
타입스크립트에서는 우리는 지금 여러분에게 OOP 기술을 사용할 수 있게 해주고 모든 플렛폼와 주요 브라우저에 js파일로 컴파일할 수 있게 할겁니다. 물론 다음 버전으로 js를 바꿀 필요 없이 말이죠.

Inheritance

타입스크립트에서는 우리는 OOP 패턴을 사용할 수 있습니다. class-based 프로그래밍의 가장 기본적인 패턴은 클래스에 다른 클래스를 상속받는 것입니다.

아래의 예제를 보세요.

class Animal {
    move(distanceInMeters: number = 0)
    {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    bark(){
        console.log('woof woof!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

이 예시는 가장 기초적인 상속 방법입니다. 클래스가 다른 클래스를 상속받아 base-class의 prop과 method를 사용이 가능해졌죠. 여기서 Dog는 Animal의 move method를 상속받기위해 extends 키워드를 사용하였습니다. 상속받는 클래스를 subclass라고 부르고 상속하는 클래스를 보통 superclass라고 부릅니다. 우리나라에서는 보통 super를 부모 클래스라고, sub을 자식 클래스라고 부릅니다.
Dog는 Animal로부터 move를 상속받았기 때문에 bark와 move 모두 사용이 가능한 인스턴스를 만들 수 있게 되었습니다.

밑의 예시를 더 보죠

class Animal {
    name: string;
    constructor(theName: string) {
        this.name = theName;
    }
    move(distanceInMeters: number = 0){
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name) }
    move(distanceInMeters = 5){
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name) }
    move(distanceInMeters = 45){
        console.log("히히힝");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse('Tommyh the Palomino');

sam.move();
tom.move(34);

위의 예제는 우리가 다루지 않았던 기능들을 다루고 있습니다.
다시 우리는 자식 클래스를 만들기 위해 extends 키워드를 사용하고 있습니다. Horse와 Snake가 Animal로부터 상속받고 있죠.

한가지 전의 예시와 다른점은, 각각의 상속받은 클래스들이 생성자에서 반드시 super()를 호출하여 base-class의 생성자를 실행시켜야 한다는 점입니다. 더욱이, 생성자 바디에 접근하기 전에 우리는 무조건 super()를 호출해야만 합니다. 이것은 타입스크립트가 강제하는 중요한 규칙입니다.

이 예시는 또한 어떻게 부모 클라스의 메서드를 자식클라스를 특수화 시키기 위해 override하는지 보여줍니다. 여기서 Snake와 Horse 둘은 move 메서드를 만드는데 이 메서드는 Animal의 move 메서드를 override 하고 있고, 각각의 클래스에 구체적인 기능을 부여하고 있습니다. 비록 tom이 Animal로 타입이 선언되어 있더라도, 그 값이 animal을 상속받은 Horse이기 때문에 tom.move(34)는 horse의 overriding된 method를 호출할 것입니다.

Public, private, and protected modifiers

Public by Default

지금까지의 예시들에서 우리는 선언한 클래스의 맴버들에 자유롭게 접근할 수 있었습니다. 다른언어를 잘 알고 있다면 깨달았겠지만, 위의 예시들에서 우리는 public 키워드를 쓰지 않았습니다. 다른언어의 예를 들자면 C# 같은 경우 반드시 public 키워드를 써주어야 한답니다. 타입스크립트에서는 각 클래스의 멤버들은 기본적으로 public 상태입니다.

하지만 public을 붙이고 싶다면 붙여도 됩니다. public을 사용하여 아래와 같은 코드로 작성할 수 있습니다.

class Animal {
    public name: string;
    public constructor(theName: string) {this.name = theName}
    public move (distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeteres}m.`); 
    }
}

Understanding private

private 키워드를 쓸경우 해당 멤버는 private이 선언되 클래스 밖에서는 사용할 수 없습니다.

예를 들자면

class Animal {
    private name: string;
    constructor(theName: string) {this.name = theName}
}

new Animal('cat').name; //Error: 'name' is private

타입스크립트는 구조적 타입 시스템입니다.
두개의 다른 타입을 비교할 때, 그게 어디서 왔건 모든 타입이 요구된 조건에 충족만 된다면 ts는 그 타입들이 동일하다고 판단합니다.

하지만 그 타입이 private이나 protected로 엮겨 있다면 우리는 이들을 다르게 취급합니다. 두 타입이 동일하게 간주되기 위해서는, 만약 하나의 타입이 private 멤버를 가지고 있다면 다른 하나 역시 동일한 선언 아래 동일한 private member를 가져야만하고, 다를경우 다른 타입으로 간주합니다. protected 멤버 역시 동일한 방식으로 타입을 검사합니다.

이해를 높이기 위해 아래 예제를 봐주세요.

class Animal {
    private name: string;
    constructor(theName: string) {this.name = theName}
}

class Rhino extends Animal {
    constructor() { super('Rhino'); }
}

class Employess {
    private name: string;
    constructor(theName: string) { this.name = theName};
}

let animal = new Animal('goat');
let rhino = new Rhino();
let employee = new Employee('Bob');

animal = rhino;
animal = employee; //error: `Animal` and `Employee` are not compatible

이 예제에서 우리는 Animal과 Rhino를 가지고 있고 Rhino는 Animal의 자식 클래스입니다. Employee클래스도 선언이 되었는데 형태만보자면 Animal과 동일합니다. 위의 클래스들로 인스턴스들을 만들었고 서로에게 할당해보고 어떤 일이 일어날지 지켜보았습니다. Animal과 Rhino는 동일한 private 멤버를 공유하고 있기 때문에 두 클래스로 만든 인스턴스들은 서로 할당이 가능합니다. 그러나 Employee의 겨우는 위와같이 않습니다. animal에 employee를 할당하려고 둘의 타입이 동일하지 않다고 error가 뜹니다. 비록 Employee가 private 의 동일한 id의 멤버를 가지고 있다고 하더라도, 이 멤버는 Animal에서 선언되지 않았기 때문에 error가 뜨게 됩니다.

Understanding protected

protected 는 private처럼 활동하지만 한가지 예외를 가지고 있는데, protected 맴버를 상속받은 클래스 내부에서 사용할 수 있다는 점입니다.

class Person {
    protected name: string;
    constructor(name: string) {this.name = name}
}

class Employee extends Person {
    private department: string;
    constructor(name: string, department: string) { 
        super(name);
        this.department = department;
    }
    public getElevatorPitch(){
        return `hello, my name is ${this.name} and I work in ${this.department}`;
    }
}

let howard = new Employee('Howard', 'sales');
console.log(howard.getElevatorPitch());
console.log(howard.name) //error

우리는 밖에서는 name을 쓸 수 없지만, Employee 안에서는 쓸수 있다는 점에 주목해 주세요. 이건 Employee가 Person을 상속받았기 때문입니다.
생성자에도 protected를 쓸 수가 있는데 이렇게 하면 생성자로 인스턴스를 만들수가 없습니다. 하지만 상속은 아직 가능합니다.

Class Person {
    protected name: string;
    protected constructor(theName: string) {this.name = name}
}

//Employee can extend Person
class Employee extends Person {
    private department: string;
    constructor(name: string, department: string) {
        super(name);
        this.department = department;   
    }

    public getElevatorPitch(){
        return `hello, my name is ${this.name} and I work in ${this.department}`;
    }

}

let howard = new Employee("Howard", "sales");
let john = new Person("John"); //Error : The 'Person' constructor is protected;

Readonly modifier

readonly 키워드로 prop을 만들수 있다. readonly prop은 반드시 처음 생성자로 인스턴스 생성할때 초기화를 시켜주어야 한다.

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor(theName: string) {
        this.name = theName;
    }

    let dad = new Octopus('Man with the 8 strong legs');
    dad.name = 'Man with 3 weak legs' //error name is readonly
}

Parameter properties

마지막 예제에서 우리는 반드시 readonly 멤버를 생성자로 인스턴스를 생성할때 인자값으로 the를 new Octopus()에 넘겨주어야한다. 그리면 즉시 name은 theName으로 바뀐다.
이것은 매우 흔하게 발생하는 일이다. Parameter properties 는 멤버를 한곳에서 초기화되게 해준다.

더 나은 parameter property의 Octopus classs의에서의 사용법이 있다. 아래 예제를 보자

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {}
}

어떻게 theName이 한꺼번에 없앴는지 그리고 어떻게 생성자에 readonly name: string으로 줄였는지 주목해 주십시요.
선언과 할당을 한곳에서 합쳐버렸습니다.

Parameter properties 는 readonly나 다른 접근 가능한 modifier를 접두해서 선언할 수 있습니다.
private 멤버를 parameter properties로 선언하고 초기화 할 수 있듯이 public이나 protected, readonly도 마찬가지로 사용가능합니다.

Accessors

타입스크립트는 getter와 setter를 통해 object 멤버에 접근하는 것을 간섭합니다. 이것은 정교하게, 개별 객체에의 접근을 통제하는 방법을 제공할 것입니다.

클래스가 get과 set을 쓰도록 바꿔봅시다. 먼저 getter와 setter 없이 예시로 시작해봅시다.

class Employee {
    fullName: string;
}

let employee = new Employee();
employee.fullName = 'Bob smith';
if(employee.fullName)
{
    console.log(employee.fullName);
}

사람들에게 렌덤으로 fullName을 변경하게 해주는것은 매우 쉬우나 만약 사람들이 변덕으로 이름을 바꿔버린다면 문제가 생길 것입니다.


이번 version에서는 employee 이름을 바꾸기 전에 사용자가 passcode를 가지고 있는지 확인합니다. 직접 fullName에 접근하는 대신 set으로 passcode가 있는지 확인하는 방식으로 바꿀겁니다. 우리는 get은 이전 코드와 상응하게 추가했습니다.

let passcode = 'secret code';

class Employee {
    private _fullName: string;
    
    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string)
    {
        if(passcode && passcode === 'secret code')
        {
            this._fullName = newName;
        }
        else
        {
            console.log("error: Unauthorized update of employee!");
        }
    }
}

let employee = new Employee();
employee.fullName = 'Bob smith';
if(employee.fullName)
{
    console.log(employee.fullName);
}

정말 passcode를 통해 내부에 접근이 가능한지 확인하고 싶다면 passcode를 바꿔보고 경고 메시지가 뜨는것을 확인해 보면 됩니다.


접근자에 대해 몇가지 주의할 점.

첫번째로, 접근자는 ECMAScript 5 이상을 지원하며 ECMAScript 3이하는 지원하지 않습니다. 두번째로 set은 없고 get만 있으면 자동으로 readonly라고 해석됩니다. 이건 .d.ts 파일만들때 유용한데 그 이유는 사용자가 당신의 코드가 수정할 수 없다는 것을 알 수 있기 때문입니다.

Static Properties

지금까지 우리는 인스턴스화했을 때 나타나는 클래스의 멤버들에대해서만 언급해왔다. static 멤버도 클래스에 만들수 있는데, 이 멤버들은 인스턴스에 보이는게 아니라 클래스 그자체에 사용 할 수있습니다. 이번 예제에서 우리는 static 멤버를 사용했는데, 이것은 모든 격자의 일반적인 값입니다. 클래스이름 뒤에 붙임으로써 각각의 인스턴스는 이 값에 접근할 수있습니다. this.뒤에 멤버를 붙이듣이 Grid.과 같이 함수이름 뒤에 멤버를 붙임으로써 접근이 가능합니다.

class Grid {
    static origin = { x: 0, y: 0};
    calculateDistanceFromOrigin(point: { x: number; y: number})
    {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }

    constructor(public scale: number) {}
}

let grid1 = new Grid(1.0);
let grid2 = new Grid(5.0);

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

Abstract Classes

추상 클래스는 다른 클래스에 상속할지도 모르는 base-class다.(애매한 표한을 쓴건 다 이유가 있다.) Abstract 클래스는 인스턴스화가 허락되지 않는다. 인터페이스와는 다르게 abstract 클래스는 멤버의 바디가 있을 수 있다. 즉, 멤버의 구현이 가능하다. abstract 클래스 안에서 (abstract)추상 메서드서 사용할 수도 있고 추상 클래스를 정의할 때에도 abstract 키워드는 사용될 수 있다.

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}

추상메서드는 추상클래스 안에서는 구현할 수 없고 반드시 추상클래스를 상속받은 자식 클래스에서 구현해야만한다. 추상 메서드는 인터페이스에서와 같은 형태를 띄는데, 둘다 메서드를 메서드 바디 없이 정의한다. 그러나 추상 메서드는 반드시 abstract 키워드를 포함해야 하며 access modifier(readonly, protected, public, private)를 옵션으로 추가할 수 있다.

abstract class Department {
    constructor(public name: string){

    }
    printName(): void {
        console.log('Department name: ' + this.name);
    }

    abstract printMeething(): void; //Must be implemented in derived classes
}

class AccountingDepartment extends Department {
    constructor () {
        super("Accounting and Auditing"); //constructors in derived classes must call super()
    }

    printMeeting(): void {
        console.log("The accounting department meets each Monday at 10am.");
    }

    generateReports(): void {
        console.log("Generating accounting reports...");
    }
}

let department: Department; // ok to create a reference to an abstract type
department = new Department(); // error: cannot create an instance of an abstract class
department = new AccountingDepartment(); // ok to create and assign a non-abstract subclass
department.printName();
department.printMeeting();
department.generateReports(); // error: method doesn't exist on declared abstract type

주석: 마지막 줄 department.generateReports()에서 error를 발생시키는 점에 주의하라. 추상메서드를 상속받을 경우에는 반드시 추상메서드의에서 구현하기를 바라는 메서드에 대해서만 구현하지 그외에는 추가되지 않는다.

1 Like