CoreJS - 클래스

SANGKU OH·2020년 12월 9일
1

CoreJavascript

목록 보기
10/10
post-thumbnail

자바스크립트는 프로토타입 기반 언어이다. 따라서 '상속' 이라는 개념이 존재하지 않는다.
다만 프로토타입을 활용한 ES6의 클래스 문법을 통해 클래스를 흉내낼 수 있다!

클래스와 인스턴트의 개념

객체지향 프로그래밍에서 거의 반드시 등장하는 제1요소닌 클래스라는 단어의 의미는 일반적으로 쓰이는 의미와 흡사하다. 영어사전에서 class는 '계급, 집단, 집합' 등으로 번역한다. 프로그래밍 언어적으로도 이와 동일한 개념에서 접근하면 된다.

예를들어, 어떤 가게에 다양한 음식이 한 개씩 있다고 할 때, '음식'이라는 범주 안에는 고기, 채소, 과일 등등 다양한 것들이 들어갈 수 있다. 이들 역시 다시 하위에 각 분류에 속하는 대상들을 나열할 수있을 것이다. 과일이라는 범주 아래에는 배, 사과, 바나나, 감 오렌지 등등이 포함된다. 여기서 배, 사과, 바나나 등은 구체적이고 실존하는 사물에 해당한다. 반면 음식이나 과일은 어떤 사물들의 공통 속성을 모아 정의 한 것일 뿐 직접 만질 수도 없는 추상적인 개념이다. 음식은 과일과의 관계에서 상위의 개념이고, 과일은 음식과의 관계에서 하위의 개념이다.


위의 그림에서와 같이, 음식, 과일은 모두 집단, 즉 클래스이다. 음식은 과일보다 상위의 개념이고, 과일은 음식보다 하위의 개념이다. 여기서 앞의 음식을 상위클래스, 뒤의 과일을 하위 클래스라고 표현한다.
그렇다면 과일 분류 하위에 또 다른 분류가 있을 경우에는 클래스 간의 관계는 어떻게 될까?

음식은 과일의 상위클래스이다. 과일은 음식의 하위클래스이면서 귤류의 상위클래스이다. 귤류는 과일의 하위클래스이다. 한편 음식은 귤류의 최상위 클래스이다. 귤류는 음식의 하위 클래스이다. 하위 개념은 상위 개념을 포함하면서 더 구체적인 개념이 추가된다.

예를 들어, 최상위 분류인 음식 클래스는 '먹을 수 있다' 정도라면, 하위의 과일 클래스는 '먹을 수 있다 + 나무에서 열린다'가 되고, 그 하위의 귤류 클래스는 '먹을 수 있다 + 나무에서 열린다 + 말랑한 껍질 속에 달고 신맛이 나는 과육이 들어 있다'가 된다.
이처럼 클래스는 하위로 갈수록 상위 클래스의 속성을 상속하면서 더 구체적인 요건이 추가 또는 변경된다. 물론 하위 클래스가 아무리 구체화되더라도 이들은 결국 추상적인 개념이다.

조건: 클래스
구체적 사례: 인스턴스

이처럼 어떤 클래스의 속성을 지니는 실존하는 개체를 일컬어 인스턴스라고 한다.
사전적의미를 보면 인스턴스는 '사례'라고 번역되고 있는데, 풀어쓰면 '어떤 조건에 부합하는 구체적인 예시'가 되는 것이다. 여기서 조건이 곧 클래스를 의미한다고 보면, 어떤 클래스에 속한 개체는 그 클래스의 조건을 모두 만족하므로 그 클래스의 구체적인 예시, 즉 인스턴스가 되는 것이다.

현실세계에는 개체들이 이미 존재하는 상태에서 이들을 구분짓기 위해 클래스를 도입한다.
반면, 프로그래밍 언어상에서는 접근 방식이 정반대이다.

컴퓨터는 위와 같은 구분법을 알지 못하므로 사용자가 직접 여러가지 클래스를 정의해야 한다. 클래스를 바탕으로 인스턴스를 만들 때 비로소 어떤 개체가 클래스의 속성을 지니게 된다. 또한 인스턴스는 하나의 클래스 만을 바탕으로 만들어져야 한다. 어떤 인스턴스가 다양한 클래스에 속할 수는 있지만, 이 클래스들은 모두 인스턴스 입장에서는 '직계존속'이다. 다중상속을 지원하는 언어든 그렇지 않은 언어이든 결국 인스턴스를 생성할 때 호출할 수 있는 클래스는 오직 하나뿐일 수밖에 없기 때문이다.

자바스크립트의 클래스

생성자 함수 Array를 new 연산자와 함께 호출하면 인스턴스가 생성된다. 이때 Array를 일종의 클래스라고 하면, Array의 prototype 객체 내부 요소들이 인스턴스에 '상속'된다고 볼 수 있습니다. 엄밀히는 상속이 아닌 프로토타입 체이닝에 의한 참조지만, 결과적으로는 동일하게 동작하므로 이렇게 이해해도 무방하다. 한편 Array 내부 프로퍼티들 중 prototype 프로퍼티를 제외한 나머지는 인스턴스에 상속되지 않는다.

인스턴스에 상속되는지(인스턴스가 참조하는지) 여부에 따라 스태틱 멤버(static member), 인스턴스 멤버(instance member)로 나뉜다. 이 분류는 다른 언어의 클래스 구성요소에 대한 정의를 차용한 것으로, 클래스 입장에서 사용 대상 구분에 따라 구분한 것이다.

자바스크립트에서는 인스턴스에서도 직접 메서드를 정의할 수 있기 때문에 '인스턴스 메소드'라는 명칭은 프로토타입에 정의한 메소드를 지칭하는 것인지 인스턴스에 정의한 메소드를 지칭하는 것인지에 도리에 혼란을 줄 수 있다.
따라서 이 명칭 대신에 자바스크립트의 특징을 살려 프로토타입 메소드(prototype method)라고 불러보자!

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

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

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

let rect1 = new Rectangle(3, 4);
console.log(rect1.getArea()); // 12
console.log(rect1.isRectangle(rect1)); // Error
console.log(Rectangle.isRectangle(rect1));; // true

Rectangle 함수를 new 연산자와 함께 호출해서 생성된 인스턴스를 rect1에 할당했다. 이 인스턴스에는 width, height 프로퍼티에 각각 3, 4의 값이 할당돼 있다.

프로토타입 객체에 할당한 메소드는 인스턴스가 마치 자신의 것 처럼 호출할 수 있다.

  • 13번째 줄에서 호출한 getArea는 실제로는 rect1.__proto__.getArea에 접근하는 것이다.
  • 이때 __proto__를 생략했으므로 this가 rect1인 채로 실행되니까, 결과로는 rect1.width * rect1.height의 계산값이 반환된다. 이처럼 인스턴스에서 직접 호출할 수 있는 메소드가 바로 프로토타입이다.

14번째 줄은 rect1 인스턴스에서 isRectangle이라는 메소드에 접근하고자 한다.

이렇게 인스턴스에서 직접 접근할 수 없는 메소드를 스태틱 메소드라고 한다. 스태틱 메소드는 15번째 줄처럼 생성자 함수를 this로 해야만 호출할 수 있다.

  • rect1에 해당 메소드가 있는지 검색했는데 없고, rect1.__proto__에도 없으며, rect1.__proto___.__proto__(= Object.prototype)에도 없다.
  • 결국 undefined를 실행하라는 명령이므로, 함수가 아니어서 실행할 수 없다는 의미의 "Uncaught TypeError: not a function" 에러가 발생한다.

프로그래밍 언어에서 클래스는 사용하기에 따라 추상적일수도 있고, 구체적인 개체가 될 수도 있다.

구체적인 인스턴스가 사용할 메소드를 정의한 '틀'의 역할을 담당하는 목적을 가질 때의 클래스는 추상적인 개념이지만, 클래스 자체를 this로 해서 직접 접근해야만 하는 스태틱 메소드를 호출할 때의 클래스는 그 자체가 하나의 개체로서 취급된다.

클래스 상속

기본 구현

let Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};
Rectangle.prototype.getArea = function () {
  return this.width * this.height;
};
let rect = new Rectangle(3, 4);
console.log(rect.getArea()); // 12

let Square = function (width) {
  this.width = width;
};
Square.prototype.getArea = function () {
  return this.width * this.width;
};
let sq = new Square(5);
console.log(sq.getArea()); // 25

Reactangle과 Square 클래스에 공통요소가 보인다. width라는 프로퍼티가 공통이고, getArea는 내용이 다르지만 비슷하다. 만약 Suqare에서 width 프로퍼티만 쓰지 않고 height 프로퍼티에 width 값을 부여하는 형태가 된다면 getArea도 동일하게 고칠 수 있겠다.

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

정사각형은 직사각형에 '네 변의 길이가 모두 같다'라는 구체적인 조건이 하나 추가된 개념이다. 위 처럼 고치고 나니 이제 소스상으로도 Suqare를 Rectangle의 하위 클래스로 삼을 수 있다. getArea라는 메소드는 동일한 동작을 하므로 상위클래스에만 정의하고, 하위 클래스에는 해당 메소드를 상속하면서 height 대신 width를 넣어주면 된다!

let Square = function (width) {
  Rectangle.call(this, width, width);
};
Square.prototype = new Rectangle();
  • Square의 생성자 함수 내부에서 Rectangle의 생성자 함수를 함수로써 호출했다.
  • 이때 인자 height 자리에 width를 전달했다.
  • 메소드를 상속하기 위해 Square의 프로토타입 객체에 Rectangle의 인스턴스를 부여했다.

다만 위 코드에는 클래스에 있는 값이 인스턴스에 영향을 줄 수 있는 구조라는 문제점이 있다.

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

1. 프로퍼티들을 일일이 지우고 더는 새로운 프로퍼티를 추가할 수 없게 하는 것

가장 쉬운 방법은 일단 만들고 나서 프로퍼티들을 일일이 지우고 더는 새로운 프로퍼티를 추가할 수 없게 하는 것이다. 이 정도로도 깔끔하고 간단하게 목적하는 바를 충분히 이뤄낼 수 있다.

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

프로퍼티가 많다면 반복작업이 될 테니까 반복을 없애고 좀 더 범용적으로 이런 동작을 수행하는 함수로 만들어보자!

let extendClass1 = function (SuperClass, SubClass, subMethods) {
  Subclass.prototype = new SuperClass();
  for (let prop in SubClass.prototype) {
    if (SubClass.prototype.hasOwnProperty(prop)) {
      delete SubClass.prototype[prop];
    }
  }
  if (subMethods) {
    for (let method in subMethods) {
      SubClass.prototype[method] = subMethods[method];
    }
  }
  Object.freeze(SubClass.prototype);
  return SubClass;
};

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

extendCalss1 함수는 SuperClass와 SubClass, SubClass에 추가할 메소드들이 정의된 객체를 받아서 SubClass의 prototype 내용을 정리하고 freeze하는 내용으로 구성되어 있다.
SubClass의 프로토타입을 정리하는 내용이 다소 복잡해졌지만 범용성 측면에서 좋은 방법이다.

2. 더글라스 크락포트가 제시해서 대중적으로 널리 알려진 방법

빈 함수에 다리 역할을 부여하는 것

SubClass의 prototype에 직접 SuperClass의 인스턴스를 할당하는 대신 아무런 프로퍼티를 생성하지 않는 빈 생성자 함수(Bridge)를 하나 더 만들어서 그 prototype이 SuperClass의 prototype을 바라보게끔 한 다음, SubClass의 prototype에는 Bridge의 인스턴스를 할당하게 하는 것

let Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};
Rectangle.prototype.getArea = function () {
  return this.width * this.height;
};
let Square = function (width) {
  Rectangle.call(this, width, width);
};
let Bridge = function () {};
Bridge.prototype = Rectangle.prototype;
Square.prototype = new Bridge();
Object.freeze(Square.prototype);

  • Bridge라는 빈 함수를 만들고, Bridge.prototype이 Rectangle.prototype을 참조하게 한다.
  • Square.prototype에 new Bridge()로 할당하면 인스턴스를 제외한 프로토타입 체인 경로상에는 더는 구체적이 데이터가 남아있지 않게 된다.

마찬가지로 범용성을 고려해서 코드를 작성해보자.

let extendClass2 = (function () {
	let Bridge = function () {};
	return function (SuperClass, SubClass, subMethods) {
      Bridge.prototype = SuperClass.prototype;
      SubClass.prototype = new Bridge();
      if (subMethods) {
        for (let method in subMethods) {
          SubClass.prototype[method] = subMethods[method];
        }
      }
      Object.freeze(SubClass.prototype);
      return SubClass;
    };
})();

즉식실행함수 내부에서 Bridge를 선언해서 이를 클로저를 활용함으로써 메모리에 불필요한 함수 선언을 줄였다. subMethods에는 subClass의 prototype에 담길 메소드들을 객체로 전달하게끔 했다.

3. ES5에서 도입된 Object.create를 이용한 방법

SubClass의 prototype의 __proto__가 SuperClass의 prototype을 바라보되, SuperClass의 인스턴스가 되지 않으므로 앞서 소개한 두 방법보다 간다하면서 안전하다!

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

클래스 상속 및 추상화를 흉내 내기 위한 라이브러리가 많이 있지만, 기본적인 접근 방법은 위의 세 가지 아이디어를 크게 벗어나지 않는다.

결국 SubClass.rptotype의 __proto__가 SuperClass.prototype를 참조하고, SubClass.prototype에는 불필요한 인스턴스 프로퍼티가 남아있지 않으면 된다.

constructor 복구하기

위 세가지 방법 모두 기본적인 상속에는 성공했지만 SubClass 인스턴스의 constructor는 여전히 superClass를 가리키는 상태이다.

엄밀히는 SubClass 인스턴스에는 constructor가 없고, SubClass.prototype에도 없는 상태이다. 프로토타입 체인상 가장 먼저 등장하는 SuperClass.prototype의 constructor에서 가리키는 대상, 즉 SuperClass가 출력될 뿐이다.
따라서 위 코드들의 SubClass.prototype.constructor가 원래의 SubClass를 바라보도록 해주면 된다.
인스턴스 생성 후 프로퍼티 제거

let extendClass1 = function (SuperClass, SubClass, subMethods) {
  SubClass.prototype = new SuperClass();
  for (let porp in SubClass.prototype) {
    if (SubClass.prototype.hasOwnProperty(prop)) {
      delete SubClass.prototype[prop];
    }
  }
  SubClass.prototype.constructor = SubClass;
  if (subMethods) {
    for (let method in subMethods) {
      SubClass.prototype[method] = subMethods[method];
    }
  }
  Object.freeze(SubClass.prototype);
  return SubClass;
};

빈 함수를 활용한 방법

let extendClass2 = (function () {
	let Bridge = function () {};
	return function (SuperClass, SubClass, subMethods) {
      Bridge.prototype = SuperClass.prototype;
      SubClass.prototype = new Bridge();
      SubClass.prototype.constructor = SubClass;
      if (subMethods) {
        for (let metohd in subMethods) {
          SubClass.prototype[method] = subMethods[method];
        }
      }
      Object.freeze(SubClass.prototype);
      return SubClass;
    }
})();

Object.create 활용

let extendClass3 = function (SuperClass, SubClass, subMethods) {
  SubClass.prototype = Object.create(SuperClass.prototype);
  SubClass.prototype.constructor. = SubClass;
  if (subMethods) {
    for (let method in subMethods) {
      SubClass.prototype[method] = subMethods[method];
    }
  }
  Object.freeze(SubClass.prototype);
  return SubClass;
};

ES6의 클래스 및 클래스 상속

클래스

let ES5 = function (name) {
  this.name = name;
};
ES5.staticMethod = function () {
  return this.name + ' staticMethod';
};
ES5.prototype.method = function () {
  return this.name + ' method';
};
let es5Instance = new ES5('es5');
console.log(ES5.staticMethod()); // es5 staticMethod
console.log(es5Instance.method()); // es5 method

let ES6 = class {
  constructor (name) {
    this.name = name;
  }
  static staticMethod () {
    return this.name + ' staticMethod';
  }
  method () {
    return this.name + ' method';
  }
};
let es6Instance = new ES6('es6');
console.log(ES6.staticMethod()); // es6 staticMethod
console.log(es6Instance.method());// es6 method
  • 13번째 줄: class라는 명령어 뒤에 바로 { }가 등장한다. 이 중괄호 묶음 내부가 클래스 본문 영역이다.
  • 14번째 줄: constructor라는 이름 바로 뒤에 () {가 등장하고 있다. 클래스 본문에서 function키워드를 생략하더라도 모두 메소드로 인식한다. constructor라는 이름에서 바로 알 수있듯이, 이 부분은 ES5의 생성자 함수와 동일한 역할을 한다.
  • 16번째 줄: 메소드와 다음 메소드 사이에는 콤마(,)로 구분하지 않는다.
  • 17번째 줄: static이라는 키워드 뒤에 staticMethod라는 이름이 등장했고, 뒤이어 () { 가 등장한다. static 키워드는 해당 메소드가 static 메소드임을 알리는 내용으로, 생성자 함수에 바로 할당하는 메소드와 동일하게 생성자 함수(클래스) 자신만의 호출할 수 있다.
  • 20번째 줄: method라는 이름이 등장한다. 이는 자동으로 prototype 객체 내부에 할당되는 메소드이다. 프로토타입 체이닝을 통해 마치 자신의 것처럼 호출할 수있는 메소드이다.

클래스 상속

let Rectangle = class {
  constructor (width, height) {
    this.width = width;
    this.height = height;
  }
  getArea () {
    return this.width * this.height;
  }
};
let Square = class extends Rectangle {
  constructor (width) {
    super(width, width);
  }
  getArea () {
    cosnole.log('size is :', super.getArea());
  }
}
  • 10번째 줄: Square를 Rectangle 클래스를 상속받는 SubClass로 만들기 위해 class 명렁어 뒤에 단순히 'extends Rectangle'이라는 내용을 추가했다. 이것만으로도 상속 관계 설정이 끝난다.
  • 12번째 줄: constructor 내부에는 super라는 키워드를 함수처럼 사용할 수 있는데, 이 함수는 SuperClass의 constructor를 실행한다.
  • 15번째 줄: constructor 메소드를 제외한 다른 메소드에서는 super 키워드를 마치 객체처럼 사용할수 있다. 이때 객체는 SuperClass.prototype을 바라보는데, 호출한 메소드의 this는 'super'가 아닌 원래의 this를 그대로 따른다.

정리

자바스크립트는 프로토타입 기반 언어라서 클래스 및 상속 개념은 존재하지 않는다.
그러나 프로토타입을 기반으로 클래스와 비슷하게 동작하게끔 하는 다양한 기법들이 도입돼 있다.

클래스

  • 클래스는 어떤 사물의 공통 속성을 모아 정의한 추상적인 개념이다.
  • 인스턴스는 클래스의 속성을 지니는 구체적인 사례이다.
  • 상위 클래스(SuperClass)의 조건을 충족하면서 더욱 구체적인 조건이 추가된 것을 하위 클래스(SubClass)라고 합니다.

메소드

  • 클래스의 prototype 내부에 정의된 메소드를 프로토타입 메소드라고 한다. 이들은 인스턴스가 마치 자신의 것 처럼 호출할 수 있다.
  • 클래스(생성자함수)에 직접 정의한 메소드를 스태틱 메소드라고 한다. 이들은 인스턴스가 직접 호출할 수 없고 클래스(생성자 함수)에 의해서만 호출할 수 있다.

클래스 상속

constructor 프로퍼티가 원래의 생성자 함수를 바라보도록 조정해야 한다.

  • SubClass.prototype에 SuperClass의 인스턴스를 할당한 다음 프로퍼티를 모두 삭제하는 방법
  • 빈 함수(Bridge)를 활용하는 방법
  • Object.create를 이용하는 방법
profile
Prof.Google을 통해 필요한 정보를 이 곳에 insert 🐸

0개의 댓글