클래스

클래스 (Classes)

클래스는 객체를 만드는 **템플릿(template)**이다. 클래스는 데이터를 그 데이터를 다루는 코드와 함께 캡슐화한다. JS의 클래스는 프로토타입 위에 구축되지만, 클래스에만 고유한 문법과 의미론도 일부 가진다.

더 많은 예와 설명은 Using classes 가이드를 보라. 이 기능은 잘 확립되어 있어 2016년 3월부터 다양한 디바이스와 브라우저 버전에서 동작한다(Baseline: Widely available).

출처: Classes - JavaScript | MDN

본문

설명 (Description)

클래스 정의하기 (Defining classes)

클래스는 사실 "특수 함수(special functions)"다. 함수 표현식과 함수 선언을 정의할 수 있는 것처럼, 클래스도 클래스 표현식(class expression) 또는 클래스 선언(class declaration) 두 가지 방식으로 정의할 수 있다.

// 선언
class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

// 표현식; 클래스는 익명이지만 변수에 할당됨
const Rectangle = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

// 표현식; 클래스는 자신만의 이름을 가짐
const Rectangle = class Rectangle2 {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

함수 표현식처럼 클래스 표현식은 익명일 수도 있고, 할당된 변수와 다른 이름을 가질 수도 있다. 그러나 함수 선언과 달리 클래스 선언은 let이나 const와 같은 일시적 사각지대(temporal dead zone) 제약을 가지며 호이스팅되지 않는 것처럼 동작한다.

클래스 본문 (Class body)

클래스의 본문은 중괄호 {} 안의 부분이다. 여기서 메서드나 생성자 같은 클래스 멤버를 정의한다.

클래스 본문은 "use strict" 지시어가 없어도 strict mode로 실행된다.

클래스 요소는 세 측면으로 특징지을 수 있다.

  • 종류(Kind): Getter, setter, method, 또는 field
  • 위치(Location): Static 또는 instance
  • 가시성(Visibility): Public 또는 private

이들은 함께 총 16가지 조합을 만든다. 참조를 더 논리적으로 나누고 내용이 겹치지 않게 하기 위해, 서로 다른 요소들은 서로 다른 페이지에서 상세히 소개된다.

  • Method definitions — Public instance method
  • getter — Public instance getter
  • setter — Public instance setter
  • Public class fields — Public instance field
  • static — Public static method, getter, setter, and field
  • Private elements — everything that's private

참고: private 요소는 같은 클래스에 선언된 모든 private 이름이 유일해야 한다는 제약이 있다. 다른 모든 public 속성에는 이 제약이 없다 — 같은 이름의 public 속성을 여러 개 가질 수 있으며, 마지막 것이 다른 것들을 덮어쓴다. 이것은 객체 초기화자와 같은 동작이다.

추가로, 각각 고유한 참조를 가진 constructorstatic initialization blocks라는 두 가지 특수 클래스 요소 문법이 있다.

constructor (생성자)

constructor 메서드는 클래스로 만들어진 객체를 만들고 초기화하는 특수 메서드다. 클래스에는 "constructor"라는 이름의 특수 메서드가 하나만 있을 수 있다 — 클래스에 constructor 메서드가 두 번 이상 나타나면 SyntaxError가 던져진다.

constructor는 super 키워드를 사용해 수퍼 클래스(super class)의 생성자를 호출할 수 있다.

constructor 안에서 인스턴스 속성을 만들 수 있다.

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

대안으로, 인스턴스 속성의 값이 생성자의 인수에 의존하지 않는다면 그것들을 **클래스 필드(class fields)**로 정의할 수 있다.

Static initialization blocks (정적 초기화 블록)

static initialization blocks는 private 범위에 접근할 수 있는 상태에서, 초기화 중 문장의 평가를 포함해 static 속성의 유연한 초기화를 허용한다.

여러 static 블록을 선언할 수 있고, 이들은 static 필드와 메서드의 선언과 섞일 수 있다(모든 static 항목은 선언 순서대로 평가된다).

메서드 (Methods)

메서드는 각 클래스 인스턴스의 프로토타입에 정의되며 모든 인스턴스가 공유한다. 메서드는 일반 함수, async 함수, generator 함수, 또는 async generator 함수일 수 있다. 자세한 내용은 method definitions를 보라.

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // Getter
  get area() {
    return this.calcArea();
  }
  // Method
  calcArea() {
    return this.height * this.width;
  }
  *getSides() {
    yield this.height;
    yield this.width;
    yield this.height;
    yield this.width;
  }
}

const square = new Rectangle(10, 10);

console.log(square.area); // 100
console.log([...square.getSides()]); // [10, 10, 10, 10]

Static 메서드와 필드 (Static methods and fields)

static 키워드는 클래스의 static 메서드 또는 필드를 정의한다. static 속성(필드와 메서드)은 각 인스턴스가 아니라 클래스 자체에 정의된다. static 메서드는 애플리케이션의 유틸리티 함수를 만드는 데 자주 사용되고, static 필드는 캐시, 고정 구성, 또는 인스턴스마다 복제될 필요가 없는 다른 데이터에 유용하다.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  static displayName = "Point";
  static distance(a, b) {
    const dx = a.x - b.x;
    const dy = a.y - b.y;

    return Math.hypot(dx, dy);
  }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);
p1.displayName; // undefined
p1.distance; // undefined
p2.displayName; // undefined
p2.distance; // undefined

console.log(Point.displayName); // "Point"
console.log(Point.distance(p1, p2)); // 7.0710678118654755

필드 선언 (Field declarations)

클래스 필드 선언 문법을 사용하면 constructor 예는 다음과 같이 쓸 수 있다.

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

클래스 필드는 변수가 아니라 객체 속성과 유사하므로, 그것들을 선언할 때 const 같은 키워드를 사용하지 않는다. JavaScript에서 private 요소는 특별한 식별자 문법을 사용하므로 public, private 같은 수식어 키워드도 사용해선 안 된다.

위에서 보듯 필드는 기본값이 있거나 없이 선언될 수 있다. 기본값이 없는 필드는 기본적으로 undefined가 된다. 필드를 앞서 선언함으로써 클래스 정의가 더 자기 문서화(self-documenting)되고, 필드가 항상 존재하므로 최적화에 도움이 된다.

자세한 내용은 public class fields를 보라.

Private 요소 (Private elements)

private 필드를 사용하면 정의를 다음과 같이 다듬을 수 있다.

class Rectangle {
  #height = 0;
  #width;
  constructor(height, width) {
    this.#height = height;
    this.#width = width;
  }
}

클래스 밖에서 private 필드를 참조하는 것은 오류다. 그것들은 클래스 본문 안에서만 읽거나 쓸 수 있다. 클래스 밖에서 보이지 않는 것들을 정의함으로써, 클래스 사용자가 버전마다 바뀔 수 있는 내부에 의존하지 못하게 보장한다.

private 필드는 필드 선언에서 앞서서만 선언할 수 있다. 일반 속성처럼 나중에 할당을 통해 만들 수는 없다.

private 메서드와 접근자(accessors)는 public 대응물과 같은 문법으로 정의할 수 있지만, 식별자가 #로 시작한다.

자세한 내용은 private elements를 보라.

상속 (Inheritance)

extends 키워드는 클래스 선언이나 클래스 표현식에서 다른 생성자(클래스 또는 함수)의 자식으로 클래스를 만들기 위해 사용된다.

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

const d = new Dog("Mitzie");
d.speak(); // Mitzie barks.

서브클래스에 constructor가 있으면 this를 사용하기 전에 먼저 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.`);
  }
}

const l = new Lion("Fuzzy");
l.speak();
// Fuzzy makes a noise.
// Fuzzy roars.

평가 순서 (Evaluation order)

클래스 선언 또는 클래스 표현식이 평가될 때, 그 다양한 구성 요소는 다음 순서로 평가된다.

  1. extends 절이 있으면 먼저 평가된다. 그것은 유효한 생성자 함수 또는 null로 평가되어야 하며, 그렇지 않으면 TypeError가 던져진다.
  2. constructor 메서드가 추출되고, constructor가 없으면 기본 구현으로 대체된다. 그러나 constructor 정의는 단지 메서드 정의일 뿐이므로, 이 단계는 관찰할 수 없다.
  3. 클래스 요소의 속성 키가 선언 순서대로 평가된다. 속성 키가 계산된(computed) 것이라면 계산된 표현식이, this 값이 클래스를 둘러싼 this 값(클래스 자체가 아님)으로 설정된 상태에서 평가된다. 속성 값은 아직 평가되지 않는다.
  4. 메서드와 접근자가 선언 순서대로 설치된다. 인스턴스 메서드와 접근자는 현재 클래스의 prototype 속성에 설치되고, static 메서드와 접근자는 클래스 자체에 설치된다. Private 인스턴스 메서드와 접근자는 인스턴스에 나중에 직접 설치되도록 저장된다. 이 단계는 관찰할 수 없다.
  5. 클래스는 이제 extends가 지정한 프로토타입과 constructor가 지정한 구현으로 초기화된다. 위의 모든 단계에서, 평가된 표현식이 클래스의 이름에 접근하려 하면 클래스가 아직 초기화되지 않았으므로 ReferenceError가 던져진다.
  6. 클래스 요소의 이 선언 순서대로 평가된다.
    • 각 인스턴스 필드(public 또는 private)에 대해 그 초기화자 표현식이 저장된다. 초기화자는 인스턴스 생성 중, (base 클래스의) constructor의 시작 또는 (파생 클래스의) super() 호출이 반환되기 직전에 평가된다.
    • 각 static 필드(public 또는 private)에 대해 그 초기화자가 this를 클래스 자체로 설정한 상태에서 평가되고, 속성이 클래스에 만들어진다.
    • static initialization blocks는 this를 클래스 자체로 설정한 상태로 평가된다.
  7. 클래스는 이제 완전히 초기화되었고 생성자 함수로 사용될 수 있다.

인스턴스가 어떻게 만들어지는지는 constructor 참조를 보라.

예시 (Examples)

인스턴스 및 static 메서드에 this 바인딩하기 (Binding this with instance and static methods)

static 또는 인스턴스 메서드가 this 값 없이 호출되면(예: 메서드를 변수에 할당한 다음 호출), 메서드 안의 this 값은 undefined가 된다. 이 동작은 "use strict" 지시어가 없어도 같다. 왜냐하면 클래스 본문 안의 코드는 항상 strict mode로 실행되기 때문이다.

class Animal {
  speak() {
    return this;
  }
  static eat() {
    return this;
  }
}

const obj = new Animal();
obj.speak(); // the Animal object
const speak = obj.speak;
speak(); // undefined

Animal.eat(); // class Animal
const eat = Animal.eat;
eat(); // undefined

위를 비strict mode의 전통적인 함수 기반 문법으로 다시 쓴다면, 이 메서드 호출들은 자동으로 globalThis에 바인딩된다. strict mode에서는 this의 값이 undefined로 남는다.

function Animal() {}

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

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

const obj = new Animal();
const speak = obj.speak;
speak(); // global object (비strict mode)

const eat = Animal.eat;
eat(); // global object (비strict mode)

더 알아보기