js


클래스란?

하나의 범주를 이루는 집합체

프로그래밍에서 클래스 역시 일반적인 의미와 크게 다르지 않습니다. 하나의 범주를 이루는 집합체를 클래스라고 정의할 수 있으며, 해당 클래스는 누군가를 부분집합으로 가질 수 도 있고, 누군가의 부분집합일 수도 있습니다.

인스턴스: 집함체의 예시

예를 들어, 노동자라는 집합이 있고 그 안에 개발자 , 그 안에 프론트엔드 개발자가 있다면 다음과 같은 상하 관계를 갖습니다.

이 때 상위 클래스는 superclass 하위 클래스는 subclass라고 부르는데, 예로 개발자는 노동자의 subclass이자 프론트엔드의 superclass입니다.

  • 노동자 : 노동하는 사람
  • 개발자: 노동 + 프로그래밍하는 하는 사람
  • 프론트엔드: 노동 + 프로그래밍 + 데이터를 받아서 화면어 뿌리는 작업을 하는 사람

집합은 하위단계로 갈 수록 하는 성질이 많아집니다. 이를 프로그래밍 방식으로 이해해보면, 상위 클래스를 하위 클래스가 상속 받아 자신의 속성을 붙이는 것과 같습니다. 그리고 프론트엔드 개발자, 하신영을 프론트엔드 집합의 구체적인 사례로 들 수 있는데, 바로 이 부분이 인스턴스입니다.

const 하신영 = new 프론트엔드();


프로그래밍언어에서 클래스 차이점

하지만 접근방식은 일반적인 집합과 다릅니다. 일반적인 집합에선 같은 계층의 교집합이 존재합니다.

하지만 컴퓨터는 이와 같은 구분법을 이해하지 못하기 때문에 사용자가 직접 여러가지 클래스를 정의해서 인스턴스를 만들어야 합니다. 또 그렇게 만들어진 인스턴스는 해당 클래스만을 바탕으로 만들어집니다. 물론 해당 인덱스가 다양한 클래스에 속할 순 있지만 위 그림처럼 같은 계층의 교집합이 아니라 직계존속 부분집합입니다. 다중 상속을 지원하든 아니든 결국 인스턴스를 호출할 수 있는 클래스는 오직 하나이기 때문입니다.

클래스 -> 인스턴스 생성

즉, 현실이 인스턴스로부터 공통점을 발견해 클래스를 정의하는 것과 달리 프로그래밍에서 클래스는 클래스를 정의 후 인스턴스를 생성할 수 있습니다. 여기서 클래스는 어떡식으로 사용하냐에 따라서 추상적일 수도, 구체적일 수도 있습니다.


자바스크립트에서 클래스

사실 ES5까지 클래스는 존재하지 않았습니다. 자바스크립트는 프로토타입으로 원형을 정의하고 프로토 타입을 활용해서 상속과 비슷한 작업을 할 수 있기 때문입니다. 저번 프로토 타입 스터디에서 유사배열 객체의 prototype을 Array의 인스턴스로 만들고 유사배열 객체에서 Array의 메서드에 접근했던 예시가 그러합니다.

image

프로토타입 체이닝을 통해 해당 인스턴스 메서드가 아니라면 prototype의 메서드를 참조하는 방식이죠. 이 때 메서드를 인스턴스 메서드 또는 프로토타입 메서드라고 부르는데 전자는 인스턴스에서 직접 생성한 메서드와 헷갈릴 수 있기에 후자를 사용하는 게 바람직합니다.


static method

__proto__로 접근할 수 없는 메서드들

한편 상속처럼 사용할 수 없는 메서드들도 있습니다. 생성자 함수 prototype이 아닌 static 메서드들이 그렇습니다. 해당 메서드는 생성자 함수를 this로 만들어서 사용해야 하는데 이때 인자로 인스턴스를 넣으면 인스턴스.프로토타입 메서드를 쓴 것과 같은 효과를 얻을 수 있습니다.

왜 이렇게 static 멤버를 분리했나?

오버라이딩을 방지하고 해당 자료형에서만 사용할 수 있도록 하기 위해서 입니다.

예시

배운 내용을 토대로 코드를 분석해 보도록 하겠습니다. 아래 Rectangle이란 생성자 함수로 정사각형 square 인스턴스를 생성합니다. Rectangle 안에는 getArea, isSquare 메서드가 있는데 해당 메서드를 사용해서 콘솔에 출력하면 어떤 결과가 나올까요?

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

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

Rectangle.isSquare = function () {
  return this.width === this.height;
};

const square = new Rectangle(4, 4);
console.log(square.getArea()); //16
console.log(square.isSquare()); //error is not a function
console.log(Rectangle.isSquare.call(square)); // true
  • 먼저, square.getArea는 16이 찍힙니다.

__proto__가 생각된 상태로 프로토타입 체이닝을 통해 인스턴스가 프로토타입 메서드 getArea를 사용했기 때문입니다.

  • 두 번째는 is not a function이라는 에러가 뜹니다.

해당 메서드는 static 메서드라서 Rectangle prototype엔 존재하지 않습니다. 따라서 프로토 타입 체이닝을 통해 접근한 수 없습니다.

  • 마지막은 true입니다.

두 번째 에러를 보완한 결과물로, static 메서드에 접근하기 위해 Rectangle생성자로 isSquare 메서드를 호출한 뒤 call을 사용해서 this가 square로 변경된 뒤 실행하도록 했습니다.

클래스는 구체적   추상적

위 결과를 바탕으로 아까 클래스가 구체적일 수도 추상적일 수도 있다고 했던 이유를 알 수 있습니다. 생성함수의 프로토타입만 쓰면 클래스가 메서드 정의하는 틀이기에 추상적이지만, 스태틱 메서드처럼 생성함수 자체를 this로 만들어 접근할 때는 클래스가 하나의 개체가 되기에 구체적입니다.


클래스 상속

ES6에서 지원하는 문법을 사용하면 간단하게 구현할 수 있지만 자바스크립트 본연의 모습을 이해하기 위해 ES5까지 사용됐던 상속 방식을 설명하도록 하겠습니다. 일단 모두 자바스크립트의 프로토타입 체이닝을 활용하고 있습니다.

프로토 타입 체이닝으로 구현하는 상속

image

위에서 언급한 성적 데이터를 담는 유사배열 객체입니다.

const Grade = function () {
  const args = Array.prototype.slice.call(arguments);
  for (var i = 0; i < args.length; i++) {
    this[i] = args[i];
  }
  this.length = args.length;
};
Grade.prototype = [];
const grade = new Grade(100, 80);

현재 { 0:100, 1:80, length: 3 }라는 grande 인스턴스 값에 grade.push(90)을 하면 인스턴스 grade가 __proto__를 통해 Array의 프로토 타입 메서드 push를 사용해 grade에 값을 추가합니다. 그럼 다음 인덱스 키에 값이 잘 들어간 걸 볼 수 있습니다.

{ 0:100, 1:80, 2:90, length: 2 }

하지만 문제점이 존재합니다.

1. 안정성 문제

만약 grade 인스턴스의 length 속성을 제거하면 어떻게 될까요?

delete grade.length;
grade.push(70);
console.log(grade); // { 0:70, 1:80,2:90, length: 1}

값이 엉망이 됩니다. configurable 속성을 삭제하지 못 하도록 막는 Array와 달리 Grade는 어떠한 방어 장치도 없기 때문입니다. grade 인스턴스는 삭제된 Grade의 length 대신 Array의 length를 참고하고 해당 length는 0인 빈 배열이라서 push를 하면 바로 0인덱스에 값을 넣고 length는 1로 인식하게 되는 겁니다.

2. constructor가 가리키는 대상의 문제

다음과 같이 삭각형 객체를 만드는 Rectangle 생성자 함수를 정사각형 생성자 함수 Square가 상속받아서 square를 만들면 위와 같이 속성값이 겹쳐서 square의 width, height를 삭제할 경우 prototype의 undefined width, height를 참조해 문제가 생깁니다.

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

const square = new Square(4, 4);

또한 square의 constructor가 Rectangle을 가리키고 있기에 constructor를 활용한 코드도 짤 수가 없습니다.

원치 않는 프로토타입 체이닝 막기

클래스의 값이 인스턴스의 동작에 영향을 미치면 안됩니다. 추상적인 틀로서만 작성하지 않고 구체적인 데이터를 지니게 되면 프로토타입 체이닝으로 잘 못된 값이 들어가는 오류가 발생합니다. 이를 막기 위해 클래스가 구체적인 데이터를 갖지 못 하게 해야 합니다.

예시

해결방법을 간단합니다. 해당 prototype 값을 삭제해준 뒤 추가로 수정하지 못 하도로 Object.freeze로 해당 객체를 동결해버립니다.

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

이러면 프로토타입 체이닝이 일어나지 않을테니 아까처럼 이상한 값을 참조할 일이 없습니다. 다만 일일이 삭제하는 건 번거롭기 때문에 반복문으로 속성을 돌면서 삭제하는 편이 좋습니다.

const extendClass = function (superClass, subClass, SubMethods) {
  subClass.prototype = new superClass();
  for (var prop in superClass.prototype) {
    //상위 클래스의 속성 순회
    if (subClass.prototype.hasOwnProperty(prop)) {
      delete subClass.prototype[prop]; // 상위 속성이 하위 클래스에도 있다면 삭제
    }
  }

  if (SubMethods) {
    for (var method in SubMethods) {
      // 하위 클래스의 메서드들 순회
      subClass.prototype[method] = SubMethods[method];
    } //해당 메서드가 상위 클래스에도 있다면 하위 클래스의 메서드로 초기화합니다.
  }

  Object.freeze(subClass.prototype);
  return subclass;
};

더글라스 클락포드의 Bridge 방법

Bridge라는 빈 함수를 만들고 해당 함수가 상위 클래스와 하위 클래스의 연결고리가 되주면 인스턴스를 제외한 나머지 프로토 타입 체인 경로상에 더는 구체적인 데이터가 없기엔 오류를 막을 수 있습니다.

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

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

const Square = function (width) {
  Rectangle.call(this, width, width);
};

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

Object.create() 활용

Object.create를 사용하면 proto가 superclass의 prototype을 바라보되 subclass의 인스턴스가 되진 않기에 위 두방법 보다 간편하고 안전합니다.

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

constructor 복구

마지막으로 constructor를 자신의 생성자 함수로 바꾸는 방법은 간단합니다. 아까 생성자 함수의 구체적인 값을 없앤 로직에 subClass.prototype.constructor = subClass를 추가해주면 끝입니다.

const extendClass = function (superClass, subClass, SubMethods) {
  subClass.prototype = new superClass();
  for (var prop in superClass.prototype) {
    //상위 클래스의 속성 순회
    if (subClass.prototype.hasOwnProperty(prop)) {
      delete subClass.prototype[prop]; // 상위 속성이 하위 클래스에도 있다면 삭제
    }
  }
  subClass.prototype.constructor = subClass;
  if (SubMethods) {
    for (var method in SubMethods) {
      // 하위 클래스의 메서드들 순회
      subClass.prototype[method] = SubMethods[method];
    } //해당 메서드가 상위 클래스에도 있다면 하위 클래스의 메서드로 초기화합니다.
  }

  Object.freeze(subClass.prototype);
  return subclass;
};


ES6 클래스

extends로 상속하고 super로 상위 클래스에 접근하면 됩니다.

const Rectangle = class {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  getArea() {
    return this.width * this.height;
  }
};
const Square = class extends Rectangle {
  constructor(width) {
    super(width, width);
  }
  getArea() {
    return super.getArea();
  }
};

const square = new Square(4, 4);
console.log(square.getArea());


퀴즈

1. 아래 코드는 에러가 난다. 어디서 에러가 나는가?

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

Rectangle.isSquare = function (instance) {
  return instance.width === instance.height;
};

const square = new Rectangle(4, 4);
console.log(square.isSquare());




참고

💻 코어 자바스크립트

태그:

카테고리:

업데이트:

댓글남기기