아카이브/JavaScript

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

kaaay 2021. 12. 13. 23:45
728x90

 

자바스크립트는 프로토타입 기반 언어라서 '상속' 개념이 존재하지 않는다. 이는 클래스 기반의 다른 언어에 익숙한 많은 개발자들을 혼란스럽게 했다. 따라서 클래스와 비슷하게 동작하게끔 흉내내는 여러 기법들이 탄생했으며, 결국 ES6에는 클래스 문법이 추가됐다. 다만, ES6의 클래스에서도 일정부분 프로토 타입을 활용하고 있기 때문에 ES5의 클래스를 흉내내기 위한 구현 방식을 학습하는 것 또한 여전히 큰 의미가 있다.

 

  1. 클래스와 인스턴스의 개념 이해
  2. 자바스크립트의 클래스
  3. 클래스 상속
  4. ES6의 클래스 및 클래스 상속

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

'음식'이라는 범주 안에는 고기, 채소, 과일 등 다양한 것들이 들어갈 수 있다.

과일 범주 아래에는 배, 사과, 바나나, 감, 오렌지 등이 포함될 수 있다.

 

배, 사과, 바나나 등은 직접 만질 수 있고, 볼 수 있고, 먹을 수 있는 구체적이고 실존하는 사물에 해당한다.

반면, 음식이나 과일은 어떤 사물의 공통 속성을 모아 정의한 것일 뿐 직접 만질 수도, 볼 수도 없는 추상적인 개념이다.

음식, 과일은 모두 집단, 즉 클래스이다.

음식은 과일보다 상위의 (superior) 개념이고, 과일은 음식보다 하위의 (subordinate) 개념이다.

이 super-, sub-를 접목해서 상위 클래스(superclass) 하위 클래스(subclass)로 표현한다.

 

  • 음식: 먹을 수 있음
  • 과일: 먹을 수 있음 + 나무에서 열림

 

클래스 사이에는 상하관계가 존재한다.

하위로 갈 수록 상위 클래스의 속성을 상속하면서 더 구체적인 요건이 추가 또는 변경된다.

하위 클래스가 아무리 구체화되더라도 이들은 결국 추상적인 개념일 뿐이다.

 

한편, 어떤 클래스의 속성을 지니는 실존하는 개체를 인스턴스(instance)라고 한다.

어떤 클래스에 속한 개체는 그 클래스의 조건을 모두 만족하므로 그 클래스의 구체적인 예시, 즉 인스턴스가 된다.

 

현실세계에서는 개체들이 이미 존재하는 상태에서 이들을 구분짓기 위해 클래스를 도입한다. 

반면, 프로그래밍 언어상에서는 접근 방식이 정반대이다.

사용자가 직접 여러가지 클래스를 정의해야 하며, 클래스를 바탕으로 인스턴스를 만들 때 비로소 어떤 개체가 클래스의 속성을 지니게 된다.

또한 한 인스턴스는 하나의 클래스만을 바탕으로 만들어지며, 인스턴스를 생성할 때 호출할 수 있는 클래스는 오직 하나뿐이다.

 

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

 


2. 자바스크립트의 클래스

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

 

생성자 함수 Array를 new 연산자와 함께 호출하면 인스턴스가 생성된다.

이때 Array를 일종의 클래스라고 하면, Array의 prototype 객체 내부 요소들이 인스턴스에 '상속'된다고 볼 수 있다.

프로토타입 체이닝에 의한 참조가 결과적으로는 클래스의 상속과 동일하게 동작하기 때문.

한편 Array 내부 프로퍼티들 중 prototype 프로퍼티를 제외한 나머지는 인스턴스에 상속되지 않는다.

 

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

 

[스태틱 메서드, 프로토타입 메서드]

// 생성자
var Rectangle = fucntion (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;
};

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

인스턴스에서 직접 호출할 수 있는 메서드를 프로토타입 메서드, 인스턴스에서 직접 접근할 수 없는 메서드를 스태틱 메서드라고 한다.

스태틱 메서드는 생성자 함수를 this로 해야만 호출 할 수 있다.

 

[인스턴스에서 직접 접근이 가능한지 여부]

 

일반적인 사용 방식, 즉 구체적인 인스턴스가 사용할 메서드를 정의한 '틀' 역할을 담당하는 목적을 가질 때의 클래스는 추상적인 개념이다.

하지만 클래스 자체를 this로 해서 직접 접근해야만 하는 스태틱 메서드를 호출할 때의 클래스는 그 자체가 하나의 개체로서 취급된다.

 


3. 클래스 상속

기본 구현

클래스 상속은 객체지향에서 가장 중요한 요소 중 하나이다. 이때문에 ES5까지의 자바스크립트 커뮤니티에서는 클래스 상속을 다른 객체지향 언어에 익숙한 개발자들에게 최대한 친숙한 형태로 흉내내는 것이 주요한 관심사였다.

 

먼저, 프로토타입 체인을 활용해 클래스 상속을 구현하고 최대한 전통적인 객체지향 언어에서의 클래스와 비슷한 형태로 발전시켜보자.

 

[Grade 생성자 함수 및 인스턴스]

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

 

ES5까지의 자바스크립트에는 클래스가 없었기 때문에, 이는 결국 프로토타입 체이닝을 잘 연결한 것으로 이해할 수 있다.

다만, 세부적으로 완벽하게 superclass와 subclass의 구현이 이루어진 것은 아니다.

 

length 프로퍼티가 configurable(삭제가능)하다는 점과, Grade.prototype에 빈 배열을 참조시켰다는 점에서 문제가 발생한다.

 

[length 프로퍼티를 삭제한 경우]

...
g.push(90);
console.log(g) // Grade {0: 100, 1: 80, 2: 90, length: 3};

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

 

내장객체인 배열 인스턴스의 length 프로퍼티는 configurable 속성이 false라서 삭제가 불가능하다.

하지만, Grade 클래스의 인스턴스는 배열 메서드를 상속하지만 기본적으로는 일반 객체의 그대로 지녀 삭제가 가능해서 문제가 된다.

한편, push를 했을 때 0번째 인덱스에 70이 들어가고 length가 다시 1이 될 수 있었던 이유는 g.__proto__, 즉 Grade.prototype이 빈 배열을 가리키고 있기 때문이다.

 

만약 Grade.prototype에 요소를 포함하는 배열을 매칭시켰다면 어땠을까?

 

[요소가 있는 배열을 prototype에 매칭한 경우]

...
Grade.prototype = ['a', 'b', 'c', 'd'];
var g = new Grade(100, 80);

g.push(90);
console.log(g); // Grade {0: 100, 1: 80,, 2: 90, length: 3};

delete g.length;
g.push(70);
console.log(g); // Grade {0: 100, 1: 80, 2: 90, __4: 70, length: 5}

 

클래스에 있는 값이 인스턴스의 동작에 영향을 줄 수 있는 문제가 발생한다.

 

[Rectangle.Square 클래스]

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);
console.log(rect.getArea());  // 12

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

 

하위 클래스로 삼을 생성자 함수의 prototype에 상위 클래스의 인스턴스를 부여하는 것만으로도 기본적인 메서드 상속은 가능하지만 다양한 문제가 발생할 여지가 있어 구조적으로 안정성이 떨어진다.

 

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

클래스(prototype)가 구체적인 데이터를 지니지 않게 하는 방법은 여러 가지가 있는데, 그 중 가장 쉬운 방법은 일단 만들고 나서 프로퍼티들을 일일이 지우고 더는 새로운 프로퍼티를 추가할 수 없게 하는 것이다.

 

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

 

[클래스 상속 및 추상화 방법(1) - 인스턴스 생성 후 프로퍼티 제거]

var extendClass1 = function (Superclass, SubClass, subMethods) {
	SubClass.prototype = new SuperClass();
    for (var prop in SubClass.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;
};

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

extendClass1 함수는 SuperClass와 SubClass, SubClass에 추가할 메서드들이 정의된 객체를 받아서 SubClass의 prototype 내용을 정리하고 freeze하는 내용으로 구성.

 

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

 

[클래스 상속 및 추상화 방법(2)- 빈 함수를 활용]

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

 

[클래스 상속 및 추상화 방법(3)- Object.create 활용]

Square.prototype = Object.create(Rectangle.prototype);
Object.freeze(Square.prototype);
// (...생략)

 

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

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

 

constructor 복구하기

[클래스 상속 및 추상화 방법- 완성본]

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

 


 4. ES6의 클래스 및 클래스 상속

[ES5와 ES6의 클래스 문법 비교]

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

var ES6 = class {
	constructor (name) {
    	this.name = name;
    }
    static staticMethod() {
    	return this.name + 'staticMethod';
    }
    method () {
    	return this.name + 'method';;
    }
};
var es6Instance = new ES6('es6');
console.log(ES6.staticMethod()); // es6 staticMethod
console.log(es6Instace.method()); // es6 method

 

[ES6의 클래스 상속]

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() {
    	console.log('size is : ', super.getArea());
    }
};