sungyup's.

typescript / TypeScript Basics / 1.4 Class and Interfaces

1.4Class and Interfaces

타입스크립트에서 class를 사용하는 방법 및 interface 키워드

TL;DR

추억의 쪽지 시험

타입스크립트의 Class

클래스는 타입스크립트가 아닌, (바닐라) 자바스크립트의 개념으로 객체들의 청사진이다. 마치 붕어빵 틀처럼 객체들을 찍어내기 위한 틀과 같이 작동한다고 볼 수 있는데, 예를 들어 name, age, id 등의 속성을 가진 User 클래스가 있다면 sungyup, snoopy, woodstock 등의 인스턴스는 이 User 클래스의 속성 값들에 대해 서로 다른 속성을 지닌 객체들인 것이다.

클래스는 보통 대문자로 시작하는 이름을 쓰고, class 키워드와 {}로 정의한다. 해당 클래스의 인스턴스를 만드는 함수는 클래스 안에 constructor라는 특수 메소드를 사용해 정의한다. 예를 들어, 아래는 자바스크립트에서 새로운 클래스를 정의하는 방법이다.

javascript
class User { constructor(n) { this.name = n } } const sungyup = new User('sungyup');

하지만 타입스크립트에선 상황이 다르다. 타입스크립트에는 해당 클래스에 name이라는 속성이 있는지부터 명확히 해줘야하기 때문에, 아래와 같이 어떤 키들이 어떤 타입의 값을 가지는지를 밝히고, 이후에야 constructor에서도 해당 키들에 접근할 수 있다.

typescript
class User { name: string; age: number; constructor(n: string, a: number){ this.name = n; this.age = a; } } const sungyup = new User('sungyup', 33);

public 키워드와 private 키워드

위의 코드는 아래와 같이 public, 또는 private를 통해 축약할 수 있다. 즉, 해당 객체의 키와 타입을 별도로 선언하는 대신 public 키워드를 통해 해당 파라미터들이 객체의 키임을 알릴 수 있다.

타입스크립트에서 제공하는 public 키워드는 객체의 밖에서도 접근할 수 있는 키임을 알리는 키워드다. 즉, 해당 객체의 밖에서 sungyup.name을 로깅한다거나 접근하는것이 가능하다. 반면 private 키워드는 객체의 밖에서는 접근할 수 없는 키임을 알리는 키워드다. 참고로, 바닐라 자바스크립트에선 private 키워드 대신 #라는 필드 문법을 사용한다.
typescript
class User { constructor(public name : string, public age: number){} } const sungyup = new User('sungyup', 33)

참고로, public인지 private인지의 접근 제어는 컴파일 시 검사된다. 런타임에선 이 키워드들과 관련없이 모두 일반 필드로 변환된다.

readonly 키워드

typescript
class User {
readonly hobbies: string[] = [];
constructor(public name : string, public age: number){}
greet(){
console.log('Hi, ', this.name);
}
}
const sungyup = new User('sungyup', 33);
sungyup.hobbies.push('coding'); // readonly인데 참조를 바꾸는게 아니므로 가능함
sungyup.hobbies = ['piano'] // 에러! readonly라서 재할당할수는 없음.

getter와 setter

private 키워드로 외부에서 접근을 못하면 대체 왜 만드는 것일까? 객체 내에서 로직을 통해 변형한 값을 접근하는 방법이 있기 때문이다. 예를 들어, 아래의 firstNamelastName은 모두 private로 선언되어 있지만 get 키워드로 메소드를 만들면 해당 속성들을 get 함수로 적절히 처리한 값들에 접근할 수 있게 된다.

typescript
class User{ constructor(private firstName: string, private lastName: string){} get fullName(){ return this.firstName + ' ' + this.lastName; } } const sungyup = new User('sungyup', 'joo'); console.log(sungyup.firstName); // 에러! private 메소드는 접근 못함 console.log(sungyup.fullName); //'sungyup joo'로 나옴

constructor없이 private 속성들에 접근하듯이 인스턴스를 만드는 방법이 있다. 아래와 같이 setter를 쓰면 된다. setter들은 메소드지만 마치 속성처럼 접근하는 식으로 사용된다.

typescript
class User{ private _firstName: string = ''; private _lastName: string = ''; set firstName(name: string) { if(name.trim() === ''){ throw new Error('Invalid first name.'); } this._firstName = name; } set lastName(name: string) { if(name.trim() === ''){ throw new Error('Invalid last name.'); } this._lastName = name; } get lastName() { return this._lastName; } get fullName(){ return this.firstName + ' ' + this.lastName; } } const sungyup = new User(); sungyup.firstName = 'sungyup'; sungyup.lastName = 'joo'

static 속성과 메소드

static은 클래스 자체(인스턴스가 아니라)에 있는 속성 또는 메소드를 정의할 때 쓰인다. 주로 해당 클래스의 유틸리티 함수들을 선언할 때 쓰인다.

typescript
class User { // ... static eid = 'USER'; static greet(){ console.log('Hello!') } } console.log(User.eid);

상속(Inheritance)

클래스는 확장이 가능하다. 즉, 해당 클래스보다 더 많은 속성을 가지고 있는 하위 클래스를 만들 수 있다. 자식(하위) 클래스는 extends 키워드를 통해 어떤 클래스를 상속하는지를 표시할 수 있다. 부모 클래스에 접근할땐 super 키워드를 쓰는데, 예를 들어 constructor에서 부모 클래스의 인스턴스를 만들고 그 외 추가적인 속성들을 만들려면 super()로 우선 부모 클래스의 인스턴스를 만든다.

typescript
class User {
constructor(public name: string, public age: number) {}
}
class Employee extends User {
constructor(name: string, age: number, public jobTitle: string){
super(name, age); // 먼저 호출 필수
}
// ...
}

protected modifier

자식 클래스에선 부모 클래스의 private에 접근 가능할까? 접근 가능하면 사적인(private)게 아니므로 접근 불가능하다.

하지만 부모-자식 간에는 공유하고 외부로만 접근을 막을 필요가 있는 속성이나 메소드가 있을 수 있다. 이럴 때 protected 키워드를 private 대신에 쓰면, 상속받는 클래스에선 쓸 수 있고 외부에선 접근할 수 없다.

typescript
class User { protected _firstName: string = ''; // ... } class Employee extends User { constructor(public jobTitle: string){ super(); // 부모 객체에 접근할 때 쓰는 키워드 } } const e = new Employee(); e._firstName; // 외부에서는 접근 불가
protected도 앞서 본 private처럼 컴파일 타임 제어이다. 자바스크립트로 번들링되면 해당 속성들은 일반 프로퍼티가 되어 보안 이슈가 있을 수도 있다. 따라서 진짜 런타임 프라이버시까 필요하면 자바스크립트의 #field를 쓰거나 클로저 패턴을 써야 한다.

abstract 클래스

앞서 살펴본 키워드들은 바닐라 자바스크립트에 존재했지만, abstract는 타입스크립트 전용 키워드다. abstract는 다른 클래스에 유틸리티 함수 등을 상속하기 위해서만 존재하는 클래스로, 인스턴스를 만들 수 없고 오로지 다른 클래스에 상속하기 위해서만 존재한다.

typescript
abstract class UIElement {
constructor(public identifier: string) {}
clone(targetLocation: string){
// ...
}
}
// let uiElement = new UIElement(); // 에러! 상속하는 부모 클래스만 될 수 있다.
class SideDrawerElement extends UIElement {
constructor(public identifier: string, public position: 'left' | 'right'){
super(identifier);
}
}

Interface

interface는 타입스크립트에서 객체의 타입을 정의하거나 implements 키워드로 객체에 형태를 정의해주기 위해 도입된 개념이다.

예를 들어, 아래는 Authenticatable이라는 interface이다.

typescript
interface Authenticatable { email: string; password: string; login(): void; logout(): void; }

이렇게 정의한 interface를 타입으로 써서 특정 변수가 해당 타입임을 선언하면, 그 변수는 interface에서 정의한 객체 구조를 따라야 한다.

typescript
let user: Authenticatable; user = { email: 'test@example.com', password: 'abc1', login(){ // 로그인 로직 구현 }, logout(){ // 로그아웃 로직 구현 } }

하지만 type 키워드로 정의하는 것과 무슨 차이가 있는걸까? 예를 들면, 위의 interface는 아래의 type와 동일하다.

typescript
type Authenticatable = { email: string; password: string; login(): void; logout(): void; }

약간 드문 용례지만, interface는 함수의 타입도 정의할 수 있는데 이 역시 type과 사실 다를게 없다.

javascript
type SumFn = (a: number, b: number) => number; interface SubtractFn { (a: number, b:number): number; }

따라서 개인 취향에 따라 쓰면 되지만, interfacetype과 차별화되는 문법들이 있으니 기억해둘 필요가 있다.

1. 선언 합치기 (Declaration Merging)

interface는 중복으로 쓰면 속성들을 합할 수 있다.

typescript
interface Authenticatable { email: string; } interface Authenticatable { password: string; } let user: Authenticatable; user = { email: 'abc@abc.com' // 에러! password 속성이 없음 }

2. implements로 형태 잡기

implements 키워드를 통해 클래스에 포함되어야 하는 속성들의 타입을 정의할 수 있다. 이 때, 클래스는 interface에 없는 속성도 추가적으로 쓸 수 있다.

typescript
class AuthenticatableUser implements Authenticatable {
constructor(
public email: string,
public password: string,
public userName: string, // interface에는 없었던 속성을 추가할 수 있다!
){}
login(){}
logout(){}
}

3. extends로 확장하기

앞서 살펴본 class처럼 interfaceextends 키워드로 보다 세부적인 하위 interface를 만들 수 있다. 이 하위 interface는 부모 interface의 모든 속성들을 상속받는다.

typescript
interface AuthenticatableAdmin extends Authenticatable { role: 'admin' | 'superadmin'; }

참고로, type도 이를 지원한다. type의 경우 &를 통해 합칠 수 있다.

typescript
type A = { x: number }; type B = { y: number }; type AB = A & B; // { x: number; y: number }