JavaScript

[ES6 JavaScript] Class

JayLee 2020. 9. 26. 20:54
반응형

 JavaScript에는 class가 없었습니다. 그래서 많은 개발자들은 JavaScript에서 객체지향 프로그래밍을 위해 함수를 이용하여 class처럼 동작하게 구현하여 사용했습니다. 프로토타입 체인과 클로저등으로 상속, 캡슐화등을 구현할 수 있기 때문입니다. 이러한 불편함을 알고 있었는지 ES6에서는 드디어 class 문법이 발표되었습니다. 그렇다고 전혀 새로운 것이 생긴 것은 아닙니다. 기존에 있던 프로토타입 기반 객체지향 프로그래밍을 조금 더 보기 좋고 쉽게 만들어준 것이라고 생각하면 될 것 같습니다. 그럼 하나 하나 살펴보도록 하겠습니다.

 

Pixabay로부터 입수된 Alltechbuzz님의 이미지 입니다.  

클래스 정의

 클래스는 사실 함수입니다. 함수를 정의하는 방법이 함수 선언과 함수 표현식이 있는 것처럼 클래스도 두 가지 모두 제공하고 있습니다. 먼저 클래스를 선언하는 방법을 살펴보겠습니다.

클래스 선언

 클래스를 선언하는 방법을 살펴보겠습니다. 간단하게 class 키워드를 사용하면 됩니다.

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

 

함수 선언 vs 클래스 선언

 함수 선언과 클래스 선언의 차이점은 호이스팅입니다. 여기서 호이스팅(Hoisting)이란, 함수 선언이 컴파일 단계에서 메모리에 저장되는 것을 말합니다. 즉, 코드의 최상단으로 옮겨진 것과 같은 효과를 얻을 수 있습니다. 이 호이스팅이 함수에서는 일어나고 클래스에서는 일어나지 않습니다. 실험을 위해 아래 코드를 한 번 실행해 보겠습니다.

console.log(Func);
console.log(Person);


function Func() {
    console.log('func');
}

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

 한 개의 함수와 한 개의 클래스를 아래에서 선언하고 가장 상단에서 함수와 클래스를 참조해보았습니다. 결과는 어떨까요?

 Func 함수는 출력이 되었는데, Person 클래스는 정의되지 않았다는 에러가 발생했습니다. 즉, 함수는 호이스팅되어 선언하기 전에 사용할 수 있고 클래스는 호이스팅되지 않아 반드시 선언 후에 사용해야한다는 것을 알 수 있습니다. 

 

클래스 표현식

 클래스 표현식을 함수 표현식과 비교하며 알아보겠습니다. 먼저 이름을 가지지 않은 함수와 클래스를 함수 표현식으로 정의해봤습니다.

// Function
let func = function() {
	console.log('function');
}

// Class
let Human = class {
	constructor(name) {
  	this.name;
  }
}

console.log(func.name);		// func
console.log(Human.name);	// Human

 이렇게 이름을 갖지 않고 정의할 수 있습니다. 정의하면서 변수에 전달하면 그 이름으로 사용할 수 있습니다. 그렇다면 이름을 갖고 있을 땐 어떻게 동작할까요?

// Function
let func = function NamedFunc() {
	console.log('function');
}

// Class
let Human = class Human2 {
	constructor(name) {
  	this.name;
  }
}

console.log(func.name);		// NamedFunc
console.log(Human.name);	// Human2

 

 함수와 클래스 모두 본연의 이름으로 name값을 갖고 있는 것을 알 수 있습니다.

 

클래스 Body와 메서드 정의

 클래스의 body는 중괄호({})로 묶여있는 안쪽 범위입니다. 이 곳에 메서드나 생성자와 같은 클래스 멤버들을 정의할 수 있습니다.

 Strict mode

 클래스 선언과 클래스 body는 strict mode에서 실행됩니다. 엄격한 문법이 적용되니 유의하여 코딩하셔야합니다.

생성자(constructor)

 생성자는 객체지향 프로그래밍을 하셨던 분들이라면 잘 아시겠지만, 생성된 객체를 초기화하기 위한 특수한 메서드 입니다. 당연히 클래스안에 한 개만 존재할 수 있고, 부모 클래스의 생성자를 호출하기 위해 super 키워드를 사용할 수 있습니다. 

Stiatic 메서드

 static 키워드로 클래스를 위한 정적(static) 메서드를 정의할 수 있습니다. static 메서드는 클래스의 인스턴스화 없이 호출되고 클래스의 인스턴스에서는 호출할 수 없습니다.

class Human {
	constructor(name) {
  	this.name = name;
  }
  
  static say() {
  	return 'hello!';
  }
}

const human1 = new Human('lee');
const human2 = new Human('kim');

console.log(human1.say);	// undefined
console.log(human2.say);	// undefined

console.log(Human.say());	// hello!

 두 개의 Human 인스턴스를 생성했습니다. 그리고 static 메서드인 say를 확인했습니다. 존재하지 않는다며 undefined를 출력하는 것을 확인할 수 있습니다. 그렇지만 인스턴스화하지 않고 Human.say()를 호출하니 정상적으로 hello! 출력하는 것을 확인할 수 있습니다.

 

클래스 내부에서의 this

// TEST Class
class AnimalClass { 
  speak() {
    return this;
  }
  static eat() {
    return this;
  }
}

let obj = new AnimalClass();
console.log(obj.speak()); // Animal {}
let speak = obj.speak;
console.log(speak()); // undefined

console.log(AnimalClass.eat()); // class Animal
let eat = AnimalClass.eat;
console.log(eat()); // undefined

// TEST Function
function AnimalFunc() { }

AnimalFunc.prototype.speak = function() {
  return this;
}

AnimalFunc.eat = function() {
  return this;
}

obj = new AnimalFunc();
speak = obj.speak;
console.log(speak()); // global object

eat = AnimalFunc.eat;
console.log(eat()); // global object

 Class 방식의 메소드의 경우, 항상 strict mode로 실행되기 때문에 this 값 없이 호출될 때, this 값은 메서드 안에서 undefined가 됩니다. 하지만, Function 방식의 경우, non-strict mode로 실행되기 때문에 초기 값이 없이 호출되면, this는 전역 객체가 설정됩니다. 

 

인스턴스 속성

인스턴스 속성은 반드시 클래스 메서드 내에 정의되어야하고 정적(클래스사이드) 속성과 프로토타입 데이터 속성은 반드시 클래스 선언부 바깥쪽에 정의되어야 합니다.

class Rectangle {
  constructor(height, width) {    
    this.height = height;	// 인스턴스 속성
    this.width = width;		// 인스턴스 속성
  }
}

Rectangle.staticWidth = 20;	// 정적 속성
Rectangle.prototype.prototypeWidth = 25;	// 프로토타입 속성

 

Public 필드와 Private 필드

실험적 기능이라고 하니 잘 알아보고 사용하셔야합니다. 단, Babel 같은 빌드 시스템을 사용하면 이 기능을 사용해볼 수 있습니다.

class Rectangle {
  height = 0;	// public default 0
  width;	// public
  #x = 0;	// private default 0
  #y;		// private
  constructor(height, width, x, y) {    
    this.height = height;
    this.width = width;
    this.#x = x;
    this.#y = y;
  }
}

 필드 선언과 동시에 기본 값도 함께 정의할 수 있고, private 필드는 클래스 외부에서 접근하려고 하면 에러가 발생합니다. 

 

Extends를 통한 클래스 상속(sub classing)

 Function을 이용한 객체지향 프로그래밍에서 조금 복잡했던 방식 중 하나는 상속을 표현하는 것이었습니다. ES6에서는 extends 키워드를 통해 기존 객체지향 언어들에게 경험했던 방법으로 상속을 구현할 수 있습니다.

lass Animal { 
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // name을 파라미터로 부모 클래스의 생성자를 호출
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

let d = new Dog('Mitzie');
d.speak(); // Mitzie barks.

 자식 클래스의 생성자에서는 부모 생성자를 호출해야합니다. 이를 위해 super()를 호출하여 부모 생성자가 실행될 수 있도록 합니다. Dog 클래스에는 this.name이 없지만, 부모 클래스인 Animal 클래스에 있는 this.name을 참조하여 console 출력이 가능합니다.

 

function Animal (name) {
  this.name = name;  
}

Animal.prototype.speak = function () {
  console.log(`${this.name} makes a noise.`);
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks.`);
  }
}

let d = new Dog('Mitzie');
d.speak(); // Mitzie barks.

 참고로, 부모 클래스가 기존 방식인 함수 기반 클래스더라도 상속이 가능합니다.

 

const Animal = {
  speak() {
    console.log(`${this.name} makes a noise.`);
  }
};

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

// If you do not do this you will get a TypeError when you invoke speak
Object.setPrototypeOf(Dog.prototype, Animal);

let d = new Dog('Mitzie');
d.speak(); // Mitzie makes a noise.

 위와 같이 Animal 클래스가 생성자가 없을 경우에는 상속이 불가능합니다. 이럴 때에는 위 예제처럼 Object.setPrototypeOf() 메서드를 사용하여 상속을 할 수 있습니다.

 

 

Species

 만약에 Array를 상속받은 MyArray 클래스가 있다고 가정해봅시다. 당연히 반환되는 생성자는 MyArray 인스턴스입니다. 하지만 누군가는 부모 클래스인 Array 인스턴스를 반환하는 것을 원할 수도 있습니다. 이때 Symbol.species가 이를 가능하게 해줍니다. 예를 하나 보겠습니다. 

class MyArray extends Array {
  // 부모 Array 생성자로 종류 덮어쓰기
  static get [Symbol.species]() { return Array; }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array);   // true

 MyArray 인스턴스가 생성되고 map으로 새로운 인스턴스를 생성했습니다. 당연히 map 함수의 결과는 동일한 클래스인 MyArray의 인스턴스가 반환되어 아래 출력 결과가 모두 true가 나온다고 생각할 것 같습니다. 하지만, Symbol.species를 사용하여 Array 클래스의 인스턴스가 반환된 것을 확인할 수 있습니다.

 

super를 통한 상위 클래스 호출

super 키워드를 통해 객체의 부모 클래스가 가지고 있는 함수들을 호출 할 수 있습니다.

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.`);
  }
}

let l = new Lion('Fuzzy');
l.speak(); 
// Fuzzy makes a noise.
// Fuzzy roars.

 Lion 인스턴스 l의 speak() 메서드를 호출했습니다. Lion의 speak 메서드 내부를 보면 super.speak() 호출하고 있습니다. Lion의 부모 클래스는 Cat이기 때문에 Cat의 speak() 메서드가 호출됩니다. Cat의 speak() 메서드 호출이 완료되면 Lion speak() 메서드의 나머지 부분이 실행됩니다.

 

 

 

참고:

developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes

 

Classes

Class는 객체를 생성하기 위한 템플릿입니다. 클래스는 데이터와 이를 조작하는 코드를 하나로 추상화합니다. 자바스크립트에서 클래스는 프로토타입을 이용해서 만들어졌지만 ES5의 클래스 의��

developer.mozilla.org

poiemaweb.com/es6-class

 

Class | PoiemaWeb

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

poiemaweb.com

 

반응형