프로그래밍 언어/JavaScript, TypeScript

[JavaScript] Class 없이 Prototype으로 상속 구현하기 (클래스 VS 프로토타입)

호무비 2024. 9. 29. 17:12
반응형

오늘은 JavaScript에서 Class 없이 Prototype으로 상속을 구현하는 방법에 관해 알아보겠습니다.

 

흔히 객체지향 언어를 공부하다 보면 class라는 개념을 만나게 될 텐데요! 사실 자바스크립트에서 class는 ES6에서 처음 도입되었습니다. 즉, 그 말은 그 이전까지는 class를 사용할 수 없었다는 것입니다. 그렇다면 ES6 이전에는 상속이나 객체지향 개념 없이 자바스크립트로 개발을 했다는 것일까요? 한번 지금부터 살펴보도록 하겠습니다.


자바스크립트의 상속(Inheritance)

 

자바스크립트에서는 일반적인 객체지향 언어와 다르게 프로토타입 상속을 한다고 많이들 들어보셨을 것입니다. 그러면서 항상 나오는 말이 클래스는 그냥 syntax sugar(문법적 첨가물)일 뿐이라고 합니다.

 

그 말은 즉, 자바스크립트에서는 prototype만 가지고도 우리가 알고 있는 class의 기능을 모두 구현 가능하다는 뜻입니다. 실제로 ES6 이전에는 프로토타입을 가지고 상속을 구현했습니다.

 

그래서 직접 class로 상속을 구현하는 방법과 prototype으로 상속을 구현하는 방법을 나란히 두고 한번 비교해 보도록 하겠습니다.


Class로 상속 구현하기

 

클래스로 상속을 구현하는 것은 자바스크립트에서 기본적으로 제공하는 기능이기 때문에 사실 특별히 해야 할 부분이 없습니다. 그냥 순리에 따라 코드를 작성해 주시면 됩니다.

 

먼저, 클래스 A를 만들고 생성자에 aaa, bbb 값을 초기화해 주었습니다. 또한 test 메소드를 정의했습니다.

 

class A {
    constructor() {
        this.aaa = 10;
        this.bbb = 5;
    }
    test() {
        console.log("hello");
    }
}

 

클래스 B는 A를 상속받았고, 생성자에서 ccc 값만 새로 추가하였습니다.

 

class B extends A {
    constructor() {
        super();
        this.ccc = 8;
    }
}

 

실행 결과를 보면 프로퍼티들이 잘 초기화되었고, 자식 객체에서도 부모 클래스의 메소드에 잘 접근 가능한 것을 확인할 수 있습니다.

 

class 코드 및 실행 결과

 

반응형

Prototype으로 상속 구현하기

 

위의 코드를 이번에는 class 키워드를 사용하지 않고, 순수 자바스크립트의 function과 prototype만 가지고 구현해 보겠습니다.

 

함수 A를 만들고, 마찬가지로 프로퍼티를 초기화했습니다. 그다음으로는 A의 프로토타입에서 test라는 프로퍼티에 함수를 대입해 줍니다.

 

function A() {
    this.aaa = 10;
    this.bbb = 5;
}
A.prototype.test = function test() {
    console.log("hello");
}

 

함수 B는 call 메서드를 통해 함수 A를 실행하고, 자신의 프로퍼티를 초기화합니다. 즉, A.call()은 super()와 동일한 역할을 하게 됩니다. class와 extends 키워드를 통해 상속 구조를 만들 때는 클래스 정의와 동시에 상속을 받지만, prototype 상속으로 구현하게 되면 함수 정의 이후 상속이 이루어지므로(B의 프로토타입에 A를 연결하는 부분) 명시적으로 함수 A의 call 메소드를 사용하였습니다.

 

function B() {
    A.call(this);
    this.ccc = 8;
}
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

 

함수 선언 뒷부분을 살펴보면 B의 프로토타입에 A의 프로토타입을 Object.create()로 생성하여 대입해 주는 것을 확인하실 수 있습니다. B.prototype = A.prototype처럼 직접 대입하지 않고 Object.create()를 사용하는 이유는 아래와 같은 차이가 발생하기 때문입니다.

 

Object.create() 사용(좌) / Object.create() 미사용(우)

 

각각의 방법으로 상속 구조를 생성한 이후 인스턴스 b와 A의 프로토타입을 순서대로 출력한 것입니다. 빨간 박스 표시 부분을 보면, 두 코드의 프로토타입 구조가 다른 것을 확인할 수 있습니다. 오른쪽을 보면, A와 B가 서로 프로토파입을 공유하게 되면서 B의 프로토타입에 적용한 수정 사항이 A에도 반영되는데, A의 프로토타입의 constructor가 f B()로 바뀐 것을 확인하실 수 있습니다. 하위 클래스의 수정이 상위 클래스에 영향을 미쳐서는 안 되므로, B의 프로토타입 역할을 해줄 Object를 따로 생성해 주어야 합니다. 이 때문에 Object.create()를 사용해야 프로토타입을 정확히 정의하고 제대로 된 상속 구조를 만들 수 있습니다.

 

추가로 B의 프로토타입의 생성자를 B로 초기화해 주는 것은 B의 프로토타입에 Object.create()로 생성한 객체를 저장하면서 기존의 constructor 정보가 사라졌기 때문입니다.

 

이제 실제 동작이 잘 되는지도 한번 살펴보겠습니다. 부모/자식 생성자가 실행되어 프로퍼티들이 잘 초기화되었고, 함수 A에서 정의한 test 메소드를 함수 B로 생성한 객체에서 잘 접근 가능한 것을 확인할 수 있습니다.

 

prototype 코드 및 실행 결과

 

만약 위의 코드에서 A.call()하는 것이 싫고 class의 super()를 흉내 내고 싶다면, 조금 더 복잡하지만, 아래와 같은 형태로 작성하실 수도 있습니다.

 

function B() {
    B.superClass.constructor.apply(this);
    this.ccc = 7;
}
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;
B.superClass = Object.create(A.prototype);

 

superClass 프로퍼티를 하나 만들어서 A를 프로토타입으로 가지는 객체를 저장합니다. 이렇게 되면 함수 B 안에서 B.superClass.constructor.apply(this)로 부모 생성자를 실행할 수 있고, super()와 유사한 효과를 낼 수 있습니다.

 

위의 코드를 한번 모듈화해서 사용한다는 가정하에 조금 정리해 보겠습니다. 아래와 같은 코드를 만들어서 extends(B, A)와 같이 호출해 주면 됩니다. (call 대신 superClass 사용하는 버전으로 작성했습니다.)

 

function extends(parent, child) {
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
    child.superClass = Object.create(parent.prototype);
}

 

또는, Object 객체를 직접 수정하는 것은 별로 바람직한 방법은 아니지만 아래와 같이 작성해 볼 수도 있을 것 같습니다. B.extends(A)와 같은 형태로 사용할 수 있습니다.

 

Object.prototype.extends = function extends(parent) {
    this.prototype = Object.create(parent.prototype);
    this.prototype.constructor = this;
    this.superClass = Object.create(parent.prototype);
};

 

다만 한가지 유의해야 할 점은, 자식의 prototype을 부모의 prototype이 덮어씌우는 로직이 포함되어 있어, extends 이후에 자식 클래스의 메소드를 선언해 주어야 한다는 것입니다.


class와 function의 차이

 

그렇다면 class 키워드는 정말 syntax sugar 역할만 하고 function을 사용하는 경우와 전혀 차이가 없을까요? 그렇지는 않습니다. 몇 가지 차이점을 확인할 수 있는데, 같이 한번 살펴보겠습니다.

 

1. 내부 구현 차이

 

class로 선언하게 되면 function 내부에 [[IsClassConstructor]]가 생성되어 new 키워드를 사용하지 않고는 호출이 불가능하게 됩니다. 그냥 function으로 선언하게 되면 new 키워드를 사용하지 않아도 호출이 가능합니다. (정상적으로 동작하지는 않습니다.)

 

class vs function new 키워드 비교

 

또한, 클래스의 prototype에 추가된 메서드는 enumerable이 false로 설정되어 for .. in 등의 구문에서 순회되지 않습니다. 아래 코드를 보면 클래스로 선언한 A의 test는 순회되지 않지만, 함수로 선언한 B의 test는 순회되고 있습니다.

 

class와 function의 메소드 enumerable 차이

 

2. 호이스팅(hosting) 여부

 

자바스크립트에서 함수를 선언하게 되면 기본적으로 호이스팅이 이루어지는데, 클래스는 호이스팅이 일어나지 않습니다.

 

호이스팅 미적용으로 발생하는 ReferenceError

 

결론적으로 class 선언 시 prototype과 함께 동작하는 것은 맞지만, 단순히 문법적으로 편리함만을 주는 것이 아니라 기능적으로 일부 변화가 있다는 점도 인지하고 사용하는 것이 바람직할 것 같습니다.


사실 그냥 class를 사용하거나 TypeScript의 type, interface 등을 사용하다 보면, Prototype을 만져보고 조작할 일이 거의 없기 때문에 깊게 공부한다고 실무적으로 큰 의미가 있는 것은 아니지만, 자바스크립트의 밑단에서 어떻게 코드가 동작하고 있는지 알아볼 수 있는 재밌는 시간이었습니다.

 

클래스와 상속에 관해서만 주로 다뤘는데, 다음에 기회가 된다면 Prototype도 한번 자세히 이야기 나눠볼 수 있으면 좋겠습니다.

 

직접 조사해서 작성하는 글이다 보니 일부 정확하지 않은 정보가 포함되어 있을 수 있습니다.

궁금한 사항이나 잘못된 내용이 있으면 댓글로 알려주세요~

구독과 좋아요, 환영합니다!

 

반응형