6.6 ECMAScript6 - Class

클래스

es6 Logo

Javascript는 프로토타입 기반(prototype-based) 객체지향형 언어다. 비록 다른 객체지향 언어들과의 차이점에 대한 논쟁들이 있긴 하지만, Javascript는 강력한 객체지향 프로그래밍 능력들을 지니고 있다.

프로토타입 기반 프로그래밍은 클래스가 필요없는(class-free) 객체지향 프로그래밍 스타일로 프로토타입 체인과 클로저 등으로 객체 지향 언어의 상속, 캡슐화(정보 은닉) 등의 개념을 구현할 수 있다.

var Person = function(name) {
  this.name = name;
}
Person.prototype.walk = function() {
  console.log(this.name + ' is walking.');
}

var me = new Person('Lee');

console.log(me instanceof Person); // true
me.walk(); // Lee is walking.

var you = new Person('Kim');
you.walk(); // Kim is walking.

prototype

프로토타입 객체에 의한 속성의 상속

하지만 클래스 기반 언어에 익숙한 프로그래머들은 혼란을 일으킬 수 있으며 JavaScript를 어렵게 느끼게하는 하나의 장벽처럼 인식되었다.

ES6의 클래스는 기존 prototype 기반 객체지향 프로그래밍보다 클래스 기반 언어에 익숙한 프로그래머가 보다 빠르게 학습할 수 있는 단순명료한 새로운 문법을 제시하고 있다. ES6의 클래스가 새로운 객체지향 모델을 제공하는 것이 아니며 사실 클래스도 함수이고 기존 prototype 기반 패턴의 Syntactic sugar일 뿐이다.

1. 클래스 정의 (Class Definition)

ES6 클래스를 정의하기 위해서는 class 키워드를 사용한다. name 속성과 walk 메서드를 갖는 Person 클래스를 정의해 보자.

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

  walk() {
    console.log(`${this._name} is walking.`);
  }
}

let me = new Person('Lee');

console.log(me instanceof Person); // true

me.walk(); // Lee is walking.

클래스는 메서드만을 포함할 수 있다. 클래스 바디에 멤버 변수를 선언하면 SyntaxError가 발생한다.

class Foo {
  let name = ''; // SyntaxError

  constructor() {}
}

따라서 멤버 변수의 선언과 초기화는 반드시 constructor 내부에서 실시한다.

class Foo {
  constructor(name) {
    this.name = name; // OK
  }
}

console.log(new Foo('Lee')); // Foo { name: 'Lee' }

constructor 내부에서 선언한 멤버 변수 name은 this(클래스 Foo의 인스턴스)에 바인딩되어 있으므로 언제나 public이다.

class Foo {
  constructor(name) {
    this.name = name; // OK
  }
}

const foo = new Foo('Lee');
console.log(foo.name); // Lee

ES6 class 사양은 private, public, protected 키워드를 지원하지 않는다. Symbol 또는 WeakMap을 사용하여 private 멤버 변수를 정의할 수 있다.

constructor 메서드는 객체를 생성하고 초기화하기 위한 특수한 메서드이다. constructor 메서드는 클래스 내에 한 개만 존재할 수 있으며 만약 클래스가 2개 이상의 constructor 메서드를 포함하면 SyntaxError가 발생한다.

constructor 메서드는 생략할 수 있다. constructor를 생략하면 constructor() {}를 포함한 것과 동일하게 동작하지만 객체의 생성과 동시에 초기화는 할 수 없다.

class Foo {}

const foo = new Foo();
console.log(foo); // Foo {}

foo.num = 1;      // 동적 속성 추가
console.log(foo); // Foo { num: 1 }

class Bar {
  constructor(num) {
    this.num = num;
  }
}

console.log(new Bar(1)); // Bar { num: 1 }

new 연산자를 사용하지 않고 객체를 생성하면 에러가 발생한다.

class Foo {}

const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'

ES6 Class는 함수이다.

class Foo {}

console.log(typeof Foo);       // function
console.log(typeof new Foo()); // object

자바스크립트는 ES6의 let, const를 포함하여 모든 선언(var, let, const, function, function*, class)을 호이스팅(Hoisting)한다.

하지만 class 선언문 이전에 class를 참조하면 참조하면 ReferenceError가 발생한다. 이는 코드블록의 시작에서 class의 선언까지 일시적 사각지대(Temporal Dead Zone; TDZ)에 빠지게 되기 때문이다.

new Foo(); // ReferenceError

class Foo {}

클래스 표현식으로도 클래스를 정의할 수 있다. 함수와 마찬가지로 클래스는 이름을 가질 수도 갖지 않을 수도 있다. 이때 클래스가 할당된 변수를 사용해 클래스를 생성하지 않고 기명 클래스의 클래스명을 사용해 클래스를 생성하면 에러가 발생한다. 이는 함수와 마찬가지로 클래스 표현식에서 사용한 클래스명은 외부 코드에서 접근 불가능하기 때문이다. 자세한 내용은 함수표현식(Function expression)을 참조하기 바란다.

const Foo = class {}

const foo = new Foo();
console.log(foo); // Foo {}

const Bar = class MyClass {}

const bar = new Bar();
console.log(bar);  // MyClass {}

new MyClass(); // ReferenceError: MyClass is not defined

Class body에는 method(Static method, Prototype method)나 constructor와 같은 class member를 정의한다.

2. 정적 메서드 (Static method)

static 키워드는 클래스의 정적(static) 메서드를 정의한다. 정적 메서드는 클래스의 인스턴스화(instantiating)없이 호출하며 클래스의 인스턴스로 호출할 수 없다. 정적 메서드는 어플리케이션을 위한 유틸리티(utility) 함수를 생성하는데 주로 사용된다.

class Foo {
  constructor(prop) {
    this.prop = prop;      
  }
  static staticMethod() {
    return 'staticMethod';
  }
  prototypeMethod() {
    return 'prototypeMethod';
  }
}

const foo = new Foo(123);

console.log(Foo.staticMethod());
console.log(foo.staticMethod()); // Uncaught TypeError: foo.staticMethod is not a function

위에서도 언급했지만 사실 Class도 함수이고 기존 prototype 기반 패턴의 Syntactic sugar일 뿐이다.

위 예제를 ES5로 표현해보면 아래와 같다.

var Foo = (function () {
  function Foo(prop) {
    this.prop = prop;
  }
  Foo.staticMethod = function () {
    return 'staticMethod';
  };
  Foo.prototype.prototypeMethod = function () {
    return 'prototypeMethod';
  };
  return Foo;
}());

var foo = new Foo(123);

console.log(Foo.staticMethod());
console.log(foo.staticMethod()); // Uncaught TypeError: foo.staticMethod is not a function

ES5로 표현한 위 코드는 ES6 Class로 표현한 코드와 정확히 동일하게 동작한다.

정적 메서드는 클래스의 인스턴스화(instantiating)없이 호출하며 클래스의 인스턴스로 호출할 수 없는 이유에 대해 알아보자. prototypeJavaScript OOP에 대한 사전 지식이 필요하므로 아직 이에 대한 학습이 안되어 있으면 skip하기 바란다.

우선 FOO는 함수이다. Class도 사실 함수라고 위에서 언급하였다.

class Foo {
  constructor() {}
}

console.log(typeof Foo); // function

함수 객체는 prototype 프로퍼티를 갖는데 일반 객체의 [[Prototype]] 프로퍼티와는 다른 것이며 일반 객체는 prototype 프로퍼티를 가지지 않는다.

함수 객체만이 가지고 있는 prototype 프로퍼티는 함수 객체가 생성자로 사용될 때 이 함수를 통해 생성된 객체의 부모 역할을 하는 객체를 가리킨다. 즉 Foo는 함수이고 생성자 함수로 사용되므로 함수 Foo의 prototype은 함수 Foo로 생성되는 객체 foo의 부모 역할을 한다.

console.log(Foo.prototype === foo.__proto__); // true

그리고 prototype이 가지고 있는 constructor 프로퍼티는 함수 객체 자신을 가리킨다.

console.log(Foo.prototype.constructor === Foo); // true

정적 메서드인 staticMethod는 함수 객체 Foo의 member, 프로토타입 메서드인 prototypeMethod는 Foo.prototype의 member가 되므로 staticMethod는 foo에서 호출할 수 없게 된다.

class Foo {
  constructor(prop) {
    this.prop = prop;      
  }
  static staticMethod() {
    return 'staticMethod';
  }
  prototypeMethod() {
    return 'prototypeMethod';
  }
}
const foo = new Foo(123);

console.log(typeof Foo.staticMethod); // function
console.log(Foo.staticMethod());      // staticMethod

console.log(typeof Foo.prototype.prototypeMethod); // function
console.log(foo.prototypeMethod());                // prototypeMethod

console.log(foo.staticMethod()); // TypeError: foo.staticMethod is not a function

class prototype

3. 클래스 상속 (Class Inheritance)

상속(또는 확장)은 코드 재사용의 관점에서 매우 유용하다. 새롭게 정의할 클래스가 기존에 있는 클래스와 매우 유사하다면, 상속을 통해 다른 점만 구현하면 된다. 코드 재사용은 개발 비용을 현저히 줄일 수 있는 잠재력이 있기 때문에 매우 중요하다.

extends 키워드는 부모 Class(Base class)를 상속하는 자식 Class(Sub class)의 생성을 위해 class 선언에 사용된다.

// Base class
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toString() {
    return `(${this.x}, ${this.y})`;
  }
}

// Sub class
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // (A)
    this.color = color;
  }
  toString() {
    return `${super.toString()} in ${this.color}`; // (B)
  }
}

const cp = new ColorPoint(25, 8, 'green');
console.log(cp.toString()); // (25, 8) in green

console.log(cp instanceof ColorPoint); // true
console.log(cp instanceof Point);      // true

ColorPoint는 Point를 상속받은(파생된 또는 확장한) 자식 class이다.

super 키워드는 부모 Class(Base Class)의 참조(Reference)이다. 위 예제의 경우 super는 ColorPoint의 부모 class인 Point를 가리키며, 부모 class의 프로퍼티를 참조하기 위해 사용한다. (B)

(A)의 super 메서드는 자식 class의 constructor 내부에서 부모 class의 constructor(super-constructor)를 호출한다. 자식 class는 constructor를 생략하지 않는 경우, 자신의 constructor내에서 반드시 super()를 호출하여야 한다.

자식class의 constructor에서 super()를 호출하지 않으면 ReferenceError가 발생한다.

class Foo {}

class Bar extends Foo {
  constructor() { // ReferenceError: this is not defined
  }
}

new Bar();

자식class에서 this를 사용하기 위해서는 반드시 super()를 호출하여야 한다.

class Foo {}

class Bar extends Foo {
  constructor(num) {
    // console.log(this); // ReferenceError: this is not defined
    super();
    this.num = num;    // OK
    console.log(this); // Bar { num: 5 }
  }
}

new Bar(5);

prototype 관점에서 바라보면 자식 class의 [[prototype]]은 부모 class이다.

class Foo {}

class Bar extends Foo {
  constructor(num) {
    super();
    this.num = num;
  }
}

console.log(Bar.__proto__ === Foo); // true
console.log(Bar.prototype.__proto__ === Foo.prototype); // true

class-prototype-relation

자식 class의 [[prototype]]은 부모 class이다

이것은 Prototype chain에 의해 부모class의 정적 메서드도 상속됨을 의미한다.

class Foo {
  static staticMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

console.log(Bar.staticMethod()); // 'hello'

자식class의 정적 메서드 내부에서도 super를 사용하여 정적 메서드를 호출할 수 있다.

class Foo {
  static staticMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static staticMethod() {
    return `${super.staticMethod()}, too`;
 }
}

console.log(Bar.staticMethod()); // 'hello, too'

class-prototype-chain

prototype chain에 의한 메서드의 상속

Reference

Back to top
Close