ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TypeScript] 클래스
    TypeScript 2022. 9. 15. 20:53
    728x90

    클래스(FClasses)

    class Greeter {
        greeting: string;
        constructor(message: string) {
            this.greeting = message;
        }
        greet() {
            return "Hello, " + this.greeting;
        }
    }
    
    let greeter = new Greeter("world");
    

    C#이나 Java를 사용해봤다면 익숙한 구문일 것입니다. Greeter 클래스는 프로퍼티인 greeting , 생성자, 메서드인 greet 인 3개의 멤버로 이루어져 있습니다.

    클래스의 멤버를 참조할 때 this. 을 앞에 붙여 멤버에 접근하는 것을 알 수 있습니다.

    new 를 사용하여 Greeter 클래스의 인스턴스를 생성할 수 있습니다. 이전에 작성한 생성자를 호출하여 Greeter 형태의 새로운 객체를 만들어 초기화합니다.

    상속(Inheritance)

    TypeScript에서는 객체-지향 패턴을 사용할 수 있으며 상속을 이용하여 이미 존재하는 클래스를 확장해 새로운 클래스를 만들 수 있습니다.

    아래는 상속 기능을 보여주는 가장 기본적인 예제입니다.

    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();
    

    Dog 는 extends 키워드를 사용하여 Animal 이라는 기초(상위)클래스로부터 파생된 파생(하위)클래스입니다. Dog는 Animal의 기능을 상속받아 확장하기 때문에 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("Galloping...");
            super.move(distanceInMeters);
        }
    }
    
    let sam = new Snake("Sammy the Python");
    let tom: Animal = new Horse("Tommy the Palomino");
    
    sam.move();
    tom.move(34);
    
    //결과
    Slithering...
    Sammy the Python moved 5m.
    Galloping...
    Tommy the Palomino moved 34m.
    

    Horse와 Snake는 Animal의 하위 클래스입니다.

    이전 예제와 다른점은 super()를 호출한다는 점인데요 하위 클래스는 상위 클래스의 생성자를 실행할 때 super() 클래스를 실행해야 합니다.

    쉽게 말해서 부모 클래스에서 this. 를 사용한 곳에 접근하려면 반드시 super()를 붙여서 사용해야 합니다.

    Public, private 그리고 protected 지정자 (Public, private, and protected modifiers)

    Public by default

    C#이나 Java에서는 노출 시킬 멤버에 public 을 붙여야 하지만 TypeScript는 명시하지 않으면 public 을 뜻합니다. 하지만 명시해도 상관 없습니다.

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

    ECMAScript 비공개 필드 (ECMAScript Private Fields)

    TypeScript 3.8부터 비공개 필드를 위한 새로운 문법을 지원합니다.

    class Animal {
        #name: string;
        constructor(theName: string) { this.#name = theName; }
    }
    
    new Animal("Cat").#name; // 프로퍼티 '#name'은 비공개 식별자이기 때문에 'Animal' 클래스 외부에선 접근할 수 없습니다.
    

    TypeScript에서 private 필드를 사용하려면 몇가지 규칙이 존재합니다.

    • private 필드는 # 문자로 시작합니다.
    • private 필드의 이름은 클래스 내에서 유일해야 한다.
    • 이미 #을 붙였다면 private나 public을 앞에 붙일 수 없다.

    TypeScript의 private

    class Animal {
        private name: string;
        constructor(theName: string) { this.name = theName; }
    }
    
    new Animal("Cat").name; // 오류: 'name'은 비공개로 선언되어 있습니다;
    

    private와 protected는 반드시 서로 같은 멤버여야만 호환된다고 판단됩니다.

    실제로 어떻게 작동되는지 알아보겠습니다.

    class Animal {
        private name: string;
        constructor(theName: string) { this.name = theName; }
    }
    
    class Rhino extends Animal {
        constructor() { super("Rhino"); }
    }
    
    class Employee {
        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; // 오류: 'Animal'과 'Employee'은 호환될 수 없음.
    

    animal = rinho가 가능한 이유는

    둘 다 Animal에서 선언된 private 이기 때문입니다.

    하지만 animal = employee에서는

    Animal에서 선언된 private와 Employee에서 선언된 private이므로 name이라는 같은 private를 갖고 있지만 다른 곳에서 선언됐기 때문에 타입이 호환되지 않는다는 에러가 발생합니다.

    Protected 이해하기

    protected 지정자도 protected로 선언된 멤버를 파생된 클래스 내에서 접근할 수 있다는 점만 제외하면 private 지정자와 매우 유사하게 동작합니다.

    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); // 오류
    

    howard 변수는 Person 외부이기 때문에 name을 사용할 수 없지만 Employee 는 person의 자식이기 때문에 Employee 내부에서는 name을 사용 및 확장할 수 있습니다.

    읽기전용 지정자(Readonly modifier)

    readonly 키워드를 사용하여 프로퍼티를 만들면 읽기전용이 됩니다. 읽기전용 프로퍼티는 선언 또는 생성자에서만 초기화 할 수 있습니다.

    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 the 3-piece suit"; // 오류! name은 읽기전용 입니다.
    

    매개변수 프로퍼티(Parameter properties)

    위의 Octopus 클래스를 매개변수 프로퍼티를 사용하면 아래와 같습니다.

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

    선언과 할당을 한 곳으로 통합 한 것입니다.

    매개변수 프로퍼티에 사용한 멤버를 선언하고 초기화합니다. 즉, readonly면 readonly로 public이면 public으로 선언 및 초기화합니다.

    접근자(Accessors)

    TypeScript는 객체의 멤버에 대한 접근을 가로채는 방식으로 getters/setters를 지원합니다.

    getters와 setters가 없는 예제입니다.

    class Employee {
        fullName: string;
    }
    
    let employee = new Employee();
    employee.fullName = "Bob Smith";
    if (employee.fullName) {
        console.log(employee.fullName);
    }
    

    사용자가 fullName을 직접 설정할 수 있도록 허용하는 것은 매우 간단합니다. 하지만 우리는 fullName 설정에 제약 조건을 원할 수 있습니다.

    fullName에 최대 길이를 제한하고싶다면 newName의 길이를 확인하는 setter를 추가합니다. 이 때 제한된 길이를 초과하면 오류가 발생합니다.

    기존의 기능을 유지하기 위해, fullName을 수정하지 않는 간단한 getter도 추가한 예제입니다.

    const fullNameMaxLength = 10;
    
    class Employee {
        private _fullName: string;
    
        get fullName(): string {
            return this._fullName;
        }
    
        set fullName(newName: string) {
            if (newName && newName.length > fullNameMaxLength) {
                throw new Error("fullName has a max length of " + fullNameMaxLength);
            }
    
            this._fullName = newName;
        }
    }
    
    let employee = new Employee();
    employee.fullName = "Bob Smith";
    if (employee.fullName) {
        console.log(employee.fullName);
    }
    

    접근자에 대해 주의해야 할 사항

    • 접근자는 ECMAScript 5 이상을 출력해야한다.
    • get, set이 없는 접근자는 자동으로 readonly로 취급한다.

    전역 프로퍼티(Static Properties)

    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);  // 1x scale
    let grid2 = new Grid(5.0);  // 5x scale
    
    console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
    console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
    

    추상 클래스(Abstract Classes)

    추상 클래스는 다른 클래스들이 파생될 수 있는 기초 클래스입니다. 추상 클래스는 직접 인스턴스화할 수 없습니다. 추상 클래스는 인터페이스와 달리 멤버에 대한 구현 세부 정보를 포함할 수 있습니다. 추상클래스 및 추상 메서든는 abstract 키워드를 통해 정의합니다.

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

    추상 클래스 내에서 추상으로 표시된 메서드는 구현을 포함하지 않으며 반드시 파생된 클래스에서 구현됩니다. 아래의 예제에서 자세히 다뤄보겠습니다.

    abstract class Department {
    
        constructor(public name: string) {
        }
    
        printName(): void {
            console.log("Department name: " + this.name);
        }
    
        abstract printMeeting(): void; // 반드시 파생된 클래스에서 구현되어야 합니다.
    }
    
    class AccountingDepartment extends Department {
    
        constructor() {
            super("Accounting and Auditing"); // 파생된 클래스의 생성자는 반드시 super()를 호출해야 합니다.
        }
    
        printMeeting(): void {
            console.log("The Accounting Department meets each Monday at 10am.");
        }
    
        generateReports(): void {
            console.log("Generating accounting reports...");
        }
    }
    
    let department: Department; // 추상 타입의 레퍼런스를 생성합니다
    department = new Department(); // 오류: 추상 클래스는 인스턴스화 할 수 없습니다
    department = new AccountingDepartment(); // 추상이 아닌 하위 클래스를 생성하고 할당합니다
    department.printName();
    department.printMeeting();
    department.generateReports(); // 오류: 선언된 추상 타입에 메서드가 존재하지 않습니다
    

    고급 기법(Advanced Techniques)

    생성자 함수(Constructor functions)

    TypeScript는 클래스를 선언하면 실제로 여러 개의 선언이 동시에 생성됩니다

    1. 인스턴스 타입
    class Greeter {
        greeting: string;
        constructor(message: string) {
            this.greeting = message;
        }
        greet() {
            return "Hello, " + this.greeting;
        }
    }
    
    let greeter: Greeter;
    greeter = new Greeter("world");
    console.log(greeter.greet()); // "Hello, world"
    

    여기서 let greeter: Greeter 라고 선언하면 Greeter 클래스의 인스턴스 타입으로 Greeter를 사용합니다.

    또한 greeter = new Greeter(”world”)로 또 다른 값을 생성하고 있습니다. 이것은 클래스 인스턴스를 new 로 생성자 함수를 호출하는 것 입니다. 예제로 확인하겠습니다.

    class Greeter {
        static standardGreeting = "Hello, there";
        greeting: string;
        greet() {
            if (this.greeting) {
                return "Hello, " + this.greeting;
            }
            else {
                return Greeter.standardGreeting;
            }
        }
    }
    
    let greeter1: Greeter;
    greeter1 = new Greeter();
    console.log(greeter1.greet()); // "Hello, there"
    
    let greeterMaker: typeof Greeter = Greeter;
    greeterMaker.standardGreeting = "Hey there!";
    
    let greeter2: Greeter = new greeterMaker();
    console.log(greeter2.greet()); // "Hey there!"
    

    greeter1은 생성자 함수를 할당받아 클래스의 인스턴스를 얻습니다.

    여기서 typeof Greeter를 사용하면 인스턴스 타입이 아닌 Greeter 클래스 자체의 타입을 제공하는 정적 측면이 제공됩니다. 더 정확하게 말하면 생성자 함수 타입인 Greeter라는 심볼의 타입을 제공합니다.

    인터페이스로써 클래스 사용하기(Using a class as an interface)

    클래스는 타입을 생성하기 때문에 인터페이스를 사용할 수 있는 동일한 위치에서 사용할 수 있습니다.

    class Point {
        x: number;
        y: number;
    }
    
    interface Point3d extends Point {
        z: number;
    }
    
    let point3d: Point3d = {x: 1, y: 2, z: 3};
    

    참고

    https://typescript-kr.github.io/pages/classes.html

    728x90

    'TypeScript' 카테고리의 다른 글

    [TypeScript] 제네릭  (0) 2022.09.17
    [TypeScript] 열거형  (1) 2022.09.16
    [TypeScript] 유니언과 교차타입  (1) 2022.09.10
    [TypeScript] 인터페이스  (0) 2022.09.08
    [TypeScript] 기본 타입  (2) 2022.09.07

    댓글

Designed by Tistory.