카테고리 없음

ES5와 Class

SambaLim 2022. 1. 28. 10:15

프로토타입 기반의 언어인 JS는 클래스가 존재하지 않았었습니다. 따라서 이는 클래스 기반의 다른 언어에 익숙한 많은 사용자를 혼란스럽게 했고 많은 개발자들은 JS에서 클래스와 유사하게 흉내내는 여러 기법들을 만들어냈습니다. 이러한 니즈를 바탕으로 결국 ES6에서 클래스 문법이 추가되었습니다.

클래스란 무엇인지 알아보고 개발자들이 ES5이하의 환경에서 클래스를 구현했던 방법을 예제를 통해 함께 해보려합니다.

클래스와 인스턴스

클래스라는 개념의 기원을 알아보고 그 시각을 가지고 클래스와 인스턴스는 무엇인지 함께 알아봅시다.

클래스라는 개념의 기원

서양철학사의 시작

서양철학사가 어디서부터 시작했느냐에 대해 여러가지 의견이 있겠지만, 일반적으로 서양철학사의 시작으로 철학자 ‘탈레스’를 이야기합니다. 탈레스가 어릴때 아버지와 여행가서 피라미드의 높이를 측정해낸 것, 일식을 정확히 예측한 것, 최초로 증명을 사용하여 도형의 닮음에 대한 정리를 증명한 것 등 여러업적들이 있지만 탈레스가 서양철학사의 시작이 된 이유는 어처구니 없게도 만물의 근원은 물이라고 이야기했기 때문입니다.

당시에는 만물의 기원에 대한 큰 관심이 없었지만 이 의문은 후대에 남겨져 철학이라는 학문을 탄생시킵니다.

플라톤의 이데아

그로부터 약 200년 뒤에 ‘플라톤’이라는 철학자가 이러한 의문에 대한 답으로 ‘이데아’를 이야기합니다. 현실에 있는 원을 엄밀히 이야기하면 완벽한 원이라고 이야기할 수 없지만 우리가 이를 원이라고 인식하는 것은 완전한 원에 대한 관념이 있기 때문이고 이 완전한 원에 대한 관념이 이데아입니다.

아리스토텔레스의 Classification

아리스토텔레스는 플라톤의 제자이지만 이데아를 반박했습니다. 아리스토텔레스은 사물의 실체(형상)가 이데아라는 가상의 영역에 있는 것이 아니라 사물의 실체(질료)에 내재되어있다고 생각하였습니다. 따라서 아리스토텔레스는 자연과학에도 관심을 갖고 생물의 속성(properties)를 관찰하여 생물의 체계를 분류하기도 하였습니다.

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

  • 클래스: 인스턴스의 공통 속성을 모은 추상적인 개념
    • 과일은 음식이라는 집단에 포함이 되는데 따라서 과일 클래스는 음식 클래스의 하위 클래스가 됩니다.
  • 인스턴스: 클래스의 속성을 지니는 실존하는 객체
    • 자몽, 사과, 귤... 등은 과일이라는 집단에 속합니다. 여기서 과일은 ‘클래스’이고 자몽, 사과 귤 등은 클래스에 속하는 인스턴스들 입니다.
    • 과일, 음식은 추상적인 개념이고 자몽, 사과, 귤 등은 눈으로 볼 수도 있고 만질 수도 있고 먹을 수도 있는 구체적인 물체입니다.
  • 인스턴스로 부터 공통점을 찾아 정의되는 현실에서의 클래스와 달리 프로그래밍에서는 클래스가 먼저 정의되어야 공통적인 요소를 지니는 인스턴스를 생성할 수 있습니다.

클래스관점으로 자바스크립트 접근하기

프로토타입을 일반적인 의미에서의 클래스 관점에서 접근해보면 비슷하게 해설할 수 있는 요소가 있습니다.

생성자 함수 Arraynew 연산자와 함께 호출하면 인스턴스가 생성됩니다.

var array = new Array(1, 2, 3);

생성자 함수 Array 를 클래스라고 한다면, Array.prototype 객체 내부의 요소들이 인스턴스에 상속된다고 볼 수 있습니다. (엄밀히 말하면 상속이 아닌 프로토타입 체이닝에 인한 참조입니다.)

static member, instance member

인스턴스에 상속되는지, 아닌지에 따라 스태틱 멤버와 인스턴스 멤버로 나뉩니다.

  • 스태틱 멤버: 프로토타입 프로퍼티 내부에 할당되지 않고 Array 생성자 함수 객체에 직접 할당되어 있는 프로퍼티
  • 인스턴스 멤버: 프로토타입 내부에 정의되어있는 메서드들

예시

var array = new Array(1, 2, 3);

// 인스턴스 멤버 (프로토타입 메서드)
array.concat([4]); // [1, 2, 3, 4]

// 스태틱 멤버
array.isArray(array); // Uncaught TypeError: array.isArray is not a function
Array.isArray(array); // true

클래스 상속

자바스크립트에서 상속을 구현했다는 것은 결국 프로토타입 체이닝을 잘 연결한 것으로 이해하면됩니다.

프로토타입 체이닝으로 인해 자바스크립트는 타언어에서의 상속과 다른 동작을 합니다. 따라서 타언어에서의 상속을 구현하기 위해서 여러 작업이 필요합니다. 아래의 예제들을 통해 타언어에서의 상속을 흉내내는 것을 함께 해봅시다.

인스턴스에 영향을 주는 클래스값 없애기

var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
}

Rectangle.prototype.getArea = function () {
  return this.width * this.height;
}

var rect = new Rectangle(3, 4);
rect.getArea(); // 12

var Square = function (width) {
  this.width = width;
  this.height = width;
  // Rectangle.call(this, width, width);
}

Square.prototype = new Rectangle(2, 3);

// Square.prototype.getArea = function() {
//   return this.width * this.width;
// }

var sq = new Square(5);
sq.getArea(); // 25

sq 
// Square {width: 5, height: 5}

sq.__proto__.width; // 2
sq.__proto__.height; // 3

인스턴스(sq)의 프로퍼티를 삭제할 경우, 체이닝을 통해 prototype의 프로퍼티(ex. sq.__proto__.width)를 가리키기때문에 문제를 야기합니다.

클래스에 있는 값이 인스턴스의 동작에 영향을 주는 일이 있어서는 안됩니다. 클래스가 구체적인 데이터를 지니지 않고 추상적인 ‘틀’로만 동작해야합니다.

따라서 우리는 하위클래스에게 상속하기 전에 prototype의 프로퍼티를 제거하는 작업을 해야합니다. 하는 방법은 아래의 3가지 예제와 같습니다.

1. delete prototype properties

delete Square.prototype.width;
delete Square.prototype.height;
Object.freeze(Square.prototype);

2. Bridge function

function Bridge() {}
Bridge.prototype = Rectangle.prototype;
Square.prototype = new Bridge();
Object.freeze(Square.prototype);

sq.__proto__;

3. Object.create

Square.prototype = Object.create(Rectangle.prototype);
Object.freeze(Square.prototype);

constructor 복구하기

위와 같은 방법으로 기본적인 상속에는 성공했지만, Square 의 인스턴스의 constructor 은 여전히 Rectangle 을 가리킵니다. 따라서 이를 수정해줍니다.

// Object.freeze 전에
Square.prototype.constructor = Square;

상위 클래스로의 접근 수단 제공

상위 클래스에 접근할 수 있는 수단인 super 구현해봅시다.

var extendClass = function (SuperClass, SubClass, subMethods) {
  SubClass.prototype = Object.create(SuperClass.prototype);
  SubClass.prototype.constructor = SubClass;
  SubClass.prototype.super = function (propName) {
    var self = this;
    if (!propName) return function () { SuperClass.apply(self, arguments); }

    var prop = SuperClass.prototype[propName];
    if (typeof prop !== 'function') return prop;
    return function () { return prop.apply(self, arguments); }

    if (subMethods) {
      for (var method in subMethods) {
        SubClass.prototype[method] = subMethods[method];
      }
    }

    Object.freeze(SubClass.prototype);
    return SubClass;
  }
}

ES6 Class is Syntatic Sugar?

MDN에도 class 가 “syntactical sugar”라고 표현했었을만큼, Class 는 문법설탕일뿐이라는 이야기가 많습니다.

Syntatic Sugar

위키피디아를 보면 문법설탕이라는 말을 다음과 같이 정의합니다.

  • 무엇인가를 만들기에 읽거나 표현하기 쉽게 만들어진 문법.
  • 사람이 사용하기에 언어를 더 “sweeter”하게 해주는 문법.
  • 더욱 간결하고 명확하게 표현이 가능한 문법.

이와 같이 개발자 입장에서 달콤해 보이는 말들이 가득하지만 프로그래밍 입장에서는 문법설탕은 “언어에서 제거하더라도 그 언어가 제공하는 기능과 표현을 똑같이 유지하는데 아무런 노력이 필요하지 않은 구조” 입니다.

프로토타입언어인 자바스크립트에 클래스가 구현된 것에 대한 반감일 수도 있고 기존 ES5이하의 문법에서도 클래스와 같이 동작하도록 개발하는 것이 가능했기때문에 ES6 클래스를 문법설탕이라고 표현하는듯 합니다.

하지만, ES6의 클래스를 ES5로 전환하는 것은 흉내는 가능하나 한계가 분명히 존재합니다. 따라서 문법설탕취급만 하는 것은 다소 아쉽다고 생각합니다.

proto Deprecated

객체의 __proto__ 를 변경하는 행위는 모든 브라우저, JS 엔진에서 매우 느린 작업입니다. __proto__ 를 처리할때, [[Prototype]] 이 변경된 객체에 접근하는 모든 코드에 대해서 일괄적으로 처리함에 따라 시간이 많이 소요됩니다. 따라서 __proto__ 를 사용하여 변경하기보다, Object.create() 를 사용하여 원하는 새 객체를 만드는 것이 성능에 더 좋습니다.

마무리

ES5에서 Class 문법을 구현하는 과정에 대해 설명하기 위해 Class가 무엇인지 서양철학사부터 시작하여 이해한 후에 프로토타입 언어인 JS에서 상속을 구현했을 경우 어떤 문제들이 생기는지 알아보았습니다.

상위 클래스의 프로퍼티에 접근이 가능한 문제, constructor 연결이 제대로 안되는 문제, super 와 같이 상위 클래스 메소드에 접근이 가능한 방법이 없다는 문제들이 있었는데 이 문제들을 해결하는 방법을 예시를 통해 알아보았습니다.

저는 주로 “코어 자바스크립트”의 예제를 따랐지만 이 외에도 Class를 흉내내기 위한 시도들은 더욱 많았고 결국 ES6에서 Class 문법을 지원해주었습니다.

ES6의 Class에 대해 비난도 많았고 문법설탕이라는 별명도 생겼지만 자바스크립트에서 개발하는데 새로운 선택지가 하나 생겼다고 생각이됩니다.

참고자료