TypeScript 핸드북 4 - 클래스

클래스 (Class)

소개

JavaScript는 전통적으로 재사용 가능한 컴포넌트를 만들기 위해 함수와 프로토 타입 기반 상속을 사용하지만, 클래스가 기능을 상속하고 객체가 이러한 클래스에서 빌드되는 객체 지향 접근 방식에 익숙하지 않은 프로그래머에게는 다소 생소합니다. ECMAScript 2015 (ECMAScript 6)로 시작하는 JavaScript 프로그래머는 이러한 객체 지향 클래스 기반 접근 방식을 사용하여 응용 프로그램을 빌드할 수 있습니다. 하지만
TypeScript 개발자는 클래스의 기능을 모든 주요 브라우저와 플랫폼에서 작동하는 JavaScript로 컴파일할 수 있습니다.

클래스

간단한 클래스 기반 예제를 살펴 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
console.log (new Greeter("world").greet());

이전에 C#이나 Java를 사용했다면 구문이 익숙할듯 합니다. 새로운 클래스 Greeter를 선언 했습니다. 이 클래스에는 세 가지 멤버가 있습니다. (greeting이라는 프로퍼티, 생성자, greet 메서드)

클래스의 멤버 중 하나를 참조할 때 클래스에서 this.를 앞에 둡니다. 이는 클래스 멤버에 대한 액세스임을 나타냅니다.

마지막 줄에서는 new를 사용하여 Greeter 클래스의 인스턴스를 만듭니다. 이것은 앞서 정의한 생성자를 호출하여 Greeter 타입의 새로운 객체를 생성하고 초기화합니다.

상속

TypeScript에서는 일반적인 객체 지향 패턴을 사용할 수 있습니다. 그중에 클래스 기반 프로그래밍에서 가장 기본적인 패턴 중 하나인 상속을 사용하여 기존 클래스를 확장하여 새로운 클래스를 생성할 수 있다는 것입니다.

예제를 살펴 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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);

이 예제는 다른 언어에서 공통적으로 사용하는 상속기능을 TypeScript에서 설명합니다. 하위 클래스를 만들때 extends 키워드를 사용합니다. HorseSnake가 기본 클래스 인 Animal의 하위 클래스로 분류되고 그 기능을 액세스할 수있습니다.

생성자 함수를 포함하는 파생 클래스는 기본 클래스에서 생성자 함수를 실행할 무조건 super()를 호출해야 합니다.

또한 이 예제의 기본 클래스는 메서드를 하위 클래스에 특화된 메서드로 재정의하는 방법을 보여줍니다. 여기서 SnakeHorseAnimalmove 메소드를 오버라이드하는 move 메소드를 생성하여 각 클래스별로 기능을 부여했습니다. tomAnimal로 선언되었지만,tom.move (34)Horse에서 재정의 메소드를 호출할 때, 그 값은 Horse입니다.

1
2
3
4
Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.

public, private, and protected

기본은 public

위의 예제에서 프로그램을 통해 선언 한 멤버들은 자유롭게 접근할 수있었습니다. 다른 언어에 익숙하다면, 위의 예에서 public이라는 키워드를 사용하지 않아도 된다는 것을 알았을 것입니다. 예를 들어, C#에서는 각 멤버가 명시적으로 public으로 표시되도록 해야합니다. TypeScript에서는 기본적으로 각 멤버가 public입니다.

하지만 public멤버를 명시 적으로 표시할 수 있습니다. 이전 예제를 다음과 같은 방식으로 작성할 수도 있습니다.

1
2
3
4
5
6
7
class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}

private 이해하기

멤버가 private으로 표시되면 클래스 외부에서 액세스할 수 없습니다.

1
2
3
4
5
6
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // Error: 'name' is private;

TypeScript는 구조형 시스템입니다. 만약 두 가지 타입을 비교할 때, 그들이 어디서 왔는지에 관계없이, 모든 구성원의 타입이 호환 가능하다면 타입 자체가 호환 가능하다고 말합니다.

그러나 privateprotected 멤버가 있는 타입을 비교할 때 이러한 타입을 다르게 처리합니다. 만약 두 가지 타입이 호환 가능한 경우는 그 중 하나가 private 멤버라면 다른 멤버도 동일한 선언에서 비롯된 private 멤버를 가져야합니다. protected 멤버에게도 동일하게 적용됩니다.

실제로 어떻게 실행되는지 더 잘 보도록 예제를 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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' and 'Employee' are not compatible

이 예제에서는 AnimalRhino를 가지고 있는데 RhinoAnimal의 하위 클래스입니다. 또한 Animal과 모양이 똑같은 Employee라는 새로운 클래스도 가지고 있습니다. 이러한 클래스의 인스턴스를 생성 한 다음 서로를 할당하여 어떤 일이 발생하는지 봅니다. AnimalRhinoAnimalprivate name : string과 같은 선언으로부터 같은 형태의 private 부분을 공유하기 때문에 호환됩니다. 그러나 Employee의 경우는 그렇지 않습니다. Employee에서 Animal에 할당하려고 할 때, 우리는이 타입들이 호환되지 않는다는 에러를 얻습니다. Employeename이라는 private 멤버를 가지고 있지만 Animal로 선언 한 멤버는 아닙니다.

protected 이해하기

protected 키워드는 private과 매우 유사하게 동작합니다. 단, protected로 선언된 멤버는 파생 클래스의 인스턴스에서 액세스할 수 있습니다. 예를 들어,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

Person의 외부에서 name을 사용할 수는 없지만 Employee의 인스턴스 메소드에서 사용할 수 있습니다. 왜냐하면 EmployeePerson에서 파생되었기 때문입니다.

생성자는 protected로 표시 될 수도 있습니다. 즉, 클래스 외부에서 클래스를 인스턴스화 할 수는 없지만 확장할 수는 있습니다. 예를 들어,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
// 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

readonly 키워드를 사용하여 읽기 전용 속성을 만들 수 있습니다. readonly 속성은 선언 또는 생성자에서 초기화해야합니다.

1
2
3
4
5
6
7
8
9
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"; // error! name is readonly.

파라미터 프로퍼티

위의 예제에서 우리는 Octopus 클래스에서 읽기 전용 멤버 name과 생성자 파라미터 theName을 선언 했습니다. 그런 다음 바로nametheName으로 설정했습니다. 이것은 매우 일반적인 방법입니다. Parameter property을 사용하면 한 곳에서 멤버를 만들고 초기화할 수 있습니다. 다음은 Parameter property를 사용하는 이전의Octopus 클래스의 수정 버전입니다.

1
2
3
4
5
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}

theName을 어떻게 버렸고, name 멤버를 생성하고 초기화하기 위해 생성자에 readonly name : string 파라미터를 사용했는지에 주목하십시오. 선언과 할당을 하나의 위치로 통합했습니다.

Parameter property는 접근자 (Accessibility Modifier) 또는 readonly 또는 둘 모두로 생성자 파라미터 앞에 접두어를 붙임으로써 선언됩니다. Parameter propertyprivate을 사용하면 private 멤버를 선언하고 초기화합니다. public, protected, readonly도 마찬가지입니다.

Getter/Setter(Accessor)

TypeScript는 객체의 멤버에 대한 액세스를 지원하는 Getter/Setter 메서드가 있습니다. 이 메서드를 이용하여 객체의 멤버 프로퍼티가 액세스되는 방식을 세밀하게 제어할 수 있습니다.

간단한 클래스를 작성하고 변환하여 getset을 사용해 보겠습니다. 우선 getter 및 setter가 없는 예제부터 시작해 보겠습니다.

1
2
3
4
5
6
7
8
9
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}

사람들이 fullName을 설정하도록 허용하는 것은 매우 편리할 수 있지만 무작위 사람들이 이름을 바꿀 수 있다면 문제가 발생할 수 있습니다.

아래 코드에서는 직원 수정을 허용하기 전에 사용자가 비밀 암호 코드를 사용할 수 있는지 확인합니다. 우리는 fullName에 대한 직접 접근을 패스 코드를 검사할 set으로 대체함으로써 이것이 가능합니다. 앞의 예제가 계속해서 원활하게 작동할 수있게하기 위해 상응하는 get도 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let passcode = "secret passcode";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "secret passcode") {
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);
}

Setter에서 암호를 수정할수 있는 권한이 있는지 비밀코드를 확인하여 일치하면해 암호를 수정하고, 일치하지 않을 때 직원에게 업데이트 할 수있는 권한이 없다는 경고 메시지를받습니다.

우리가 Getter/Setter(Accessor)에 대해 알아야 할 몇 가지 사항은 다음과 같습니다.

첫째, Getter/Setter는 ECMAScript 5 이상을 사용하도록 컴파일러를 설정해야합니다. ECMAScript 3에 대한 하위 레벨링은 지원되지 않습니다. 둘째, getset이 없는 프로퍼티는 자동으로 읽기 전용으로 추정됩니다. 이것은 여러분의 코드에서 .d.ts 파일을 생성할 때 유용합니다. 왜냐하면 여러분의 프로퍼티 사용자가 그것을 변경할 수 없다는 것을 알 수 있기 때문입니다.

static 프로퍼티

지금까지는 클래스의 인스턴스 멤버에 대해서만 설명했습니다. 인스턴스가 아닌 클래스 자체에서 볼 수있는 클래스의 정적 멤버를 만들 수도 있습니다. 아래 예제에서는 static을 사용합니다. 모든 Grid의 일반 값이기 때문입니다. 각 인스턴스는 클래스의 이름을 선행하여 이 값에 액세스합니다. 인스턴스 액세스 앞에 this.를 추가하는 것과 마찬가지로 정적 액세스 앞에 Grid.를 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 Class)

추상 클래스는 다른 클래스를 파생시킬 수 있는 기본 클래스입니다. 하지만 직접 인스턴스화 할 수 없습니다. 인터페이스와 달리 추상 클래스는 멤버에 대한 Implementation 세부 정보를 포함할 수 있습니다. abstract 키워드는 추상 클래스 내 추상 메서드뿐만 아니라 추상 클래스를 정의하는데 사용됩니다.

1
2
3
4
5
6
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("roaming the earth...");
}
}

abstract로 표시된 추상 클래스 내의 메소드에는 Implementation이 포함되어 있지 않으므로 파생 클래스에서 구현해야합니다. 추상 메소드는 인터페이스 메소드와 유사한 구문을 사용합니다. 둘 다 메소드 본문을 포함하지 않고 Method signature를 정의합니다. 그러나 추상 메소드는abstract 키워드를 포함해야 하며 선택적으로 Getter/Setter를 포함할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
abstract class Department {
constructor(public name: string) {
}
printName(): void {
console.log("Department name: " + this.name);
}
abstract printMeeting(): 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

고급 기술

생성자 함수

TypeScript에서 클래스를 선언하면 실제로 동시에 여러 선언이 만들어집니다. 아래 예제는 첫 번째 클래스의 Instance 타입입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
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());

여기서 greeter : Greeter라고 할 때 우리는 Greeter 클래스의 인스턴스 타입으로 Greeter를 사용합니다. 이것은 프로그램에서 클래스를 사용하는 두가지 방법중 하나입니다.

또한 생성자 함수(Constructor function)이라고 하는 또 다른 함수가 있습니다. 이것은 클래스의 인스턴스를 새로 만들 때 호출되는 함수입니다. 실제로 어떤 모습인지 보려면 위의 TypeScript로 생성한 JavaScript를 살펴 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
let Greeter = (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
return Greeter;
})();
let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

여기서 let Greeter는 생성자 함수를 할당 받게 될 것입니다. new를 호출하고 이 함수를 실행하면 우리는 클래스의 인스턴스를 얻습니다. 생성자 함수에는 클래스의 모든 정적 멤버도 포함됩니다. 클래스를 생각하는 또 다른 방법은 instance 측면과 static 측면이 있다는 것입니다.

이 차이를 보여주기 위해 예제를 약간 수정 해 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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());
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());

이 예에서greeter1은 이전과 비슷하게 작동합니다. 우리는 Greeter 클래스를 인스턴스화하고 이 객체를 사용합니다. 이것은 우리가 전에 보았습니다.

그런 다음 클래스를 직접 사용합니다. 여기서 우리는 greeterMaker 라는 새로운 변수를 생성합니다. 이 변수는 클래스 자체를 보유하거나 다른 방법으로 생성자 함수를 나타냅니다. 여기서 우리는 typeof Greeter를 사용합니다. 즉, 인스턴스 유형이 아닌 “Greeter 클래스 자체의 타입”을 사용합니다. 또는 더 정확하게 얘기하면 생성자 함수의 유형 (“Greeter”라는 심볼의 타입)입니다. 이 타입은 Greeter 클래스의 인스턴스를 생성하는 생성자와 함께 Greeter의 모든 정적 멤버를 포함합니다. 우리는 greeterMakernew를 사용하여 이것을 보여 주며, Greeter의 새로운 인스턴스를 생성하고 이전과 같이 호출합니다.

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

이전 섹션에서 말한 것처럼, 클래스 선언은 클래스의 인스턴스를 나타내는 타입과 생성자 함수 두 가지를 작성 해야합니다. 클래스는 타입을 생성하기 때문에 인터페이스를 사용할 수있는 동일한 장소에서 이 타입을 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};

이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://www.typescriptlang.org/docs/handbook/classes.html]

참고

공유하기