[TS] 클래스

ClassBinu·2024년 3월 26일

클래스

원래 JS는 재사용 컴포넌트를 위해 함수와 프로토타입 기반 상속을 사용
ES6에서 클래스 문법이 적용됨

상속

extends 키워드로 상속 구현
자식 클래스를 subclasses, 부모 클래스를 superclasses라고 함.

파생 클래스 생성자

파생 클래스에서 생성자 함수는 기초 클래스의 생성자를 실행할 super()를 호출해야 함.
super()는 부모의 생성자 함수(constructor)를 호출함.

class Person {
    constructor(name: string) {
        console.log(`${name} is a person.`);
    }
}

class Employee extends Person {
    constructor(name: string, title: string) {
        super(name); // 부모 클래스인 Person의 생성자 호출
        console.log(`${name} is an employee and works as a ${title}.`);
    }
}

const newEmployee = new Employee("John", "Developer");

생성자 함수 contrcutor는 클래스 내에 정의되며, 새로운 인스턴스가 생성될 때 자동으로 호출
인스턴스 변수의 초기값을 설정, 또는 객체 생성 시 필요한 준비 작업 수행

오버라이드

같은 이름의 메서드라면 자식 클래스로 덮어 씌어짐.

접근 제어자

public

해당 멤버에 자유롭게 접근

private

외부에서 해당 멤버에 접근 불가, 내부에서만 접근

protected

내부 및 하위 클래스에서 접근 가능

타입 호환성

호환된다고 판단되는 두 개의 타입 중 한 쪽에서 private멤버를 가지고 있다면, 다른 한 쪽도 무조건 동일한 선언에 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'은 호환될 수 없음.

읽기 전용 지정자

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은 읽기전용 입니다.

매개변수 프로퍼티

클래스의 생성자에게 멤버 변수를 선언하고 초기화하는 작업을 한 줄로 간결하게 처리
생성자의 매개변수에 접근 제어자나 readonly를 붙여서 사용

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) { // 이 부분 주목
    }
}

// 매개변수 프로퍼티가 없으면 이렇게
class Octopus {
    readonly name: string; // 여기 선언하고,
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName; // 여기서 할당해야 함.
    }
}

접근자

각 객체 멤버 접근 방법 제어

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

Getter & Setter

Getter

  • get 키워드로 정의
  • 객체의 특정 속성 값 읽을 때 호출
  • 매개 변수 받지 않음, 호출 시 특정 값 반환
  • 객체 내부 상태 노출하지 않고, 필요한 처리를 거쳐 속성 값 제공할 때 사용

Setter

  • set 키워드로 정의
  • 객체의 특정 속성에 값을 할당할 때 호출
  • 정확히 하나의 매개변수를 받으며, 반환 값은 없음
  • 객체 속성 값이 변경될 때 필요한 검증이나 추가 처리 가능

전역 프로퍼티

클래스 내에 static으로 선언하면
인스턴스 없이 객체 자체의 멤버를 접근할 수 있음.

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) { }
}

this로 접근하지 않는다.
this는 인스턴스 생성 후 해당 인스턴스에 바인딩 됨.

전역 프로퍼티는 인스턴스마다 독립적으로 유지될 필요가 없는 데이터 처리할 때 사용. 메모리 사용을 최적화함.

추상 클래스

다른 클래스들이 파생될 수 있는 기초 클래스.
단, 직접 인스턴스화할 수 없음.
추상 메서드도 있다.

추상 클래스는 일반 메서드와 추상 메서드를 모두 포함할 수 있음.
추상 클래스 상속 시 하위 클래스는 추상 클래스의 일반 메서드는 상속되고,
추상 메서드는 직접 구현을 해야 함.

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

생성자 함수

클래스의 인스턴스 new를 하면 호출되는 함수(constructor)
new를 호출하고 생성자 함수를 실행하면 인스턴스를 얻는다.

클래스를 인터페이스처럼 사용

클래스를 선언하면 인스턴스 타입(속성, 메서드)과 생성자 함수를 생성한다.
클래스는 타입을 생성하므로, 인터페이스를 사용할 수 있는 동일 위치에서 사용할 수 있음.

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

인터페이스가 올 자리에 클래스가 올 수 있다!

그냥 이것도 가능

class Point {
  x: number;
  y: number;
}

let point: Point = { x: 1, y: 2 };

structural typing(duck typing)

객체의 실제 타입보다 객체가 어떤 속성이나 메서드를 가지고 있는지가 중요
클래스의 인스턴스가 인퍼테이스에서 정의한 모든 속성과 메서드를 충족한다면,
해당 클래스의 인스턴스는 인터페이스 타입의 변수에 할당될 수 있음.

그럼 이건 무슨 차이?

class Point {
  x: number;
  y: number;
}

interface Point {
  x: number;
  y: number;
}

let point: Point = { x: 1, y: 2 };

클래스

  • 생성자를 통해 인스턴스 생성 가능
  • 런타임에도 존재
  • 컴파일 시 js코드로 존재
  • 클래스 확장(상속)(extends), 인터페이스 구현(implements) 가능

인터페이스

  • 타입 체크를 위한 명세
  • 타입 체크 시점에만 존재
  • 런타임 시 존재하지 않음
  • 컴파일 시 타입 체크 사용 후 제거
  • 인터페이스 확장(extends) 가능, 구현을 불가능

extends vs implements

extends

  • 단일 클래스 상속만 지원
  • 상속에 초점(기존 코드 확장에 초점)

implements

  • 클래스가 특정 인터페이스 계약 준수를 강제하고 싶을 때
  • 다중 구현 지원
  • 구현 강제에 초점

추상 클래스도 구현을 강제하지만, 클래스의 성격을 가지고 있음.
(일반 메서드 상속 가능, 단일 상속, 런타임 시 존재 등)

생성자 없는 클래스?

가능함.
타입스크립트에서 클래스는 타입 체크 기능도 있기 때문에 그렇게 쓸 수도 있음.

그리고 생성자 함수가 없으면 아무 것도 없는 생성자 함수가 있는 걸로 간주됨.
클래스에 명시적인 생성자가 없어도 기본 생성자가 사용되지만,
생성자를 명시적으로 선언하여 예상치 못한 동작 방지하고 클래스 의도 명확히 표현하는 게 좋음.

0개의 댓글