1.4Class and Interfaces
타입스크립트에서 class를 사용하는 방법 및 interface 키워드
TL;DR
추억의 쪽지 시험
타입스크립트의 Class
클래스는 타입스크립트가 아닌, (바닐라) 자바스크립트의 개념으로 객체들의 청사진이다. 마치 붕어빵 틀처럼 객체들을 찍어내기 위한 틀과 같이 작동한다고 볼 수 있는데, 예를 들어 name, age, id 등의 속성을 가진 User 클래스가 있다면 sungyup, snoopy, woodstock 등의 인스턴스는 이 User 클래스의 속성 값들에 대해 서로 다른 속성을 지닌 객체들인 것이다.
클래스는 보통 대문자로 시작하는 이름을 쓰고, class
키워드와 {}
로 정의한다. 해당 클래스의 인스턴스를 만드는 함수는 클래스 안에 constructor
라는 특수 메소드를 사용해 정의한다. 예를 들어, 아래는 자바스크립트에서 새로운 클래스를 정의하는 방법이다.
javascriptclass User { constructor(n) { this.name = n } } const sungyup = new User('sungyup');
하지만 타입스크립트에선 상황이 다르다. 타입스크립트에는 해당 클래스에 name
이라는 속성이 있는지부터 명확히 해줘야하기 때문에, 아래와 같이 어떤 키들이 어떤 타입의 값을 가지는지를 밝히고, 이후에야 constructor
에서도 해당 키들에 접근할 수 있다.
typescriptclass 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
키워드 대신 #
라는 필드 문법을 사용한다.typescriptclass 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
키워드로 외부에서 접근을 못하면 대체 왜 만드는 것일까? 객체 내에서 로직을 통해 변형한 값을 접근하는 방법이 있기 때문이다. 예를 들어, 아래의 firstName
과 lastName
은 모두 private로 선언되어 있지만 get
키워드로 메소드를 만들면 해당 속성들을 get
함수로 적절히 처리한 값들에 접근할 수 있게 된다.
typescriptclass 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
들은 메소드지만 마치 속성처럼 접근하는 식으로 사용된다.
typescriptclass 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
은 클래스 자체(인스턴스가 아니라)에 있는 속성 또는 메소드를 정의할 때 쓰인다. 주로 해당 클래스의 유틸리티 함수들을 선언할 때 쓰인다.
typescriptclass 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
대신에 쓰면, 상속받는 클래스에선 쓸 수 있고 외부에선 접근할 수 없다.
typescriptclass 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이다.
typescriptinterface Authenticatable { email: string; password: string; login(): void; logout(): void; }
이렇게 정의한 interface
를 타입으로 써서 특정 변수가 해당 타입임을 선언하면, 그 변수는 interface
에서 정의한 객체 구조를 따라야 한다.
typescriptlet user: Authenticatable; user = { email: 'test@example.com', password: 'abc1', login(){ // 로그인 로직 구현 }, logout(){ // 로그아웃 로직 구현 } }
하지만 type
키워드로 정의하는 것과 무슨 차이가 있는걸까? 예를 들면, 위의 interface
는 아래의 type
와 동일하다.
typescripttype Authenticatable = { email: string; password: string; login(): void; logout(): void; }
약간 드문 용례지만, interface
는 함수의 타입도 정의할 수 있는데 이 역시 type
과 사실 다를게 없다.
javascripttype SumFn = (a: number, b: number) => number; interface SubtractFn { (a: number, b:number): number; }
따라서 개인 취향에 따라 쓰면 되지만, interface
는 type
과 차별화되는 문법들이 있으니 기억해둘 필요가 있다.
1. 선언 합치기 (Declaration Merging)
interface
는 중복으로 쓰면 속성들을 합할 수 있다.
typescriptinterface 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
처럼 interface
도 extends
키워드로 보다 세부적인 하위 interface를 만들 수 있다. 이 하위 interface는 부모 interface의 모든 속성들을 상속받는다.
typescriptinterface AuthenticatableAdmin extends Authenticatable { role: 'admin' | 'superadmin'; }
참고로, type
도 이를 지원한다. type
의 경우 &
를 통해 합칠 수 있다.
typescripttype A = { x: number }; type B = { y: number }; type AB = A & B; // { x: number; y: number }