TypeScript (5) 클래스

Jiwon Youn·2021년 3월 11일
0

TypeScript

목록 보기
5/6

Class

객체 지향 접근 방식에 익숙한 입장에서는 클래스가 함수를 상속받고 이런 클래스에서 객체가 만들어지는 것에 다소 어색함을 느낄 수 있다. JavaScript 프로그래머들은 이런 객체 지향적 클래스 기반의 접근 방식을 사용해서 프로그램을 작성할 수 있다.

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

let greeter = new Greeter("world");

새로운 클래스 Greeter는 3개의 멤버를 가지고 있다.

  • greeting 프로퍼티
  • 생성자 (constructor)
  • greet() method

클래스 안에서 클래스의 멤버를 참조할 때는 this를 사용한다.이것은 멤버에 접근하는 것을 의미한다.
마지막 줄에서, new를 사용하여 Greeter 클래스의 인스턴스를 생성한다. 이 코드는 이전에 정의한 생성자를 호출하여 Greeter 형태의 새로운 객체를 만들고, 생성자를 실행해 초기화한다.

Inheritance (상속)

TypeScript에서는, 일반적인 객체 지향 패턴을 사용할 수 있다. 클래스 기반 프로그래밍의 가장 기본적인 패턴 중 하나는 상속을 이용하여 이미 존재하는 클래스를 확장해 새로운 클래스를 만들 수 있다는 것이다.

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

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

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

클래스는 기초 클래스로부터 프로퍼티와 method를 상속받는다. Dogextends 키워드를 사용하여 Animal이라는 기초 클래스로부터 파생된 파생 클래스이다. 파생된 클래스는 하위 클래스(subclasses), 기초 클래스는 상위 클래스(superclasses)로 불린다.

조금 더 복잡한 extends 예제

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

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

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

let sam = new Snake("Sammy the Python");
let tom = new Horse("Tommy the Palomino");

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

Animalextends해서 하위 클래스 HorseSnake를 생성한다. 위와 같이 클래스를 상속 받을 때 주의할 점이 있다.

  • 파생된 클래스의 생성자 함수는 기초 클래스의 생성자를 실행할 super()를 실행해야 한다.
  • 생성자 내에서 this에 있는 프로퍼티에 접근하기 전에 super()를 호출해야 한다.

이 점은 TypeScript에서 중요한 규칙이다.


Private

TypeScript에는 멤버를 포함하는 클래스 외부에서 이 멤버에 접근하지 못하도록 멤버를 private으로 표시하는 방법이 있다.

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

new Animal("Cat").name; // Error. 'name'은 private로 선언되어 있다.

privateprotected 멤버가 있는 타입들을 비교할 때는 타입을 다르게 처리한다. 호환된다고 판단되는 두 개의 타입 중 한 쪽에서 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; // Error. 'Animal'과 'Employee'는 호환 불가능

위 예제에서 Animal과 그를 상속 받은 클래스 Rhino가 있다. Animal과 형태가 같아보이는 Employee라는 클래스도 있다. AnimalRhinoAnimalprivate name: string이라는 동일한 선언으로부터 private 부분을 공유하기 때문에 호환이 가능하다. 하지만 Employee의 경우 name이라는 private 멤버를 보유하지만, Animal에서 선언한 것이 아니기 때문이다.

Protected

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

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 `name: ${this.name}, work: ${this.department}.`;
 }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // Error

Person 외부에서 name을 사용할 수 없지만, EmployeePerson에서 파생되었기 때문에 Employee의 인스턴스 method 내에서는 여전히 사용할 수 있다. 생성자 또한 protected로 표시될 수 있다.

Readonly

readonly 키워드를 사용하여 프로퍼티를 읽기전용으로 만들 수 있다. 읽기전용 프로퍼티들은 선언 또는 생성자에서 초기화해야 한다.

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

let add = new Octpus("Man with the 8 strong legs");

dad.name = "Man with the 3-piece suit"; 
// Error. name은 readonly. 

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

위의 Octopus 클래스의 생성자 매개변수를 좀 더 간단하게 선언할 수 있다.

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

생성자에 짧아진 readonly name: string 파라미터를 사용하여 theName을 제거하고 name 멤버를 생성하고 초기화했다. 즉 선언과 할당을 한 곳으로 통합했다.
매개변수 프로퍼티에 private를 사용하면 비공개 멤버를 선언하고 초기화한다. 마찬가지로, public, protected, readonly도 동일하게 작용한다.

Accessors (접근자)

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

class Employee {
 fullName: string;
}

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

위는 getters/setters가 없는 예제이다. 임의로 fullName을 직접 설정할 수 있도록 허용하는 것은 매우 편리하지만, 경우에 따라 fullName이 설정될 때 몇 가지 제약 조건을 걸어야 할 수 도 있다.

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

위 예시에서는 newName의 길이가 상수 fullNameMaxLength의 길이를 넘지 않는지 확인하는 setter를 추가한다. 만약 최대 길이를 초과한다면 new Error로 오류를 발생시킨다. 기존의 기능을 유지하기 위해 fullName을 수정하지 않는 getter도 생성한다.

Abstract Class (추상 클래스)

추상 클래스는 다른 클래스들이 파생될 수 있는 기초 클래스이다. 추상 클래스는 직접 인스턴스화 할 수 없다. abstract 키워드는 추상 클래스 뿐만 아니라 추상 클래스 내에서 추상 method를 정의하는데 사용된다.

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

추상 클래스 내에서 추상으로 표시된 method는 구현을 포함하지 않으며 반드시 파생된 클래스에서 구현되어야 한다. 추상 method는 인터페이스 method와 비슷한 문법을 공유하나, 추상 method는 반드시 abstract 키워드를 포함해야 한다.

abstract class Department {
 constructor(public name: string) {
 }
 
 printName(): void {
  console.log("Department name: " + this.name);
 }
 
 abstract printMeeting(): void;
 // 반드시 파생된 클래스에서 구현되어야 하는 method
}


class AccountingDepartment extends Department {
 constructor() {
  super("Accounting and Auditing");
  // 파생된 클래스의 생성자는 반드시 super()를 호출해야 한다.
 }
 
 printMeeting(): void {
  console.log("The Accounting Department meets each Monday.");
 }
 
 generateReports(): void {
  console.log("Generating accounting reports...");
 }
}

let department: Department; // 추상 타입의 레퍼런스 생성
department = new Department(); // Error. 추상 클래스는 인스턴스화 불가능
department = new AccountingDepartment(); // 추상이 아닌 하위 클래스를 생성하고 할당
department.printName();
department.printMeeting();
department.generateReports(); // Error. 선언된 추상 타입에 method가 존재하지 않음

인터페이스로 클래스 사용하기

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

interface Point3d extends Point {
 z: number;
}

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

클래스 선언은 클래스의 인스턴스를 나타내는 타입과 생성자 함수라는 두 가지를 생성한다. 클래스는 타입을 생성하기 때문에 인터페이스를 사용할 수 있는 동일한 위치에서 사용 가능하다.

참고 문서

0개의 댓글