들어가기 전에

자바스크립트 개발자라면 알아야 할 33가지 개념 #14 자바스크립트 클래스의 내면

자바스크립트 클래스는 절대 특별한 것이 아니고 그냥 기존에 존재하던 프로토타입 기반의 상속과 생성자 함수의 문법적인 포장지입니다. JS 클래스 뒤에 숨은 아이디어를 이해하기 위해서, 우리는 생성자 함수와 프로토타입 그리고 다른 연관된 개념들을 이해할 필요가 있습니다.

1. 생성자 함수

자바스크립트는 모든 것이 함수인 함수형 언어이기 때문에 자바스크립트내의 기능에서 클래스를 가지기 위해서는 생성자 함수가 사용됩니다. 생성자 함수가 어떻게 이용되는지 확인해봅시다.

function Vehicle(make, model, color) {
  this.make = make,
  this.model = model,
  this.color = color,
  this.getName = function () {
    return this.make + " " + this.model;
  }
}

위의 함수는 자바스크립트 클래스의 생성자와 거의 흡사한 기능을 제공합니다. Vehicle 타입의 오브젝트를 제공하기 위해서, 우리는 다음과 같은 코드를 작성합니다.

let car = new Vehicle("Toyota", "Corolla", "Black");
let car2 = new Vehicle("Honda", "Civic", "White");

완벽합니다. 이제 코드 한 줄이면 Vehicle 타입의 오브젝트를 우리가 원하는 만큼 만들 수 있습니다.

하지만 기다려보세요. 이 기술에는 몇가지 문제점이 있습니다.

우리가 new Vehicle()이라는 코드를 작성할 때, 자바스크립트 엔진이 실제로 하는 일은 우리의 각 오브젝트에 대해서 Vehicle 생성자 함수를 복사하는 일입니다. 각각 그리고 모든 프로퍼티 그리고 메소드가 Vehicle의 새로운 인스턴스에 복사됩니다. 그래서 이게 무슨 문제가 있냐고요?

문제는 바로 우리는 우리 생성자 함수의 멤버 함수가 모든 오브젝트에서 반복되는 것을 원하지 않는다는 것이죠. 이건 중복된 코드를 계속 생성하는 것입니다. 또 다른 문제는 우리가 새로운 프로퍼티나 메소드를 존재하는 생성자 함수(constructor function)에 추가할 수 없다는 것입니다. 다음과 같이요.

car2.year = "2012"
// (역자 주: 댓글에 이 부분을 지적하는 사람이 있었습니다. 
// '멀쩡하게 동작하는데 왜 동작이 안된다고 했느냐, 무슨 의미냐'라는 식으로 물었고
// 저자는 '존재하는 오브젝트에는 가능하지만 생성자 함수에는 추가할 수 없다'라고 하였습니다.)

year 프로퍼티를 추가하려면 생성자 함수 자체에 추가해야 할 것입니다.

function Vehicle(make, model, color, year) {
        this.make = make,
        this.model = model,
        this.color = color,
        this.year = year,
        this.getName = function () {
            return this.make + " " + this.model;
        }
}

2. 프로토타입

자바스크립트에서 새로운 함수가 만들어질때마다, 자바스크립트 엔진은 기본으로 prototype 프로퍼티를 추가합니다. 이 프로토타입은 우리가 "프로토타입 오브젝트(prototype object)"라고 부르는 것입니다. 기본으로 이 프로토타입 오브젝트는 우리 함수를 다시 가리키는 생성자 프로퍼티와 오브젝트인 또 다른 프로퍼티 __proto__를 갖고 있습니다. 다음 내용을 봅시다.

prototype_vehicle.png

__proto__ 프로퍼티는 'dunder proto'라고 불립니다. 그리고 이 프로퍼티는 우리의 생성자 함수의 프로퍼티를 가리킵니다.

'dunder'라는 말은 파이썬에서 왔습니다. 변수 양 끝이 __ 로 묶여 있는 변수를 'dunder' 프로퍼티라고 합니다.

생성자 함수의 새로운 인스턴스가 생성될 때마다, 다른 프로퍼티와 메소드와 함께 이 프로퍼티(__ proto__)도 인스턴스에 복사됩니다.

__proto__.png

이 프로퍼티 오브젝트는 생성자 함수에 새로운 프로퍼티와 메소드를 추가하기 위해 사용될 수 있습니다. 다음 문법을 사용하면 모든 생성자 함수 인스턴스에서 사용 가능할 것입니다.

car.__proto__.year = "2016"; // 원문은 car.prototype.year = "2016" 이었으나 현재는 동작하지 않는 코드입니다.

프로토타입은 멋집니다 하지만 프로토타입 접근법을 사용하는동안 몇가지 유의해야할 점이 있습니다. 프로토타입 프로퍼티와 메소드는 모든 생성자 함수 인스턴스 간에 공유가 되지만 생성자 함수의 인스턴스 중 하나에서 어떤 프리미티브 프로퍼티를 변경하였을 때는, 해당 인스턴스에만 반영이 되고, 다른 인스턴스들 사이에서는 반영이 안된다는 것입니다.

reflect-only-one.png

또 하나 알아둬야 할 것은, 참조 타입 프로퍼티는 항상 모든 인스턴스 사이에서 공유된다는 것입니다. 예를 들면, 배열 타입의 프로퍼티의 경우, 만일 생성자 함수의 한 인스턴스에 의해 수정되었다면, 모든 인스턴스에 대해 수정됩니다.

array-property-ref.png

프로토타입에 대해 매우 흥미롭고 더욱 상세한 아티클이 여기있습니다.

3. 클래스

이제 우리는 생성자 함수와 프로토타입에 대해 이해했습니다. 앞의 것들을 제대로 이해했다면, 이제 클래스를 이해하는 것은 쉽습니다. 왜냐구요? 자바스크립트 클래스는 프로토타입의 힘을 활용함으로써 새롭게 생성자 함수를 작성하는 방법에 지나지 않기 때문입니다. 예제를 봅시다.

class Vehicle {
    constructor(make, model, color) {
        this.make = make;
        this.model = model;
        this.color = color;
    }

    getName() {
        return this.make + " " + this.model;
    }
}

Vehicle 클래스의 새 인스턴스를 만들기 위해 우리는 다음과 같은 코드를 실행할 것입니다.

let car = new Vehicle("Toyota", "Corolla", "Black");

처음에 우리가 생성자 함수를 배우기 위해 작성했던 코드랑 비교하면, 사실 별 다를 것이 없습니다. 매우 비슷합니다.

위의 코드를 작성함으로써, 우리는 Vehicle이라는 이름을 가진 변수를 만들었습니다. 이 변수는 클래스에 정의된 생성자 함수를 참조합니다. 또한 우리는 Vehicle 변수의 프로토타입에 메소드도 추가했었습니다. 아래와 같이 말입니다.

function Vehicle(make, model, color) {
    this.make = make;
    this.model = model;
    this.color = color;
}

Vehicle.prototype.getName = function () {
    return this.make + " " + this.model;
}

let car = new Vehicle("Toyota", "Corolla", "Black");

우리가 배운 내용이 증명하는 것은, 클래스는 그저 생성자 함수를 작성하는 새로운 방법이란 것입니다. 하지만 진짜 클래스처럼 만들기 위해서 새로 소개된 몇가지 것들과 규칙들이 더 있습니다.

  1. 클래스에서는 constructor를 작동시키기 위해 new 키워드가 필요합니다. 이것이 의미하는 것은 생성자는 우리가 다음과 같이 코드를 작성했을 때만 호출시킬 수 있다는 것입니다.
let car = new Vehicle("Toyota", "Corolla", "Black");

하지만 생성자 함수에서는 다음과 같은 코드도 가능합니다.

constructor-function-without-new.png

하지만 클래스에서는 위와 같은 방식을 시도하면 다음과 같은 결과가 나타납니다.

class-constructor.png

  1. 클래스 메소드는 enumerable하지 않습니다. 자바스크립트에서, 오브젝트의 각 프로퍼티는 enumerable 플래그를 갖고 있습니다. 이 플래그는 그 프로퍼티에서 어떤 명령이 실행되는지 유효성을 정의합니다. 클래스는 prototype에 정의된 모든 메소들에 대해 이 플래그를 false로 설정합니다.

역자 주: enumerable 플래그의 용도는 Object.keys()와 같은 메소드를 실행했을 때 프로퍼티가 'for... loop'의 반복에 포함되는지를 결정하는 것이라 생각하면 편할 것 같습니다. 자세한 설명은 이 링크에서 확인하시면 됩니다.

  1. constructor를 클래스에 추가하지 않는다면, 기본 값으로 빈 constructor가 자동으로 추가됩니다. 다음과 같습니다.
constructor() {}
  1. 클래스 내부의 코드는 항상 strict 모드입니다. 이러한 점은 코드를 작성하는 도중 에러를 날림으로써, 오타 또는 문법적인 에러가 없는 코드를 작성하는 것을 돕습니다. 실수로 어딘가에서 참조되는 코드를 지웠을 때도 알아채기 쉽습니다.

  2. 클래스 선언은 hoisted되지 않습니다. 자바스크립트에서 호이스팅은 모든 선언문들이 자동적으로 현재 스코프의 가장 위로 올라가는 것입니다. 호이스팅은 변수나 함수가 실제로 선언되기 전에 쓰이게 만들어 버그와 의도치 않은 동작을 유발합니다.

호이스팅의 예는 다음과 같습니다.

hoisting1.png

이 코드는 동작합니다.

hoisting2.png

이 코드는 동작하지 않습니다.

  1. 클래스는 오브젝트 리터럴이나 생성자 함수 같은 것을 프로퍼티의 값으로 할당하는 것을 허락하지 않습니다. 함수나 getters/setters 같은 것만 가질 수 있습니다. 그러니 클래스에서 property:value 할당을 바로 하지마세요.

클래스 특성

1. 생성자

클래스 선언에서, 생성자는 특별한 함수입니다. 생성자는 클래스 자체를 표현하는 함수를 정의합니다. new 키워드를 쓰면 생성자는 자동으로 호출됩니다.

let car = new Vehicle("Honda", "Accord", "Purple");

생성자는 클래스의 생성자를 확장된 형태로 부르기 위해 super 키워드를 사용할 수 있습니다.

클래스는 1개 이상의 생성자 함수를 소유할 수 없습니다.

2. 정적 메소드

정적 메소드는 프로토타입 위에 있는 것이 아닌 클래스 자체에 있는 함수입니다. prototype에서 정의된 메소드들은 정적 메소드와 다릅니다.

정적 메소드들은 static 키워드를 사용하여 선언됩니다. 정적 메소드의 대부분은 공용 함수(utility functions)를 만들기 위해 사용됩니다. 정적 메소드들은 클래스의 인스턴스를 생성하지 않고 호출됩니다. 아래의 예제를 보면 이해가 되실 겁니다.

class Vehicle {
    constructor(make, model, color) {
        this.make = make;
        this.model = model;
        this.color = color;
    }

    getName() {
        return this.make + " " + this.model;
    }

    static getColor(v) {
        return v.color;
    }
}

let car = new Vehicle("Honda", "Accord", "Purple");

Vehicle.getColor(car); // "purple"

기억하셔야 할 점은, 정적 메소드는 클래스 인스턴스에서 호출할 수 없다는 점입니다.

3. Getters/Setters

클래스는 또 프로퍼티의 값을 가져오거나/프로퍼티의 값을 설정하기 위해 getters/setters를 가질 수 있습니다. 예제는 아래와 같습니다.

class Vehicle {
    constructor(model) {
        this.model = model;
    }

    get model() {
        return this._model;
    }

    set model(value) {
        this._model = value;
    }
}

내부에서(under the hood), getters/setters는 클래스 prototype에 정의되어 있습니다.

4. Subclassing

Subclassing은 자바 클래스에서 상속을 구현할 수 있는 방법입니다. extends라는 키워드는 클래스의 자식 클래스를 만들 때 사용됩니다.

예제를 봅시다.

class Vehicle {
    constructor(make, model, color) {
        this.make = make;
        this.model = model;
        this.color = color;
    }

    getName() {
        return this.make + " " + this.model;
    }
}

class Car extends Vehicle{
    getName(){
        return this.make + " " + this.model +" in child class.";
    }
}

let car = new Car("Honda", "Accord", "Purple");

car.getName(); // "Honda Accord in child class."

위 소스에서 getName() 함수를 불러올 때, 자식 클래스에서 불러진 것을 볼 수 있습니다.

때때로 우리는 베이스 클래스의 함수를 불러올 필요가 있을 때가 있습니다. 우린 자식 클래스의 메소드 내에서 베이스 클래스의 메소드를 호출하기 위해 super 키워드를 사용합니다.

class Car extends Vehicle{
    getName(){
        return super.getName() + " - called base class function from child class.";
    }
}

extends.png

자바스크립트 클래스 뒤에 있는 아이디어를 설명해보았습니다. 그리고 마지막으로, 클래스의 여러가지 특성에 대해서도 알아보았습니다.

이번 토픽에서 배운 상세사항 중에 대부분을 잊어버렸겠지만, 계속해서 만들어지는 자바스크립트 고수들의 아티클을 읽으면서, 이번 토픽 뒤에 있는 아이디어에 대해 더 배울 수 있을 것입니다.