본문 바로가기
👩‍💻 Programming/JavaScript

[코어 자바스크립트] 7장 클래스

by codingBear 2022. 12. 8.
728x90
반응형

01 클래스와 인스턴스의 개념 이해

👨‍💻 자바스크립트의 클래스

👉 자바스크립트는 프로토타입 기반 언어라서 '상속' 개념이 존재하지 않는다. ES6에서 클래스 문법이 추가되었는데 일정 부분 프로토타입을 활용하기 때문에 ES5의 클래스를 흉내내기 위한 구현 방식을 학습해두어야 한다.

 

👉 음식은 과일보다 상위의(superior) 개념이고, 과일은 음식보다 하위의(subordinate) 개념이다. 이를 상위 클래스(superclass)/하위 클래스(subclass)로 표현한다.

 

💙 과일 분류 하위에 또 다른 분류가 있을 경우 클래스 간의 관계

음식 과일의 superclass, 귤류의 super-super class
과일 음식의 subclass, 귤류의 superclass
귤류 음식의 sub-subclass, 과일의 subclass

👉 표를 보면 알 수 있듯 하위 개념은 상위 개념에 더 구체적인 개념이 추가된다.

 

💙 인스턴스?

👉 위 예제의 감귤, 자몽, 천혜향처럼 어떤 클래스(귤류)의 속성을 띠는 구체적인 개체 인스턴스(instance)라고 한다. 

👉 위 문장을 달리 말하자면 '어떤 조건에 부합하는 구체적인 예시(instance)'가 될 텐데 여기서 조건은 클래스라 본다면 어떤 클래스에 속한 개체는 해당 클래스의 조건을 모두 만족하므로 그 클래스의 구체적인 예시, 즉 인스턴스가 되는 것이다.

 

💙 프로그래밍 언어에서의 클래스?

👉 현실 세계에서와는 달리 사용자가 직접 클래스를 정의해야 하고, 클래스를 바탕으로 인스턴스가 생성된다. 클래스를 바탕으로 인스턴스를 만들 때 비로소 어떤 개체가 클래스의 속성을 지닌다.

👉 한 인스턴스는 하나의 클래스만을 바탕으로 만들어진다. 다중상속을 지원하는 언어든 그렇지 않은 언어든 인스턴스를 생성할 때는 하나의 클래스밖에 호출하지 못한다.

👉 현실 세계의 클래스는 추상적인 개념이지만, 프로그래밍 언어의 클래스는 사용하기에 따라 추상적인 대상이기도 하고 구체적인 개체이기도 하다.


02 자바스크립트의 클래스

 

👉 자바스크립트에는 클래스의 개념이 존재하지 않는다. 따라서 프로토타입을 활용하여 클래스와 유사한 기능을 구현하는 것이다.

👉 new 연산자로 생성자 함수 Array를 호출하면 인스턴스가 생성된다. 이 Array를 일종의 클래스라 하면 Array의 prototype 객체 내부 요소들이 인스턴스에 프로토타입 체이닝을 통해 '참조'되는데 이를 인스턴스에 '상속'된다고도 볼 수 있다. 이때 Array 내부 프로퍼티들 중 prototype 프로퍼티만 상속되고 나머지는 상속되지 않는다. 인스턴스에 상속되는지(인스턴스가 참조하는지) 여부에 따라 스태틱 멤버(static member)인스턴스 멤버(instance member)로 나뉜다.

👉 클래스 기반 언어와는 달리 자바스크립트에서는 인스턴스에서도 직접 메서드를 정의할 수 있는데 이를 프로토타입 메서드(prototype method)라고 한다.

 

💙 클래스 관점에서 바라본 프로토타입 시스템

👉 프로토타입 객체에 할당한 메서드는 인스턴스가 자신의 것처럼 호출할 수 있다. 13번째 줄에서 호출한 getArea는 실제로는 rect1.__proto__.getArea에 접근하는데 예제에서는 __proto__를 생략하여 this가 rect1인 채로 실행되고 따라서 결괏값으로 rect1.width * rect1.height 반환하는 것이다. 이처럼 인스턴스에서 직접 호출할 수 있는 메서드를 프로토타입 메서드라고 한다.

👉 14번째 줄은 rect1 인스턴스에서 isRectangle이라는 메서드에 접근한다. 해당 메서드는 rect1, rect1.__proto__, rect1.__proto__.__proto__(Object.prototype) 어디에도 없다. 따라서 undefined를 반환한다. 이처럼 인스턴스에서 직접 접근할 수 없는 메서드를 스태틱 메서드라고 한다. 스태틱 메서드는 15번째 줄처럼 생성자 함수를 this로 해야만 호출 가능하다.

 

🔵 "프로그래밍 언어의 클래스는 사용하기에 따라 추상적인 대상이기도 하고 구체적인 개체이기도 하다."

👉 구체적인 인스턴스가 사용할 메서드를 정의한 '틀'의 역할을 할 때 클래스는 '추상적인 대상' → 5번째 줄 Rectnagle.prototype.getArea

👉 클래스 자체를 this로 해서 직접 접근해야 하는 스태틱 메서드를 호출할 때 클래스는 '구체적인 개체'  8번째 줄 Rectangele.isRectangle


03 클래스 상속

7-3-1 기본 구현

👉 객체지향에서 클래스 상속은 가장 중요한 요소이다. 이 때문에 클래스 문법이 없던 ES5까지의 자바스크립트 사용자들은 클래스 상속을 최대한 흉내내는 것이 주요한 관심사였다. 앞으로 클래스 문법을 사용하지 않고 자바스크립트에서 클래스를 구현했던 다양한 방법들을 살펴보겠다.

 

👉 위의 예제가 클래스 상속의 핵심이다. 자바스크립트에서 클래스 상속을 구현했다는 것은 결국 프로토타입 체이닝을 잘 했다는 말이다.

👉 다만 위 예제에는 몇 가지 문제가 있다. length 프로퍼티가 configurable(삭제 가능)하다는 점과 Grade.prototype이 빈 배열을 참조한다는 점이다.

 

🔵 length 프로퍼티가 configurable한 문제

👉 12번째 줄에서 length 프로퍼티를 삭제하고 다시 push를 하니 push한 값이 맨 뒤가 아닌 0번째 인덱스에 들어갔고, lengoth가 1이 되었다. 내장 객체인 배열 인스턴스의 length 프로퍼티configurable 속성이 false라서 삭제가 불가능하지만, Grade 클래스의 인스턴스는 배열 메서드를 상속하나 기본적으로 일반 객체의 성질을 그대로 가지고 있어 삭제가 가능해서 생기는 문제이다.

👉 한편 push를 했을 때 0번째 인덱스에 70이 들어가고 length가 다시 1이 되는 것은 왜일까? 바로 g.__proto__ 즉 Grade.prototype이 빈 배열을 가리키고 있기 때문이다. push 명령에 의해 자바스크립트 엔진이 g 인스턴스에서 length 프로퍼티를 읽으려는데 length 프로퍼티가 없으므로 프로토타입 체이닝을 통해 g.__proto__의 length 프로퍼티를 읽어온 것이다. 빈 배열의 length 프로퍼티는 0이므로 여기에 값이 할당되어 length가 1로 증가된 것이다.

 

🤔 Grade.prototype에 요소를 포함한 배열을 할당한 경우?

👉 앞서 살펴본 예제와 마찬가지로 g 인스턴스에는 length 프로퍼티가 없으니 g.__proto__에서 length 프로퍼티를 찾아 length의 값이 4이므로 인덱스 4에 70을 넣고 다시 length를 5로 증가시킨다.

 

🙅‍♂️ 위의 예제들처럼 클래스에 있는 값이 인스턴스의 동작에 영향을 줘서는 안 된다. 그 자체로 클래스의 추상성을 해치는 것이기 때문이다. 클래스는 구체적인 데이터를 가져서는 안 되고 오직 인스턴스가 사용할 메서드만을 가져야 한다.

 

🔵 사용자가 정의한 두 클래스에 상속관계 부여하기

👉 위 예제에서 Rectangle과 Square 클래스 간에는 width라는 프로퍼티가 있다는 점과 getAreat의 내용이 비슷하다는 공통 요소가 존재한다. 이를 통합하여 고치면 예제 7-6과 같이 된다.

👉 위의 예제를 토대로 Square를 Rectangle의 하위 클래스가 되게끔 소스 코드를 고칠 수 있다. getArea 메서드는 동일한 동작을 하므로 상위 클래스에만 정의하고 하위 클래스에서는 해당 메서드를 상속하면서 height 대신 width를 받으면 된다.

👉 11번째 줄에서는 Square의 생성자 함수 내부에서 Rectangle의 생성자 함수를 함수로써 호출했다. 이때 인자 height 자리에 width를 전달했다. 13번째 줄에서는 메서드를 상속하기 위해 Square의 prototype 객체에 Rectangle의 인스턴스를 부여했다.

 

🙅‍♂️ 하위 클래스로 삼을 생성자 함수의 prototype에 상위 클래스의 인스턴스를 부여하는 것만으로도 기본적인 메서드 상속은 가능하지만 클래스에 있는 값이 인스턴스에 영향을 준다는 등 다양한 문제가 발생할 소지가 있다.

 

👉 그림 7-7 sq 인스턴스의 콘솔 출력 결과를 살펴보자. 첫 줄에서 Square의 인스턴스임을 표시하고 있고 width와 height 모두 5가 들어 있다. __proto__는 Rectangle의 인스턴스임을 표시하는데 width와 height에 undefined가 할당되어 있음을 볼 수 있다. 즉 Square.prototype에 값이 존재하는 것이 문제이다. 왜냐하면 Square.prototype.width(혹은 height)에 값을 부여한 뒤 sq.width(혹은 height)의 값을 삭제한다면 프로토타입 체이닝에 의해 엉뚱한 결과가 나오기 때문이다.

👉 또한 constructor가 Rectangle을 바라본다는 것도 문제이다.

 

7-3-2 클래스가 구체적인 데이터를 지니지 않게 하는 방법

 

👉 클래스가 구체적인 데이터를 지니지 않게 하는 가장 간단한 방법은 모든 프로퍼티들을 지우고 새로운 프로퍼티를 더 이상 추가할 수 없게 하는 것이다. 위 예제를 보면 SubClass의 prototype 내용을 정리하고 freeze하도록 되어 있다. 

 

🔵 빈 생성자 함수를 활용하는 방법

👉 SubClass에 SuperClass의 인스턴스를 할당하는 대신 빈 생성자 함수(Bridge)를 하나 더 만들고 그 prototype이 SuperClass의 prototype을 바라보게 한 다음 SubClass의 prototype에는 Bridge의 인스턴스를 할당했다. 빈 함수에 다리 역할을 부여한 것이다.

👉 Bridge라는 빈 함수를 만들고, Bridge.prototype이 Rectangle.prototype을 참조하게 한 다음, Square.prototype에 new Bridge()로 할당하면, 그림 7-9의 우측 그림처럼 Rectangle 자리에 Bridge가 대체된다.

 

👉 위 예제를 범용성 있게 함수화한 코드이다. 즉시실행함수 내부에 Bridge를 선언해서 이를 클로저로 활용함으로써 메모리에 불필요한 함수 선언을 줄였다. subMethods에는 SubClass의 prototype에 담길 메서드들을 객체로 전달하게끔 했다.

 

🔵 ES5 Object.create

👉 앞서 소개한 방법들 모두 SubClass.prototype의 __proto__가 SuperClass.prototype을 참조하고, SubClass.prototype에는 불필요한 인스턴스 프로퍼티를 남가지 않는다는 면에서 동일하다.

 

7-3-3 constructor 복구하기

👉 SubClass 인스턴스의 constructor는 SuperClass를 가리키는 상태이다. 엄밀히는 SubClass 인스턴스와 SubClass.prototype에는 constructor가 없는 상태이다. 따라서 앞선 예제들의 SubClass.prototype.constructor가 원래의 SubClass를 바라보도록 만들면 된다.

👉 다만 두 번째 Bridge를 활용한 방법의 경우 SubClass.prototype이 SuperClass 대신 Bridge의 인스턴스를 바라보는 상태이므로 예제 7-12의 7번째 줄과 같이 Bridge.prototype.constructor가 SuperClass를 바라보게 하여 SuperClass와의 관계를 복구할 필요가 있다.

 

7-3-4 상위 클래스에의 접근 수단 제공

👉 위 예제는 하위 클래스의 메서드에서 상위 클래스의 메서드 실행 결과를 바탕으로 추가적인 작업을 수행하고자 'super'를 흉내낸 것이다.

👉 6번째 줄에서는 인자가 비었을 경우 SuperClass 생성자 함수에 접근하는 것으로 보았고 this가 달라지는 것을 막기 위해 클로저를 활용했다. 10번째 줄에서는 SuperClass의 prototype 내부의 propName에 해당하는 값이 함수가 아닐 경우 해당 값을 그대로 반환하게 했고 함수일 경우에는 클로저를 활용해 메서드에 접근하도록 했다.

👉 33번째 줄과 36번째 줄에서 보듯, SuperClass의 생성자 함수에 접근할 때는 this.super(), SuperClass의 프로토타입 메서드에 접근할 때는 this.super(propName)과 같이 사용하면 된다.

👉 41번째 줄에서는 sq.getArea()를 통해 SubClass 메서드를 실행했고 42번째 줄에서는 sq.super('getName')으로 SuperClass의 메서드를 실행했다.


04 ES6의 클래스 및 클래스 상속

🔵 ES5와 ES6의 클래스 문법 비교

 

🔵 ES6의 클래스 상속


05 정리

  • 자바스크립트는 프로토타입 기반 언어라서 클래스 및 클래스의 상속 개념은 없지만 클래스 기반 언어와 비슷하게 동작하게끔 하는 다양한 기법들이 도입돼 왔다.
  • 클래스는 어떤 사물의 공통 속성을 모아 정의한 추상적 개념이고, 인스턴스는 클래스의 속성을 지니는 구체적인 사례이다.
  • 상위 클래스(superclass)의 조건을 충족하면서 더 구체적인 조건이 추가된 것을 하위 클래스(subclass)라고 한다.
  • 클래스의 prototype 내부에 정의된 메서드를 프로토타입 메서드라고 하며 이 메서드들은 인스턴스에서 자신의 것처럼 호출할 수 있다.
  • 클래스(생성자 함수)에 직접 정의한 메서드를 스태틱 메서드라고 하며 이 메서드들은 인스턴스가 직접 호출할 수 없고 클래스(생성자 함수)에 의해서만 호출된다.
  • 클래스 상속을 흉내내기 위한 3가지 방법
    • SubClass.prototype에 SuperClass의 인스턴스를 할당한 다음 프로퍼티를 모두 삭제하는 방법
    • 빈 함수(Bridge)를 활용하는 방법
    • Object.create를 이용하는 방법
    • 위 3가지 방법 모두 constructor 프로퍼티가 원래의 생성자 함수를 바라보도록 조정해야 함.
728x90
반응형

댓글