ES6. Class

vancouver·2024년 1월 28일

javascript.info 정리

목록 보기
1/19

클래스 상속

클래스 상속을 사용하면 클래스를 다른 클래스로 확장할 수 있습니다.

기존에 존재하던 기능을 토대로 새로운 기능을 만들 수 있죠.

extends 키워드


class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  run(speed) {
    this.speed = speed;
    alert(`${this.name} 은/는 속도 ${this.speed}로 달립니다.`);
  }
  stop() {
    this.speed = 0;
    alert(`${this.name} 이/가 멈췄습니다.`);
  }
}

let animal = new Animal("동물");


// Animal 클래스 확장 생성
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} 이/가 숨었습니다!`);
  }
}

let rabbit = new Rabbit("흰 토끼");

rabbit.run(5); // 흰 토끼 은/는 속도 5로 달립니다.
rabbit.hide(); // 흰 토끼 이/가 숨었습니다

super 키워드

메서드 오버라이딩

이제 한발 더 나아가 메서드를 오버라이딩 해봅시다. 특별한 사항이 없으면 class Rabbit은 class Animal에 있는 메서드를 ‘그대로’ 상속받습니다.

그런데 Rabbit에서 stop() 등의 메서드를 자체적으로 정의하면, 상속받은 메서드가 아닌 자체 메서드가 사용됩니다.

  • 개발을 하다 보면 부모 메서드 전체를 교체하지 않고, 부모 메서드를 토대로 일부 기능만 변경하고 싶을 때가 생깁니다. 부모 메서드의 기능을 확장하고 싶을 때도 있죠. 이럴 때 커스텀 메서드를 만들어 작업하게 되는데, 이미 커스텀 메서드를 만들었더라도 이 과정 전·후에 부모 메서드를 호출하고 싶을 때가 있습니다.
    이것을 해결하기 위해 super키워드를 사용합니다.
  • super.method(...)는 부모 클래스에 정의된 메서드, method를 호출합니다.
  • super(...)는 부모 생성자를 호출하는데, 자식 생성자 내부에서만 사용 할 수 있습니다.

예시

// Animal 클래스 확장 생성
class Animal {
    constructor(name) {
        this.speed = 0;
        this.name = name;
    }
    run(speed) {
        this.speed = speed;
        console.log(`${this.name}가 속도 ${this.speed}로 달립니다.`)
    }
    stop() {
        this.speed - 0;
        console.log(`${this.name}가 멈췄습니다.`)
    }
}

class Rabbit extends Animal {
    hide() {
        console.log(`${this.name}가 숨었습니다.`)
    }

    stop() {
        // Animal클래스의 stop()이 실행이안되고,
        // 이 메소드가 실행이됨.
    }
}

let rabbit = new Rabbit("토끼");
rabbit.run(5); // 토끼가 속도 5로 달립니다.
rabbit.stop(); // 
class Animal {
    constructor(name) {
        this.speed = 0;
        this.name = name;
    }
    run(speed) {
        this.speed = speed;
        console.log(`${this.name}가 속도 ${this.speed}로 달립니다.`)
    }
    stop() {
        this.speed - 0;
        console.log(`${this.name}가 멈췄습니다.`)
    }
}

class Rabbit extends Animal {
    hide() {
        console.log(`${this.name}가 숨었습니다.`)
    }

    stop() {
        super.stop(); // Animal클래스의 stop()을 실행 후 다음 실행
        this.hide(); // Rabbit클래스의 hide()가 실행.
    }
}

let rabbit = new Rabbit("토끼");
rabbit.run(5); //토끼가 속도 5로 달립니다.
rabbit.stop(); //토끼가 멈췄습니다.
                //토끼가 숨었습니다.

화살표 함수엔 super가 없습니다.

화살표 함수 다시 살펴보기에서 살펴본 바와 같이, 화살표 함수는 super를 지원하지 않습니다.
super에 접근하면 아래 예시와 같이 super를 외부 함수에서 가져옵니다.

class Rabbit extends Animal {
  stop() {
    setTimeout(() => super.stop(), 1000); // 1초 후에 부모 stop을 호출합니다.
  }
}

화살표 함수의 super는 stop()의 super와 같아서 위 예시는 의도한 대로 동작합니다. 그렇지만 setTimeout안에서 ‘일반’ 함수를 사용했다면 에러가 발생했을 겁니다.

// Unexpected super
setTimeout(function() { super.stop() }, 1000);

생성자 오버라이딩

생성자 오버라이딩은 좀 더 까다롭습니다.

지금까진 Rabbit에 자체 constructor가 없었습니다.

명세서에 따르면, 클래스가 다른 클래스를 상속받고 constructor가 없는 경우엔 아래처럼 ‘비어있는’ constructor가 만들어집니다.

class Rabbit extends Animal {
  // 자체 생성자가 없는 클래스를 상속받으면 자동으로 만들어짐
  constructor(...args) {
    super(...args);
  }
}

보시다시피 생성자는 기본적으로 부모 constructor를 호출합니다. 이때 부모 constructor에도 인수를 모두 전달합니다. 클래스에 자체 생성자가 없는 경우엔 이런 일이 모두 자동으로 일어납니다.

이제 Rabbit에 커스텀 생성자를 추가해보겠습니다. 커스텀 생성자에서 nameearLength를 지정해보겠습니다.

class Animal {
    constructor(name) {
        this.speed = 0;
        this.name = name;
    }
    run(speed) {
        this.speed = speed;
        console.log(`${this.name}가 속도 ${this.speed}로 달립니다.`)
    }
    stop() {
        this.speed - 0;
        console.log(`${this.name}가 멈췄습니다.`)
    }
}

class Rabbit extends Animal {
    constructor(name, earLength) {
        this.name = name; // ERROR!  ---> super(name);
        this.earLength = earLength;
    }
}

let rabbit = new Rabbit("흰토끼", 10)
//ReferenceError: 
//Must call super constructor in derived class before accessing 
//'this' or returning from derived constructor
  • 상속 클래스의 생성자에선 반드시 super(...)를 호출해야 하는데, super(...)를 호출하지 않아 에러가 발생했습니다. super(...)는 this를 사용하기 전에 반드시 호출해야 합니다.

상속 클래스의 생성자가 호출될 때 어떤 일이 일어나는지 알아보며 이유를 찾아봅시다.

자바스크립트는 '상속 클래스의 생성자 함수(derived constructor)'와 그렇지 않은 생성자 함수를 구분합니다. 상속 클래스의 생성자 함수엔 특수 내부 프로퍼티인 [[ConstructorKind]]:"derived"가 이름표처럼 붙습니다.

일반 클래스의 생성자 함수와 상속 클래스의 생성자 함수 간 차이는 new와 함께 드러납니다.

일반 클래스가 new와 함께 실행되면, 빈 객체가 만들어지고 this에 이 객체를 할당합니다.
반면, 상속 클래스의 생성자 함수가 실행되면, 일반 클래스에서 일어난 일이 일어나지 않습니다. 상속 클래스의 생성자 함수는 빈 객체를 만들고 this에 이 객체를 할당하는 일을 부모 클래스의 생성자가 처리해주길 기대합니다.
이런 차이 때문에 상속 클래스의 생성자에선 super를 호출해 부모 생성자를 실행해 주어야 합니다. 그렇지 않으면 this가 될 객체가 만들어지지 않아 에러가 발생합니다.

아래 예시와 같이 this를 사용하기 전에 super()를 호출하면 Rabbit의 생성자가 제대로 동작합니다.

class Rabbit extends Animal {
    constructor(name, earLength) {
        super(name);
        this.earLength = earLength;
    }
}

let rabbit = new Rabbit("흰 토끼", 10);
alert(rabbit.name); // 흰 토끼
alert(rabbit.earLength); // 10

[[HomeObject]]

자바스크립트엔 이런 문제를 해결할 수 있는 함수 전용 특수 내부 프로퍼티가 있습니다. 바로 [[HomeObject]]입니다.

클래스이거나 객체 메서드인 함수의[[HomeObject]] 프로퍼티는 해당 객체가 저장됩니다.

super는[[HomeObject]]를 이용해 부모 프로토타입과 메서드를 찾습니다.

예시를 통해 [[HomeObject]]가 어떻게 동작하는지 살펴봅시다. 먼저 일반 객체를 이용해 보겠습니다.

let animal = {
  name: "동물",
  eat() {         // animal.eat.[[HomeObject]] == animal
    alert(`${this.name} 이/가 먹이를 먹습니다.`);
  }
};

let rabbit = {
  __proto__: animal,
  name: "토끼",
  eat() {         // rabbit.eat.[[HomeObject]] == rabbit
    super.eat();
  }
};

let longEar = {
  __proto__: rabbit,
  name: "귀가 긴 토끼",
  eat() {         // longEar.eat.[[HomeObject]] == longEar
    super.eat();
  }
};

// 이제 제대로 동작합니다
longEar.eat();  // 귀가 긴 토끼 이/가 먹이를 먹습니다.

[[HomeObject]]의 메커니즘 덕분에 메서드가 의도한 대로 동작하는 것을 확인해 보았습니다. 이렇게 longEar.eat같은 객체 메서드는 [[HomeObject]]를 알고 있기 때문에 this 없이도 프로토타입으로부터 부모 메서드를 가져올 수 있습니다.

메서드는 자유롭지 않습니다

자바스크립트에서 함수는 대개 객체에 묶이지 않고 ‘자유롭습니다’. 이런 자유성 때문에 this가 달라도 객체 간 메서드를 복사하는 것이 가능하죠.

그런데 [[HomeObject]]는 그 존재만으로도 함수의 자유도를 파괴합니다. 메서드가 객체를 기억하기 때문입니다. 개발자가 [[HomeObject]]를 변경할 방법은 없기 때문에 한 번 바인딩 된 함수는 더 이상 변경되지 않죠.

다행인 점은 [[HomeObject]]는 오직 super 내부에서만 유효하다는 것입니다. 그렇기 때문에 메서드에서 super를 사용하지 않는 경우엔 메서드의 자유성이 보장됩니다. 객체 간 복사 역시 가능하죠. 하지만 메서드에서 super를 사용하면 이야기가 달라집니다.

객체 간 메서드를 잘못 복사한 경우에 super가 제대로 동작하지 않는 경우를 살펴봅시다

let animal = {
  sayHi() {
    console.log(`나는 동물입니다.`);
  }
};

// rabbit은 animal을 상속받습니다.
let rabbit = {
  __proto__: animal,
  sayHi() {
    super.sayHi();
  }
};

let plant = {
  sayHi() {
    console.log("나는 식물입니다.");
  }
};

// tree는 plant를 상속받습니다.
let tree = {
  __proto__: plant,
  sayHi: rabbit.sayHi // (*) 
//  "plant를 상속 받았기 때문에 "나는 식물입니다"라는 출력을 기대.(틀린 상황)
};

tree.sayHi();  // 나는 동물입니다. (?!?)

tree.sayHi()를 호출하니 "나는 동물입니다."가 출력됩니다. 뭔가 잘못된 것이 분명해 보이네요.

원인은 꽤 단순합니다.

(*)로 표시한 줄에서 메서드 tree.sayHi는 중복 코드를 방지하기 위해 rabbit에서 메서드를 복사해왔습니다.
그런데 복사해온 메서드는 rabbit에서 생성했기 때문에 이 메서드의 [[HomeObject]]는 rabbit입니다, 개발자는 [[HomeObject]]를 변경할 수 없습니다.
tree.sayHi()의 코드 내부엔 super.sayHi()가 있습니다. rabbit의 프로토타입은 animal이므로 super는 체인 위에있는 animal로 올라가 sayHi를 찾습니다.
일련의 과정을 그림으로 나타내면 다음과 같습니다.

함수 프로퍼티가 아닌 메서드 사용하기

[[HomeObject]]는 클래스와 일반 객체의 메서드에서 정의됩니다. 그런데 객체 메서드의 경우 [[HomeObject]]가 제대로 동작하게 하려면 메서드를 반드시 method() 형태로 정의해야 합니다.method: function()형태로 정의하면 안 됩니다.

개발자 입장에선 두 방법의 차이는 그리 중요하지 않을 수 있지만, 자바스크립트 입장에선 아주 중요합니다.

메서드 문법이 아닌(non-method syntax) 함수 프로퍼티를 사용해 예시를 작성해 보면 다음과 같습니다. [[HomeObject]] 프로퍼티가 설정되지 않기 때문에 상속이 제대로 동작하지 않는 것을 확인할 수 있습니다.

let animal = {
  eat: function() { // 'eat() {...' 대신 'eat: function() {...'을 사용해봅시다.
    // ...
  }
};

let rabbit = {
  __proto__: animal,
  eat: function() {
    super.eat();
  }
};

rabbit.eat();  // SyntaxError: 'super' keyword unexpected here ([[HomeObject]]가 없어서 에러가 발생함)

함수 프로퍼티가 아닌 메서드를 이용할 때 super()가 먹히지 않음.
만약 정상적으로 출력을 하려면 super.eat(); 이 아닌 animal.eat(); 으로 수정해야함.

요약

  1. 클래스 확장하기: class Child extends Parent
  • Child.prototype.__proto__Parent.prototype이 되므로 메서드 전체가 상속됩니다.
  1. 생성자 오버라이딩:
  • this를 사용하기 전에 Child 생성자 안에서 super()로 부모 생성자를 반드시 호출해야 합니다.
  1. 메서드 오버라이딩:
  • Child에 정의된 메서드에서 super.method()를 사용해 Parent에 정의된 메서드를 사용할 수 있습니다.
  1. super 키워드와 [[HomeObject]]
  • 메서드는 내부 프로퍼티 [[HomeObject]]에 자신이 정의된 클래스와 객체를 기억해놓습니다.
  • super는 [[HomeObject]]를 사용해 부모 메서드를 찾습니다.
  • 따라서 super가 있는 메서드는 객체 간 복사 시 제대로 동작하지 않을 수 있습니다.
    추가 사항:

추가사항:

  • 화살표 함수는 this나 super를 갖지 않으므로 주변 컨텍스트에 잘 들어맞습니다.

REFERECNE

https://ko.javascript.info/class-inheritance

0개의 댓글