클래스

정수·2023년 3월 15일
0

JavaScript

목록 보기
10/15
post-thumbnail

Javascript는 프로토타입 기반 언어이기 때문에 "상속"이라는 개념이 존재하지 않지만 사용자의 니즈에 따라 결국 ES6에는 클래스 문법이 추가되었습니다. 다만 ES6의 클래스에서도 프로토타입을 활용하고 있기에 ES5 체제 하에서 클래스를 흉내내기 위한 구현 방식을 학습하는 것은 여전히 큰 의미가 있습니다.

클래스와 인스턴스의 개념

클래스(class)는 단어의 의미와 동일하게 "집단"이라는 의미로 접근하면 됩니다. 집단은 "어떤 기준을 충족하는 것들의 모임"이며 어떤 기준이란 추상적일 수도, 구체적일 수도 있습니다. 그리고 그 기준을 충족하는 것을 인스턴스(instance)라고 부릅니다.

예를 들면 필자는 대구에서 태어났으며 서울에서 거주 중인 분가한 불효자라는 여러 분류에 속합니다. (부모님 사랑합니다!) 이는 이미 존재하는 필자를 성질에 따라 분류해서 다양한 클래스가 생성이 된 것이죠. 즉, 현실세계에선 개체들이 이미 존재하는 상태에서 이들을 구분짓기 위해 클래스를 도입했습니다.

반면 프로그래밍 언어상에서는 접근 방식이 정반대입니다. 사용자가 직접 여러 가지 클래스를 정의한 후에 이를 바탕으로 인스턴스를 만들 때 비로소 개체가 클래스의 속성을 지니게 되는 것입니다. 또한 하나의 인스턴스는 하나의 클래스만을 바탕으로 만들어집니다. 다중상속을 지원한다 할지라도 인스턴스를 생성할 때 호출할 수 있는 클래스는 오직 하나뿐일 수 밖에 없기 때문입니다.

Javascript의 클래스

Javascript는 프로토타입 기반 언어이기 때문에 클래스의 개념이 존재하지 않습니다. 하지만 클래스와 비슷한 성질을 가지고 있는 곳도 있습니다. 이전 게시글에서 설명했던 prototype이 그렇습니다.

생성자 함수 Arraynew 연산자와 함께 호출하면 인스턴스가 생성됩니다. 이때 Array를 일종의 클래스라고 하면, Arrayprototype 객체 내부 요소들이 인스턴스에 "상속"된다고 볼 수 있습니다. Array의 내부 프로퍼티들 중 prototype를 제외한 나머지는 인스턴스에 상속되지 않습니다.

이렇게 인스턴스 상속 여부에 따라 다른 언어들에서는 스태틱 멤버, 인스턴스 멤버로 나뉘며 Javascript 에서는 스태틱 메서드, (프로토타입) 메서드라고 부릅니다.

var Rectangle = function (width, height) { // 생성자
  this.width = width;
  this.height = height;
};

Rectangle.prototype.getArea = function () { // (프로토타입) 메서드
  return this.width * this.height;
};

Rectangle.isRectangle = function () { // 스태틱 메서드
  return instance instanceof Rectangle &&
    instance.width > 0 && instance.height > 0;
};

var rect1 = new Rectangle(3, 4);
console.log(rect1.getArea()); // 12
console.log(rect1.isRectangle(rect1)); // TypeError: rect1.isRectangle is not a function
console.log(Rectangle.isRectangle(rect1)); // true

클래스 구현

Javascript에서 클래스 상속을 구현했따는 것은 결국 프로토타입 체이닝을 잘 연결한 것으로 이해하면 됩니다. 위 Rectangle 객체를 상속하는 Square 객체를 만들어봅시다. (실제로 정사각형은 직사각형의 subclass입니다.)

var Square = function (width) {
  Rectangle.call(this, width, width);
};
Square.prototype = new Rectangle();

var sq = Square(5);

기본적으로는 위와 같이 구현이 가능하지만 몇가지 문제점들이 아직 존재합니다. sq.constructorSquare가 아닌 Rectangle을 바라보고 있습니다. 해당 문제는 단순히 constructorRectangle을 할당하면 되는 부분이기에 추가적인 설명은 하지 않고 넘어가겠습니다.

...
Square.prototype = new Rectangle();
Square.prototype.constructor = Square;
...

또 다른 문제로는 불필요한 값이 존재하는 문제입니다. Rectangle의 인스턴스를 Square.prototype에 할당했기 때문에 아래와 같이 Square.prototype에 값이 존재하게 됩니다.

console.log(Square.prototype)
// Rectangle {
//   height: undefined
//   width: undefined
// }

이후에 임의로 sq.width 값을 제거하고 Square.prototype.width에 값을 부여하게 된다면 프로포타입 체이닝에 의해 예상치 못한 값이 출력될 수 있습니다. 이렇게 클래스에 있는 값이 인스턴스의 동작에 영향을 주어선 안 됩니다. 이는 클래스의 추상성을 해칠 뿐더러 예기치 않은 오류가 발생한 가능성을 안고 가야합니다.

클래스에 있는 값 제거

쉽게 이해하기 위해 상위 객체(Rectangle)를 superClass, 하위 객체(Square)를 subClass라 지칭하겠습니다.

단순하게 생각하면 클래스에 있는 값(프로퍼티)들을 모두 제거하고 새로운 프로퍼티를 추가할 수 없게 처리할 수 있을 것입니다.

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

또 다른 방법으로는 superClassprototype을 참조하는 빈 함수를 만들어 subClassprototype을 연결하는 역할을 하는 Bridge 함수를 만드는 것입니다.

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

마지막 방법으로는 ES5에서 도입된 Object.create를 이용한 방법입니다.

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

더 나아가

하위 클래스의 메서드에서 상위 클래스의 메서드 실행 결과를 바탕으로 추가적인 작업을 수행하고 싶을 때가 있습니다. 즉, subClass 에서 superClassprototype 메서드에 접근하기 위한 별도의 수단, super을 구현해보겠습니다.

Square.prototype.super = function (propName) {
  var self = this;
  if (!propName)
    return function () {
      Rectangle.apply(self, arguments);
    };
  var prop = Rectangle.prototype[propName];
  if (typeof prop !== "function") return prop;
  return function () {
    return prop.apply(self, arguments);
  };
};

Square.prototype.getArea = function () {
  return "size is: " + this.super("getArea")();
};

Object.freeze(Square.prototype);

var sq = new Square(5);

console.log(sq.getArea()); // size is: 25
console.log(sq.super("getArea")()); // 25

ES6 class 문법

RectangleSquare을 ES6의 class 문법으로 구성하면 아래와 같습니다.

var Rectangle = class {
  constructor (width, height) {
    this.width = width;
    this.height = height;
  }
  getArea () {
    return this.width * this.height;
  }
};
var Square = class extends Rectangle { // 상속 관계 설정
  constructor (width) {
    super(width, width);
  }
  getArea () {
    return "size is: " + super.getArea();
  }
}

참고 자료


profile
해피한하루

0개의 댓글