[Typescript] 10. 클래스
포스트
취소

[Typescript] 10. 클래스

Class

typescript를 쓰는 이유라고 할 수 있는 class이다.
javascript에서도 class문법이 이미 있긴 하지만 우리가 원하는 객체지향 프로그래밍 언어 수준까지는 지원해주지 않기 때문에 javascript에서의 class는 그저 좀 더 구체적인 객체처럼 다루기 위해서 쓰는 것에 불과했기 때문이다.

Field

1
2
3
4
5
class Point {
  x: number;
  y: number;
}
const pt = new Point();

객체의 타입 선언과 비슷한 문법으로 classfield 정의를 할 수 있다. (다만 위 코드는 오류가 날 것이다.)

초기화 생략

위 코드를 직접 실행 해봤다면 에러가 표시 되는 것을 확인할 수 있었을 것이다.
class를 구성하고 있는 변수들은 반드시 constructor에서 초기화를 진행해줘야 하기 때문이다.

만약 멤버 변수들의 정의만을 하고 싶을 뿐, constructor에서 초기화를 진행하고 싶지 않다면 확정 할당 연산자(Non-null assertion operator)를 사용한다. !

1
2
3
4
5
6
7
class Point {
  x!: number;
  y!: number;
}
const pt = new Point();

console.log(pt.x, pt.y); // undefined undefined

멤버 변수 이름 뒤에 !를 붙이면 constructor에서 초기화를 해야하는 의무를 선택사항으로 만들 수 있다.
다만 초기화를 진행하지 않으면 undefined가 나오게 된다.

Readonly

멤버 변수에 readonly 속성을 사용할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Greeter {
  readonly name: string = "world";
 
  constructor(otherName?: string) {
    if (otherName !== undefined) {
      this.name = otherName;
    }
  }
 
  err() {
    this.name = "not ok"; // Cannot assign to 'name' because it is a read-only property.
  }
}
const g = new Greeter();
g.name = "also not ok"; // Cannot assign to 'name' because it is a read-only property.

readonly 속성이 주어진 멤버 변수는 클래스 내부에서도, 외부에서도 수정이 불가능한 상태가 된다.

Constructor

어느 객체지향 언어처럼 생성자 역시 지원한다.

1
2
3
4
5
6
7
8
9
10
class Point {
  x: number;
  y: number;
 
  // Normal signature with defaults
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
}

constructor라는 이름으로 클래스 내에서 함수를 정의하면 해당 함수는 생성자로서 취급받는다.

생성자란?

const point: Point = new Point(); 처럼 new연산자를 통해 Instance를 생성할 때 실행되는 함수이다.
new Point(1, 2)와 같이 파라미터를 넘겨주면 constructor 함수를 통해 받을 수 있다.

Constructor overloading

1
2
3
4
5
6
7
8
class Point {
  // Overloads
  constructor(x: number, y: string);
  constructor(s: string);
  constructor(xs: any, y?: any) {
    // TBD
  }
}

일반 멤버 함수와 마찬가지로 constructor역시 Overloading이 가능하다.
필요하다면 Union Type도 사용할 수 있다.

Super call

다른 객체지향 언어를 써 본 사람이라면 역시 super도 이해하기 쉬울 것이다.
클래스는 exntends를 지원한다. 그렇다면 자식 클래스의 인스턴스를 생성하게 되면 부모 클래스의 constructor는 언제 실행되는 걸까?

1
2
3
4
5
6
7
class Base {
  constructor() {}
}
class Children extends Base {
  constructor() {}
}
const childrenInstance: Children = new Children();

정확히 말하면 부모 클래스의 constructor는 영원히 실행되지 않는다.
자식 클래스의 인스턴스를 생성한 것이지 부모의 인스턴스를 생성한 것이 아니기 때문이다.
다만 자식 클래스에서 부모 클래스의 생성자를 호출해야만 하는 상황이 생길 수가 있는데, 이럴 경우에 부모의 생성자를 호출하는 함수가 super함수이다.

1
2
3
4
5
6
7
8
9
class Base {
  k = 4;
}
 
class Derived extends Base {
  constructor() {
    super();
  }
}

super() 형태로 사용하면 부모의 constructor를 호출하게 된다. super를 실행하는 타이밍을 조정하는 식으로 부모 constructor의 실행 주기를 마음대로 할 수 있긴 하지만 가급적이면 constructor제일 첫 줄에 선언하는게 베스트이다.
super를 실행하지 않으면 부모의 멤버 변수를 참조할 수 없기 때문이다.

1
2
3
4
5
6
7
8
9
10
class Base {
  k = 4;
}
 
class Derived extends Base {
  constructor() {
    console.log(this.k); // Error
    super();
  }
}

Getter

Javascript에서 Array.length를 수정해 본 적이 있으면 알겠지만 실제로 값을 재정의 할 때는 아무런 에러가 나오지 않는데도 값을 재정의 한 다음 출력을 해보면 내가 정의한 값이 아닌 올바른 값이 나오는 것을 볼 수 있다.

1
2
3
4
const arr = [1, 2, 3];
console.log(arr.length); // 3;
arr.length = 777; // OK
console.log(arr.length); // 3;

이런 Attribute는 어떻게 만드는걸까?
답은 Getter이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Stack<T> {
  items: T[];
  constructor() {
    this.items = [];
  }
  public push(item: T) {
    this.items.push(item);
  }
  public get length(): number {
      return this.items.length;
  }
}

const stack: Stack<number> = new Stack();
console.log(stack.length); // 0
stack.push(1);
console.log(stack.length); // 1
stack.length = 777; // Cannot assign to 'length' because it is a read-only property.(2540)
console.log(stack.length);

함수 앞에 get 접근자를 붙이면 해당 함수 이름으로 속성을 꺼낼 때 get 접근자 함수 안에 있는 로직에 따라 값을 꺼내오게 된다.
하지만 javascript에서의 예제와는 다르게 read-only 이슈가 발생하여 수정이 되지는 않는다.

Setter

Getter는 원하는 값을 실제로 존재하지 않는 멤버 변수명을 통해 얻어올 수 있는 방법임을 알았다. 그러면 Setter도 대충 감이 올텐데, Setter대입 연산자(=)를 통해 Attribute에 값을 대입할 때, 정직하게 대입만 하는 것이 아닌 특정 로직을 거쳐서 값에 변조를 줄 때 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Square {
    x!: number;
    y!: number;
    public set square(area: number) {
        this.x = Math.sqrt(area);
        this.y = Math.sqrt(area);
    }
}

const rect: Square = new Square();
console.log(rect.x, rect.y); // undefined undefined
rect.square = 25;
console.log(rect.x, rect.y); // 5, 5

정사각형의 넓이를 입력받아서 xy의 길이를 구하는 클래스이다.
다만 한가지 다른 점이 있다면, xy를 직접 초기화하지 않았음에도 불구하고 xy의 값이 설정 되었다는 점이다.

set 설정자 통해 함수를 정의하면 해당 함수 이름을 속성처럼 사용할 수 있다.
속성에 값을 대입하면 그 값을 parameter로 넘기게 되고, set함수를 실행하게 되는 것이다.

Index Signatures

여기서 일반 객체와 마찬가지로 Index Signatures를 사용할 수 있다.

1
2
3
4
5
6
7
class MyClass {
  [s: string]: boolean | ((s: string) => boolean);
 
  check(s: string) {
    return this[s] as boolean;
  }
}

Class Heritage

Classinterface를 통해 멤버들의 범위를 정해줄 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Pingable {
  ping(): void;
}
 
class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}
 
class Ball implements Pingable {
  pong() { // Error.
    console.log("pong!");
  }
}

Overriding

다른 객체지향 언어처럼 Overriding을 지원한다.
부모 클래스가 갖고있는 멤버 함수와 동일한 이름으로 설정 하되, parametertype이나 갯수를 다르게 설정하는 방식으로 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
  greet() {
    console.log("Hello, world!");
  }
}
 
class Derived extends Base {
  greet(name?: string) {
    if (name === undefined) {
      super.greet();
    } else {
      console.log(`Hello, ${name.toUpperCase()}`);
    }
  }
}
 
const d = new Derived();
d.greet();
d.greet("reader");

super 키워드를 통해 부모의 메소드에 접근 할 수도 있다.

Relationship

A클래스와 B클래스가 서로 다르다는 것을 Typescript는 어떻게 구분할까?
일반 객체와 마찬가지로 Class 구성하는 멤버들을 비교하여 구분한다.

1
2
3
4
5
6
7
8
9
10
11
12
class Point1 {
  x = 0;
  y = 0;
}
 
class Point2 {
  x = 0;
  y = 0;
}
 
// OK
const p: Point1 = new Point2();

그렇기 때문에 위 코드가 성립 된다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.