Published on

ES6) Class

Authors
  • avatar
    Name
    Hoehyeon Jung
    Twitter

Class

객체지향으로 유명한 언어들은 대체로 Class를 기반으로 객체 지향을 구현하였다. 하지만 javascript의 경우 prototype 기반의 객체 지향 프로그래밍을 구현하였기 때문에 기존의 class 기반의 언어들과는 상속 방식에 있어 차이가 있었다. 이러한 요구에 부응한 것인지, ES6에 들어서면서 javscript에도 class가 도입되었다. 하지만 자바스크립트는 prototype 기반으로 구성되어 있기 때문에 class 역시 기본적으로 prototype으로 상속을 구현되어 있다. 때문에 누군가는 단순히 class를 syntax sugar라고 칭하기도 한다. 그렇다면 이를 이해하기 위해 class의 기본적인 사용법과 차이점에 대해 알아보자.

class의 기본 사용법

ES6의 class는 다른 객체지향 언어들처럼 기본적인 선언과 상속이 지원된다.

const me = new Person('jung', 17); // reference error

// class declarations
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

// class expressions
const location = class {
  constructor(lat, lng) {
    this.lat = lat;
    this.lng = lng;   
  }
}

const me = new Person('jung', 17);

클래스를 선언하면 기본적으로 new로 선언해야 한다. 클래스 선언할 때는 함수를 선언할 때와 유사하게 class [이름] {...};와 같이 선언 가능하다. 하지만 클래스 선언식은 함수 선언식과 다르게 hoisting이 발생하기 않아 선언 전에 호출할 경우 reference error가 발생한다.

constructor

생성자는 인스턴스를 생성할 때 클래스 내부 변수들을 초기화 하는 등의 목적을 위해 사용하는 메서드이다.

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  // syntax error
  // constructor(name, age) {
  //   this.name = name;
  //   this.age = age;
  // }
}

class Driver extends Person {
  constructor(name, age, car) {
    super(name, age);
    this.car = car;
  }
}

클래스에는 생성과 동시에 초기화를 돕는 생성자를 하나씩 선언할 수 있다. 두 개 이상의 생성자를 선언하려고 하면 SyntaxError가 발생할 것이다. extends를 통해 부모 클래스를 상속받게 된다. 또한 이러한 부모 클래스에게도 값을 전달하는 super()라는 메소드가 있는데 생성자에서 이를 선언할 수 있다.

Method

class Polygon {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  showSize() {
    console.log(`width: ${this.width}, height: ${this.height}`);
  }

  // getter
  get area() {
    return this.height * this.width;
  }

  // setter
  set size({width, height}) {
    this.width = width;
    this.height = height;
  }

  // static 
  static getDistance(a, b) {
    return Math.hypot(a.x-b.x, a.y-b.y);
  }
}
const rectangle = new Polygon(3, 4);

const rectangleArea = rectangle.area;

rectangle.size = {width: 4, height: 4};

Polygon.getDistance({x: 1, y: 2}, {x: 2, y: 3});

기본적으로 클래스를 내에는 다양한 방식의 메서드를 사용할 수 있다. 기본적인 getter, setter 메서드, 일반적으로 public하게 접근할 수 있는 메서드도 있다. getter 메서드는 값을 가져오는 것이고 setter는 메서드의 파라미터 값을 지정하는 메서드이다. 또한 ES6에 들어서면서 메서드 표현식도 간결해졌다. 기존에 메서드로 지정하기 위해 함수 표현식과 같이 사용할 메서드의 이름과 함수를 나눠서 선언해야 했다면, ES6 이후로 표현을 간결하게 할 수 있다.

static으로 선언한 메서드는 새로운 인스턴스로 선언하면 클래스 내에 선언된 메서드를 사용할 수 없다. 이렇게 선언된 메서드 static 메서드는 클래스를 이용해서 사용할 수 있는데 이러한 방식을 통해 주로 utility 함수 등을 만들어 사용한다.

Prototype vs Class

혹자는 ES6에서 새로이 생성된 문법들에 대해 의구심을 가지고 Class는 단순한 문법 설탕에 불과하다고 말한다. 문법 설탕이란 것은 기본적으로 이전의 문법을 통해 수고를 들이지 않고 해당 구현사항을 만족할 수 있는 것이다. 하지만 실제로 class는 기존의 Prototype만으로 구현한 것과 차이를 가지고 있다. 이러한 차이에 대해 알아보자.

new

function PersonES5(name) {
  if(!(this instanceof PersonES5)) {
    return new PersonES5();
  }
  this.name = name;
}

class PersonES6 {
  constructor(name) {
    this.name = name;
  }
}

const meES5 = PersonES5(name);
const meES6 = PersonES6(name); // TypeError

ES6 이전에는 생성자 함수는 기본적으로 함수이기 때문에 new 없이 호출이 가능했다. 따라서 이러한 문제를 방지하기 위해 위와 같이 new를 호출하지 않은 경우를 걸러서 새로이 new를 붙여서 호출한다. 하지만 ES6는 클래스의 인스턴스를 생성할 떄, new를 붙이지 않으면 TypeError를 반환한다.

extends & super

// ES5
function ParentES5(){
  this.a = 1;
}
function ChildES5(){
  const parentObj = Object.getPrototypeOf(this);
  for(let i in parentObj){
    this[i] = parentObj[i];
  }
  this.b = 2;
}
ChildES5.prototype = new ParentES5();
ChildES5.prototype.constructor = ChildES5;
const myChild = new ChildES5();
console.log(myChild.a, myChild.b);          // 1 2
console.log(myChild.hasOwnProperty('a'));   // true
console.log(myChild.hasOwnProperty('b'));   // true

// ES6
class ParentES6 {
  constructor(){
    this.a = 1;
  }
}
class ChildES6 extends ParentES6 {
  constructor(){
    super();
    this.b = 2;
  }
}
const yourChild = new ChildES6();
console.log(yourChild.a, yourChild.b);        // 1 2
console.log(yourChild.hasOwnProperty('a'));   // true
console.log(yourChild.hasOwnProperty('b'));   // true

console.log(Object.getPrototypeOf(yourChild).a);  // undefined
console.log(Object.getPrototypeOf(yourChild).hasOwnProperty('a'));  // false

ES5에서는 상속을 표현하기 위해 Prototype Chain을 이용해서 구현하였다. ES5에서는 또한 부모의 attribute를 가져오기 위해 getPrototypeOf 메서드로 Prototype으로 연결된 부모의 속성을 복사해서 가져온다. 따라서 실질적으로 상속의 개념이라기보단 부모 메서드 복사에 가깝다.

하지만, ES6에서는 super 메서드로 이를 좀 더 간결하게 가져올 수 있다. 또한 ES6에서의 상속은 Prototype 체인이 아니라 단순히 메서드나 속성만 상속하는 진정한 의미의 상속이라고 볼 수 있다.

Hoisting & Temporal Dead Zone

// hoisted as 'var a = undefined;'
console.log(a);
var a = 'a';
console.log(b); // ReferenceError
const b = 'b';

Hoisting은 ES6에 들어서면서 생긴 개념으로 varlet, const의 차이에서 발생한 개념이다. 기본적으로 ES5에서의 var은 해당 변수가 선언과 동시에 undefined로 초기화된다. 따라서 선언 이전에 값을 호출한다 하더라도 오류가 발생하지 않는다. 하지만 ES6부터 도입된 letconst는 선언 이전에 호출을 하면 ReferenceError가 발생한다. 이러한 문제가 function 선언식과 class 선언식에서도 적용된다. class 선언식도 선언 이전에 호출을 이룰 경우 ReferenceError가 발생한다. 이러한 내용에 대해서 흔히 hoisting이 이루어지지 않아 발생한 에러라고 알고 있지만 실상은 다르다.

function A(){ this.a = 1; }
{
  console.log(new A());  // A {a: 2}
  function A(){ this.a = 2; }
}
class B {
  constructor(){ this.a = 1; }
}
{
  console.log(new B());  // Uncaught ReferenceError: B is not defined
  class B {
    constructor(){ this.a = 2; }
  }
}

위의 예제를 보면, 생성자 함수 A에서는 console.log()로 호출 이전에 선언하더라도 함수 선언식은 hoisting으로 인해 값이 덮어 씌워진다. 하지만 class인 B를 살펴보자. 만일 hoisting이 이루어지지 않았다면 단순히 console.log()에서는 사전에 선언된 class B가 나와야 한다. 하지만 실제로는 ReferenceError가 발생하게 되는데, 이는 객체 내 class가 hoisting 되지만 선언 전에 호출되므로 발생하는 에러이다.

이렇게 hoisting은 실제로 이루어지지만 실제 참조할 때 에러를 뱉는 구간을 Temporal Dead Zone(이하 TDZ)라고 한다. let, const 등 ES6 이후에 생긴 class나 generator 등과 같은 것들은 전부 TDZ를 가지고 있다고 한다. hoisting을 통해 실제 값 선언은 hoisting되어 올라가지만 초기화되지 않으므로 이러한 값으로의 접근을 제한한다. 하지만 이런 값에 접근하는 순간 ReferenceError가 발생하는 것이고 이러한 오류가 발생하는 구간을 TDZ라고 하는 것이다.

마치며

ES6에서 새로 도입된 class를 기존에는 단순하게 사용하기만 했다면 이번 기회를 통해 좀 더 깊게 알아볼 수 있는 시간이었다. 기본적인 method나 사용법에서부터 prototype chaining과의 차이점까지 알 수 있는 시간이었다. 또한 기본적으로 참조했던 블로그들에 언급된 내용들에 대해 더 자세히 나와 있기 떄문에 시간되시면 꼭 한 번 읽어볼 것을 추천한다.

참조