ES6 Class 파헤치기

ES6 Class 문법

JavaScript **Class**는 ECMAScript 6을 통해 소개되었습니다. ES6의 Class는 기존 prototype 기반의 상속을 보다 명료하게 사용할 수 있도록 문법을 제공합니다. 이를 Syntatic Sugar라고 부르기도 합니다.

Syntatic Sugar : 읽고 표현하는것을 더 쉽게 하기 위해서 고안된 프로그래밍 언어 문법을 말합니다.

JavaScript를 ES6를 통해 처음 접하시는 분들은 알아두셔야할 것이 JavaScript의 Class는 다른 객체지향 언어(C++, C#, Java, Python, Ruby 등…)에서 사용되는 Class 문법과는 다르다는 것입니다. JavaScript에는 Class라는 개념이 없습니다.
Class가 없기 때문에 기본적으로 Class 기반의 상속도 불가능합니다. 대신 다른 언어에는 존재하지 않는 프로토타입(Prototype)이라는 것이 존재합니다. JavaScript는 이 prototype을 기반으로 상속을 흉내내도록 구현해 사용합니다. Prototype을 처음 접하시는 분은 “Prototype 이제는 이해하자”를 참고하시면 도움이 될것같습니다.

Class 정의

JavaScript에서 Class는 사실 함수입니다. 함수를 함수 선언함수 표현식으로 정의할 수 있듯이 class 문법도 class 선언class 표현식 두가지 방법으로 정의가 가능합니다.

JavaScript 엔진은 function 키워드를 만나면 Function 오브젝트를 생성하듯, class 키워드를 만나면 Class 오브젝트를 생성합니다. class는 클래스를 선언하는 키워드이고 Class 오브젝트는 엔진이 class 키워드로 생성한 오브젝트입니다.

Class 선언

함수 선언과 달리 클래스 선언은 호이스팅이 일어나지 않기 때문에, 클래스를 사용하기 위해서는 먼저 선언을 해야합니다. 그렇지 않으면 ReferenceError 가 발생합니다.

1
2
3
4
5
6
7
8
9
class People {
constructor(name) {
this.name = name;
}

say() {
console.log('My name is ' + this.name);
}
}

Class 표현식

Class 표현식은 이름을 가질 수도 있고 갖지 않을 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const People = class People {
constructor(name) {
this.name = name;
}

say() {
console.log('My name is ' + this.name);
}
}

const People = class {
constructor(name) {
this.name = name;
}

say() {
console.log('My name is ' + this.name);
}
}

constructor

constructor는 클래스 인스턴스를 생성하고 생성한 인스턴스를 초기화하는 역할을 합니다. new People() 코드를 실행하면 People.prototype.constructor가 호출됩니다. 이를 default constructor라고 하며 constructor가 없으면 인스턴스를 생성할 수 없습니다.

1
const people = new People('KimJongMin');

new People(‘KimJongMin’)을 실행하면 People 클래스에 작성한 constructor가 자동으로 호출되고 파라미터 값으로 ‘KimJongMin’을 넘겨 줍니다.

new 연산자가 인스턴스를 생성하는 것처럼 보이지만, 사실 new 연산자는 constructor를 호출하면서 파라미터를 넘겨주는 역할만 합니다. 호출된 constructor가 인스턴스를 생성하여 반환하면 new 연산자가 받아 new를 실행한 곳으로 반환합니다. 과정은 다음과 같습니다.

  1. new People(‘KimJongMin’)을 실행
  2. new 연산자가 constructor를 호출하면서 파라미터 전달
  3. constructor에 작성한 코드를 실행하기 전에 빈 Object 를 생성
  4. constructor 코드를 실행
  5. 생성한 Object(인스턴스)에 property 할당 (인스턴스를 먼저 생성했기 때문에 this로 Object 참조 가능
  6. 생성한 Object 반환

다음은 생성된 인스턴스의 구조입니다.

1
console.dir(people);

people 인스턴스의 **_proto_**는 People Class 오브젝트와 함께 생성된 Prototype object를 가리키고 있습니다. 결국 Class 문법을 이용한 코드를 prototype 기반의 코드로 변경하면 다음과 같습니다.

1
2
3
4
5
6
7
function People(name) {
this.name = name;
}

People.prototype.say = function () {
console.log('My name is ' + this.name);
};

Prototype 기반 상속(ES5)과 Class 기반 상속(ES6) 비교

먼저 ES5에서 Prototype을 사용하여 상속을 구현하는 방법을 살펴보고, 그 후 ES6에서 Class로 상속을 구현하는 형태를 보겠습니다.

ES5 Prototype 기반 상속

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Cat(name) {
this.name = name;
}

Cat.prototype.speak = function () {
console.log(this.name + ' makes a noise.');
};

function Lion(name) {
// `super()` 호출
Cat.call(this, name);
}

// `Cat` 클래스 상속
Lion.prototype = Object.create(Cat.prototype);
Lion.prototype.constructor = Lion;

// `speak()` 메서드 오버라이드
Lion.prototype.speak = function () {
Cat.prototype.speak.call(this);
console.log(this.name + ' roars.');
};

var lion = new Lion('Samba');
lion.speak();

[결과]
Sambda makes a noise.
Sambda roars.

new Lion()을 실행하면 Lion()이 호출되고, default constructor를 호출합니다. 그래서 Lion()을 생성자(constructor) 함수라고 합니다.

생성자 함수가 있으면 Cat.prototype.speak와 같이 prototype에 메서드를 연결한 코드가 있습니다. 이와 같이 prototype에 작성하지 않으면 각각의 인스턴스에 메서드가 생성되게 됩니다. 이 형태가 ES5에서 인스턴스를 구현하는 기본 형태 입니다.

Object.create()를 통해 Cat.prototype에 연결된 메서드를 Lion.prototype.__proto__에 첨부합니다. Lion.prototype에는 constructor가 연결되어 있는데 prototype을 재 할당했기 때문에 지워진 constructor를 다시 할당해 줍니다.

결과적으로 Lion 생성자 함수의 구조는 다음과 같습니다.

ES6 Class 기반 상속

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Cat {
constructor(name) {
this.name = name;
}

speak() {
console.log(this.name + ' makes a noise.');
}
}

class Lion extends Cat {
speak() {
super.speak();
console.log(this.name + ' roars.');
}
}

const lion = new Lion('Samba');
lion.speak();

[결과]
Sambda makes a noise.
Sambda roars.

ES6에서는 extends 키워드로 상속을 구현합니다. Cat 클래스를 상속받은 Lion 클래스의 구조는 다음과 같습니다.

위의 prototype을 통해 상속을 구현한 Lion 생성자 함수의 구조와 비교했을때 일치합니다. 추가적으로 new Lion(‘Samba’) 를 실행하면 다음의 과정을 거치게됩니다.

  1. Lion 클래스의 constructor를 호출
  2. Lion 클래스에 constructor를 작성하지 않았기 때문에 슈퍼 클래스의(Cat) constructor가 호출됨 (내부적으로 프로토타입 체인으로 인해)
  3. 슈퍼 클래스의 constructor에서 this는 현재의 인스턴스를 참조하므로 인스턴스의 name 프로퍼티에 파라미터로 전달받은 값을 설정
  4. 생성한 인스턴스를 lion에 할당

super 키워드

서브 클래스와 슈퍼 클래스에 같은 이름의 메서드가 존재하면 슈퍼 클래스의 메서드는 호출되지 않습니다. 이때 super 키워드를 사용해서 슈퍼 클래스의 메서드를 호출할 수 있습니다. (서브 클래스의 constructor에 super()를 작성하면 슈퍼 클래스의 constructor가 호출됩니다.)

static 키워드

static 키워드는 클래스를 위한 정적(static) 메소드를 정의합니다. 정적 메소드는 prototype에 연결되지 않고 클래스에 직접 연결되기 때문에 클래스의 인스턴스화(instantiating) 없이 호출되며, 클래스의 인스턴스에서는 호출할 수 없습니다. 동일한 클래스 내의 다른 정적 메서드 내에서 정적 메서드를 호출하는 경우 키워드 this를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
class Lion {
static speak() {
console.log('Noise~');
}
}

Lion.speak();

[결과]
Noise~

정적 메소드는 어플리케이션(application)을 위한 유틸리티(utility) 함수를 생성하는데 주로 사용됩니다.


마치며

ES6의 Class 문법에 대해 정리해 보았다. JavaScript 언어를 약 1년전 Node.js 를 시작하며 처음 접하게 되었는데 사실 그 당시 Prototype과 상속에 대해 크게 다룰일이 없었다. (어쩌면 너무 무지해서 사용 필요성을 느끼지 못했을 수도…) 그 후 Node.js 버전을 올리고 ES6를 공부하며 Class 문법을 접하게 되었는데 JavaScript의 Prototype에 대한 이해와 지식이 부족하다 보니 이전에 공부했던 C++과 Java의 Class 처럼 이해했던 것 같다. 그래도 그 후 Prototype과 더불이 Class까지 공부하며 지금은 어느정도 이해하게 된것 같다. 결론은… 역시나 JavaScript에서 Prototype을 이해하는건 중요한것 같다.

참고
ES6 Class는 단지 prototype 상속의 문법설탕일 뿐인가?
MDN - Classes
MDN - 상속과 프로토타입

Author

KimJongMin

Posted on

2017-06-18

Updated on

2021-03-22

Licensed under

댓글