[JS] 클래스

cabbage·2023년 6월 17일

JS

목록 보기
37/43
post-thumbnail

기술면접을 준비하는 과정에서 자바스크립트의 클래스 개념에 대해 공부하였고 클래스 개념에 대해 정리한다. 이 정리글은 코어 자바스크립트(정재남)를 참고하여 작성하였다.

클래스

ES6에서 클래스 문법이 추가되었다. 다만 ES6의 클래스에서도 일정 부분 프로토타입을 활용하기 때문에 프로토타입을 통해 클래스를 이해할 수 있다.

클래스와 인스턴스 개념 이해하기

클래스는 어떤 구체적인 개체들의 공통 속성을 모아 정의한 개념이다.

인스턴스는 클래스의 속성을 갖는 실존하는 개체를 말한다.

책에서는 클래스 예시를 '음식', '과일'에 비유하여 설명한다.

  • '음식'이라는 범주에는 고기, 채소, 과일 등 다양한 것들이 존재한다.
  • '과일'이라는 범주에는 배, 사과, 바나나 등 다양한 것들이 존재한다.

'음식', '과일'은 어떤 사물들의 공통 속성을 모아 정의한 것일 뿐 직접 만질 수도 볼 수도 없는 추상적인 개념이다. 반면, 배, 사과, 바나나는 직접 만지고, 보고, 먹을 수 있는 구체적이고 실존하는 사물에 해당한다.

여기서 '음식', '과일'은 모두 클래스다.

  • 음식은 과일의 상위 개념이고, 과일은 음식의 하위 개념이다.
  • 음식은 과일의 상위 클래스(superclass)이고, 과일은 음식의 하위 클래스(subclass)라고 표현한다.

하위 개념(하위 클래스)는 상위 개념(상위 클래스)를 포함하면서 더 구체적인 개념을 추가한다.

  • 음식: 먹을 수 있다
  • 과일: 먹을 수 있다 + 나무에서 열린다
  • 귤류: 먹을 수 있다 + 나무에서 열린다 + 말랑한 껍질 속에 신맛이 나는 과육이 들어있다

하위 클래스로 갈수록 상위 클래스의 속성을 '상속'하면서 더 구체적인 요건이 추가된다. 하위 클래스가 상위 클래스보다 구체적일지라도 결국은 추상적인 개념이다.

귤류 클래스의 감귤, 자몽, 천혜향은

  • 음식에 속하므로 먹을 수 있다
  • 과일에 속하므로 나무에서 열린다
  • 귤류에 속하므로 말랑한 껍질 속에 신맛이 나는 과육이 들어있다

감귤, 자몽, 천혜향은 모두 인스턴스다. 음식, 과일, 귤류 클래스들의 속성을 갖는 구체적인, 실존하는 개체이기 때문이다.

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

  • 이런 이유로 하나의 인스턴스는 같은 레벨에 있는 서로 다른 여러 클래스의 인스턴스가 될 수 있다.
  • 남자면서, 학생이고, 한국인이다.

프로그래밍 언어의 접근 방식은 정반대이다.

  • 사용자가 직접 클래스를 정의해야 한다.
  • 클래스를 바탕으로 인스턴스를 만들 때 클래스의 속성을 갖는다.

또한 하나의 인스턴스는 하나의 클래스만을 바탕으로 만들어진다.

  • 어떤 인스턴스가 다양한 클래스에 속할 수 있다. 하지만 인스턴스 입장에서 클래스는 '직계존속'이다.
  • 다중 상속을 지원하든 그렇지 않든 결국 인스턴스를 생성할 때 호출할 수 있는 클래스는 오직 하나일 수밖에 없다.

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

자바스크립트의 클래스

자바스크립트는 프로토타입 기반 언어다. 따라서 클래스 개념이 존재하지 않는다.

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

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

  • Array를 클래스라고 보면, Array.prototype 객체 내부 요소들이 인스턴스에 '상속'된다고 볼 수 있다. (인스턴스가 참조하는 것을 상속한다고 보는 것)
  • 상속이 아닌 프로토타입 체이닝에 의한 참조가 맞지만 결과적으로 동일하게 동작하므로 이렇게 이해할 수 있다는 것이다.
  • 한편 Array 내부 프로퍼티들 중 prototype 프로퍼티를 제외한 나머지는 인스턴스에 상속되지 않는다.(인스턴스가 참조하지 않는다)

인스턴스에 상속되는지(인스턴스가 참조하는지) 여부에 따라 스태틱 멤버와 인스턴스 멤버로 나뉜다.(다른 언어의 클래스 구성 요소에 대한 정의를 차용하여 분류한 것) 하지만 인스턴스에서도 직접 메서드를 정의할 수 있기 때문에 인스턴스 멤버인 '인스턴스 메서드'는 프로토타입에 정의된 메서드를 지칭하는 것인지, 인스턴스에 정의한 메서드를 지칭하는 것인지에 대한 혼란을 야기한다.

따라서 '인스턴스 메서드' 대신 자바스크립트의 특징을 살린 '프로토타입 메서드'라고 부르는 것이 좋다.

생성자 함수 Array와 인스턴스 예시로 다시 알아보자.

생성자 함수 Array를 클래스라고 보면

  • prototype 프로퍼티에 있는 여러 멤버들은 프로토타입 멤버가 된다.
    • 메서드의 경우 프로토타입 메서드
  • Array에서 prototype 프로퍼티를 제외한 나머지 프로퍼티들은 모두 스태틱 멤버가 된다.

클래스 관점에서 바라본 프로토타입 시스템

예제를 통해 클래스 관점에서 바라본 프로토타입 시스템을 좀 더 살펴보자.

var 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;
};

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
  • Rectangle 생성자 함수를 new 연산자와 함께 호출하여 생성된 인스턴스를 rect1에 할당한다.
  • 프로토타입 객체에 할당한 getArea 메서드는 인스턴스가 마치 자신의 것처럼 호출할 수 있다.
    • rect1.getArea는 사실 rect1.__proto__.getArea에 접근한다.
    • __proto__를 생략하므로 this가 rect1인 상태로 실행된다.
    • 결과로 rect1.width * rect1.height 계산값이 반환된다.
    • getArea 메서드는 프로토타입 메서드다.
  • rect1 인스턴스에서 isRectangle 메서드에 접근하려고 한다.
    • rect1 인스턴스에서 해당 메서드가 존재하는지 검색한다.
    • 찾지 못하여 rect1.__proto__(Rectangle.prototype)에서 검색한다.
    • 찾지 못하여 rect1.__proto__.__proto__(Object.prototype)에서 검색한다.
    • 결국 프로토타입 체인에서 찾지 못하므로 undefined를 실행하라는 코드가 된다. 함수가 아니어서 실행할 수 없다는 에러가 발생한다.
    • isRectangle 메서드는 스태틱 메서드다.

위 예제를 통해 프로토타입 메서드와 스태틱 메서드를 확인하였다.

  • 프로토타입 메서드는 인스턴스에서 직접 호출할 수 있다.(프로토타입 체이닝)
  • 스태틱 메서드는 인스턴스에서 직접 호출할 수 없고, 생성자 함수를 this로 해서 호출할 수 있다.

정리하자면

  • 인스턴스에서 직접 호출할 수 있는 메서드가 프로토타입 메서드다.
  • 인스턴스에서 직접 호출할 수 없는 메서드가 스태틱 메서드다.
  • 스태틱 메서드는 생성자 함수를 this로 해야만 호출할 수 있다.

위 예제를 통해 클래스가 추상적일 수도, 구체적일 수도 있다는 것을 알 수 있다.

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

ES6의 클래스 및 클래스 상속

ES6에서 도입된 클래스 문법을 ES5에서의 생성자 함수 및 프로토타입과 비교하며 알아보자.

var ES5 = function (name) {
    this.name = name;
};
ES5.staticMethod = 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(es5Instance.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(es6Instance.method()); // es6 method
  • class 뒤에 바로 {}가 등장하는데, 이 중괄호 내부가 클래스 본문 영역이다.
  • constructor 뒤에 바로 () {가 등장한다.
    • 클래스 본문에서는 function 키워드를 생략해도 모두 메서드로 인식한다.
    • constructor 메서드는 ES5의 생성자 함수와 동일한 역할을 수행한다.
  • static 키워드는 해당 메서드가 스태틱 메서드임을 알려준다.
    • ES5에서 생성자 함수에 바로 할당하는 메서드와 동일하게 생성자 함수(클래스) 자신만 호출할 수 있다.
  • method 메서드
    • 자동으로 생성자 함수(클래스)의 prototype 객체 내부에 할당되는 메서드이다.
    • ES5.prototype.method와 동일하게 인스턴스가 프로토타입 체이닝을 통해 마치 자신의 것처럼 호출할 수 있는 메서드이다.

클래스 상속을 알아보자.

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());
    }
};

var sq = new Square(10);
sq.getArea();  // 'size is: 100'

Square 클래스를 Rectangle 클래스를 상속하는 서브클래스로 만들기 위해 class 뒤에 extends Rectangle을 추가한다. 이것만으로 상속 관계 설정이 끝난다.

  • constructor 메서드 내부에서는 super 키워드를 함수처럼 사용할 수 있다.
    • 이 함수는 슈퍼클래스의 constructor 메서드를 실행한다.
  • constructor 메서드를 제외한 다른 메서드에서는 super 키워드를 마치 객체처럼 사용할 수 있다.
    • 이 경우 super 객체는 슈퍼클래스.prototype을 참조한다.
    • super로 호출한 메서드의 this는 super가 아니라 원래의 this를 그대로 따른다.
    • sq.getArea()의 경우 this는 sq가 된다.

참고

  • 코어 자바스크립트 - 정재남
profile
캐비지 개발 블로그입니다. :)

0개의 댓글