📦

[JS] 객체지향 프로그래밍(Object-Oriented Programming) - 2


클래스 표기법

이전 글에서 자바스크립트의 클래스는 'prototype 속성이 있는 constructor 함수'인 것을 알아봤다. 하지만 프로토타입 함수를 작성하고, 거기에 프로토타입 속성을 또 따로 작성해서 넣어주고 하니 너무 복잡하고 읽기가 어렵다...(나는 그렇게 느꼈다ㅎㅎ;).

이걸 개선해서 나온 표기법이 class 키워드다.

1class Rabbit {
2 constructor(type) {
3 this.type = type;
4 }
5 spaek(line) {
6 console.log(`The ${this.type} rabbit says '${line}'`);
7 }
8}
9let whiteRabbit = new Rabbit("white");

Rabbit이라는 이름을 가진 함수가 만들어진다. 함수 본문은 constructor에서 가져온다. 그리고 speak같은 클래스 내의 메서드는 Rabbit.prototype에 저장된다.

class 키워드는 클래스 선언을 뜻한다. 클래스의 body 안에 생성자와 메서드를 모두 정의할 수 있다.

constructor는 우리가 이전 포스트에서 작성한 constructor 함수와 동일하고, Rabbit이라는 이름에 바인딩된다. 나머지 메서드는 constructor.prototype(=Rabbit.prototype)에 포함된다. 설명을 보면 표기법만 다를 뿐, 앞서 알아본 클래스와 동일한 것임을 알 수 있다.

물론 class는 단순 편의 문법이 아니다. class는 다음과 같은 특징을 가진다.

  • class로 만든 함수엔 특수 내부 속성인 [[IsClassConstructor]]: true가 붙는다.
  • 클래스에 정의된 메서드는 열거할 수 없다(non-enumerable).
  • 클래스는 항상 '엄격 모드'로 실행된다.

클래스 필드

필드 선언은 자바스크립트 표준화 위원회에 실험적 기능으로 제안되어 있다. 현재 지원하는 브라우져가 제한적이니 주의하자.

클래스를 정의할 때 '<프로퍼티 이름> = <값>'을 써주면 간단히 클래스 필드를 만들 수 있다. 만들어진 필드는 Class.prototype이 아닌 개별 객체에 설정된다.

1class User {
2 name = "woong-jae";
3 sayHi() {
4 console.log(`${this.name}님 안녕하세요!`);
5 }
6}
7new User().sayHi();
8// -< woong-jae님 안녕하세요!
9console.log(User.prototype.name);
10// -> undefined

필드 선언 앞에 #을 붙이면 그 필드는 'private' 필드가 된다. Private 필드는 클래스 내부에서만 읽고 쓰기가 가능하다.

Getter, Setter

안터페이스는 대부분 함수로 되어있지만, Array.length 같이 함수가 아닌 값을 속성으로 가질 수 있다. 이런 경우 length와 같은 속성은 미리 계산해서 저장하는 방법도 있지만, 메서드로 숨기는 방법이 더 좋다.

아래 코드를 보자.

1let varySize = {
2 get size() {
3 return Math.floor(Math.random() * 100);
4 }
5}
6console.log(varySize.size);
7// -> 73
8console.log(varySize.size);
9// -> 49

size라는 속성을 읽으면 관련된 메서드가 호출된다. 이런 메서드를 'getter'라고 부른다. 객체나 클래스의 메서드 이름 앞에 get을 붙여주면 정의할 수 있다.

속성에 기록 할 때 'setter'를 사용해서 비슷한 작업을 수행할 수 있다.

1class Temperature {
2 constructor(celsius) {
3 this.celsius = celsius;
4 }
5 get fahrenheit() {
6 return this.celcius * 1.8 + 32;
7 }
8 set fahrenheit(value) {
9 this.celcius = (value - 32) / 1.8;
10 }
11 static fromFahrenheit(value) {
12 return new Temperature((value - 32) / 1.8);
13 }
14}

Temperature 클래스는 온도를 섭씨나 화씨로 읽고 쓸 수 있게 한다. 섭씨는 내부에 저장하고, 화씨는 getter와 setter로 사용할 수 있다.

Statics

필요에 따라 프로토타입이 아닌 생성자 함수에 일부 속성을 직접 추가할 수도 있다.

클래스 선언에서 이름 앞에 'static' 키워드가 붙어있는 메서드는 생성자에 포함된다(정적 메서드/속성). 메서드를 프로퍼티 형태로 직접 할당하는 것과 같은 효과를 낸다.

1class User {
2 static staticMethod() {
3 console.log(this === User);
4 }
5}
6User.staticMethod();
7// -> true

상속

네모, 동그라미, 세모 같은 도형들을 클래스로 만들고 싶을 때, 어떻게 만들면 덜 귀찮게 만들 수 있을까? 네모, 동그라미, 세모는 색상, 너비와 높이를 가지는 도형(Shape)이니까, 이 속성들을 도형에 정의하고 재사용하면 반복되는 코드를 줄여 비교적 쉽게 만들 수 있을 것 같다.

자바스크립트의 프로토타입 시스템을 사용하면 기존 클래스와 비슷하지만 일부 속성을 새롭게 정의해 새로운 클래스를 만들 수 있다. 이것을 '상속'이라고 한다.

1class Shape {
2 constructor(width, height, color) {
3 this.width = width;
4 this.height = height;
5 this.color = color;
6 }
7 draw() {
8 console.log(`drawing ${this.color} of`);
9 }
10 getArea() {
11 return this.width * this.height;
12 }
13}
14
15class Triangle extends Shape {
16 draw() {
17 super.draw();
18 console.log(", Triangle");
19 }
20 getArea() {
21 return (this.width * this.height) / 2;
22 }
23}

extends 키워드로 만들어진 클래스를 'subclass(서브 클래스)'라고 하고 기존 클래스를 'superclass(슈퍼 클래스)'라고 한다.

extends는 프로토타입을 기반으로 동작한다. extendsTriangle.prototype.[[Prototype]]Shape.prototype으로 설정한다. 그리고 Triangle[[Prototype]]Shape이 된다. 그렇기 때문에 클래스의 일반 메서드와 정적 메서드를 상속할 수 있는 것이다.

개발을 하다보면 부모 메서드를 호출하고 싶을 때가 있을 것이다. 이때 super 키워드를 사용할 수 있다.

  • super.method(...)는 슈퍼 클래스의 메서드를 호출한다.
  • super(...)는 슈퍼 클래스의 생성자를 호출한다.
  • 메서드는 내부 속성 [[HomeObject]]을 통해 자신이 정의된 클래스와 객체를 기억한다.

위 코드를 보면 drawgetArea 메서드를 재정의해서 사용하고 있다. 이것을 '메서드 오버라이딩(method overriding)'이라고 한다.

생성자 오버라이딩은 좀 더 까다롭다. 클래스를 상속할 때 생성자를 정의하지 않으면 비어있는 생성자가 만들어진다. 위의 Triangle 클래스에 아래와 같은 일이 자동으로 일어난다.

1class Triangle extends Shaoe {
2 constructor(...args) {
3 super(...args);
4 }
5 // ...
6}

생성자는 기본적으로 슈퍼 클래스의 생성자를 호출한다. 생성자를 오버라이딩 하기 위해서는 생성자에 반드시 super(...)를 호출해야 한다. super(...)this를 사용하기 전에 반드시 호출해야 한다.

자바스크립트는 상속 클래스의 생성자 함수와 그렇지 않은 함수를 구분한다. 상속 클래스의 생성자 함수엔 특수 내부 속성인 [[ConstructorKind]]:"derived"가 붙는다. 상속 클래스의 생성자 함수가 실행되면, 빈 객체가 만들어지고 this에 이 객체를 할당하는 일을 부모 클래스의 생성자가 처리해주길 기대한다.

그렇기 때문에 상속 클래스의 생성자에서 super를 호출해야 한다. 아니면 this가 될 객체가 만들어지지 않아 에러가 발생한다.

instanceof

instanceof 연산자를 사용해서 객체가 어떤 클래스로부터 파생됐는지 알 수 있다. 대상의 프로토타입 체인을 거슬러 올라가며 확인한다.

1console.log(new Triangle(2, 2, "blue") instanceof Triangle);
2// -> true
3console.log(new Triangle(2, 2, "blue") instanceof Shape);
4// -> true
5console.log([1] instanceof Array);
6// -> true

심볼(Symbol)

속성의 이름은 보통 문자열이지만, 경우에 따라선 'symbol'일 수도 있다. 심볼은 Symbol 함수로 만든 값이다. 문자열과 달리 새롭게 생성된 심볼은 고유하며 동일한 심볼을 두 번 만들 수 없다.

1let sym = Symbol("name");
2console.log(sym == Symbol("name"));
3// -> false

Symbol의 매개변수로 넘겨준 문자열은 toString을 호출하면 출력된다. 하지만 그 이상의 의미를 가지진 않는다.

심볼은 속성 이름으로 사용할 수 있고 고유하기 때문에, 어떤 이름을 사용하더라도 다른 속성과 충돌하지 않는 인터페이스를 정의할 수 있다.

1const toStringSymbol = Symbol("toString");
2Array.prototype[toStringSymbol] = function () {
3 return `${this.length} cm of blue yarn`;
4};
5
6console.log([1, 2].toString());
7// -> 1, 2
8console.log([1, 2][toStringSymbol]());
9// -> 2 cm of blue yarn

심볼을 대괄호로 감싸서 객체 표현식클래스에 심볼 속성을 포함시킬 수 있다. 대괄호를 사용해 속성에 접근하는 표기법으로 해당 심볼이 포함된 바인딩을 참조할 수 있다.

1let stringObejct = {
2 [toStringSymbol]() { return "a jute rope"; }
3}
4console.log(stringObject[toStringSymbol]());
5// -> a jute rope

참조:
Eloquent JavaScript
ko.javascript.info