[모던 자바스크립트 Deep Dive] 25장. 클래스

minji·2025년 9월 22일
post-thumbnail

"내가 가지고 있는 속성을 원해? 주문(super)을 외쳐봐!"


클래스 정의방법

  • 클래스는 class 키워드를 사용하여 정의한다.
  • 생성자 함수와 마찬가지로 파스칼 케이스를 사용한다.
    (단, 파스칼 케이스를 사용하지 않아도 에러가 발생하지는 않는다🤷🏻‍♀️)
  • 일반적이지는 않지만 표현식으로 정의할 수도 있다.
    으로 사용할 수 있는 일급 객체 이다. (즉, 클래스는 함수다.)
// 기본형태
class Person {}

// 표현식 정의형태
// 익명 클래스 표현식
const Person = class {}

// 기명 클래스 표현식
const Person = class Myclass {}

클래스의 메서드

클래스 몸체에서는 다음 세 가지의 메서드를 정의할 수 있다.

  • 생성자(constructor)
  • 프로토타입 메서드
  • 정적 메서드

클래스와 생성자함수의 정의방식은 참 닮았는데 비교해보면 다음과 같다.


클래스의 호이스팅

앞서 살펴본 것처럼 클래스는 함수이다.

다음과같이 클래스 선언문으로 정의한 클래스는 함수 선언문처럼 평가 과정 이전에 먼저 평가되어 함수 객체를 생성한다.

class Person {}

console.log(typeof Person) // ?

생성된 함수 객체는 생성자 함수로서 호출할 수 있는 생성자(constructor) 이다.
함수 객체 생성시점에 프로토타입 도 더불어 생성이 된다.
이 때, 19장.프로토타입-리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입 에서도 살펴본 바와같이 생성자 함수와 프로토타입은 단독으로 존재할 수 없고 언제나 쌍으로 존재한다.

단, 클래스는 정의 이전에 참조할 수 없다.
그래서 호이스팅이 발생하지 않는 것처럼 보이지만 그렇지 않다.

클래스 선언문도 변수 선언, 함수 정의와 마찬가지로 호이스팅이 발생하는데, let, const 키워드로 선언한 변수처럼 발생한다.

✋🏻 잠깐! 이게 무슨 소리야? let, const 키워드로 선언된 건 뭐가 다른데?
let, const 키워드로 선언한 변수는 일시적 사각지대(TDZ; Temporal Dead Zone) 에 빠지게된다.

즉, 클래스 선언문도 TDZ 에 빠지게 되므로 호이스팅이 발생했지만 하지않은 것처럼 보이는 것이다.

var, let, const, function, class 키워드로 선언된 모든 식별자는 호이스팅 된다. 왜? 모든 선언문은 런타임 이전에 실행되기 때문이다.


클래스의 인스턴스 생성

| 클래스는 생성자 함수이며, new 연산자와 함께 호출되어 인스턴스를 생성한다.

클래스의 존재이유는 인스턴스 생성이다. 그렇기 때문에 반드시 new 연산자와 함께 호출해야한다.

class Person {}

// ❌ 잘못 호출한 경우 → 에러발생
const me = Person()
// TypeError: Class constructor cannot be invoked without 'new'

// ✅ 올바르게 호출한 경우
const me = new Person()

인스턴스 생성 조건1. new 연산자로 호출해라

또한, 클래스 표현식으로 정의된 클래스의 경우에는 식별자를 사용해 인스턴스를 생성하지 않고, 기명 클래스 표현식의 클래스 이름을 사용해 인스턴스를 생성하면 에러가 발생한다.

const Person = class MyClass {}

const me = new Person()

console.log(MyClass) // 

위와같은 오류가 발생하는 이유는 클래스 표현식에서 사용한 클래스 이름은 외부 코드에서는 접근이 불가능하기 때문이다. (like 기명함수 표현식)

인스턴스 생성 조건2. 클래스를 가리키는 식별자를 사용하여 인스턴스를 생성해라


클래스의 메서드

클래스 몸체에서 정의할 수 있는 메서드는 클래스의 정의방법에서도 살펴본 것처럼 세 가지가 있다.

  • 생성자(constructor)
  • 프로토타입 메서드
  • 정적 메서드

생성자(constructor)

| 인스턴스를 생성하고 초기화하기 위한 특수한 메서드이며, 이름을 변경할 수 없다.

클래스 생성자(constructor)의 특징

  • 클래스 내에 최대 한 개만 존재할 수 있다.
    (만약 2개 이상의 포함하게되면 문법 에러가 발생한다.)
  • 생략할 수 있다. → 생략시, 빈 constructor가 암묵적으로 정의된다.
  • 프로퍼티 추가로 인스턴스 초기화 생성이 필요하다면 constructor 내부에서 this인스턴스 프로퍼티를 추가한다.
    ⇒ 인스턴스 초기화를 위해서는 constructor를 생략하면 안된다.
  • 별도의 반환문을 갖지 않아야 한다.
    이 부분은 17장.생성자 함수에 대한 객체 생성에서도 살펴보았듯이, 명시적으로 this 가 아닌 다른 값을 반환하는 것은 클래스의 기본 동작을 방해하는 것이기 때문에 반드시 반환문을 생략해야 한다.

프로토타입 메서드

| 클래스 몸체에서 정의한 메서드는 생성자 함수에 의한 객체 생성 방식과는 다르게 기본적으로 프로토타입 메서드가 된다.

또한, 생성자 함수와 마찬가지로 클래스가 생성한 인스턴스는 프로토타입 체인에 포함된다.

클래스 몸체에서 정의한 메서드는 객체의 프로토타입에 존재하는 프로토타입 메서드가 되며, 객체는 프로토타입 메서드를 상속받아 사용할 수 있다.

프로토타입 체인은 기존의 모든 객체 생성방식(객체 리터럴, 생성자 함수 등), 클래스에 의해 생성된 객체에도 동일하게 적용된다.
클래스는 생성자 함수의 역할을 하는 즉, 객체를 생성하는 생성자 함수라고 볼 수 있다.

정적 메서드

19장.프로토타입-정적 프로퍼티/메서드 에서도 살펴보았듯이 정적 메서드는 인스턴스를 생성하지 않아도 자신의 프로퍼티와 메서드를 가지기 때문에 호출할 수 있는 메서드이다.

생성자 함수에서는 정적 메서드를 다음과 같이 명시적으로 추가를 했다.

function Person(name) {
  this.name = name
}

// 정적 메서드 생성
Person.sayHi = function() {
  console.log("Hi!")
}

// 정적 메서드 호출
Person.sayHi() // Hi!

하지만, 클래스에서는 간단하게 메서드에 static 키워드만 붙이면 정적 메서드(클래스 메서드)가 된다. (와웅!!!!)

class Person {
  constructor(name) {
    this.name = name
  }
  
  // 정적 메서드 생성
  static sayHi() {
    console.log("Hi!")
  }
}

// 정적 메서드 호출
Person.sayHi() // Hi!

클래스는 클래스 선언문이나 클래스 표현식으로 정의가 평가되는 시점에 함수 객체가 된다.
그래서 정적 메서드는 클래스 정의 후, 별다른 인스턴스를 생성하지 않아도 호출 할 수 있다.

프로토타입 메서드 vs. 정적 메서드

  1. 정적 메서드와 프로토타입 메서드는 속해있는 프로토타입 체인이 다르다.

  2. 정적 메서드는 클래스로 호출하고 프로토타입 메서드는 인스턴스로 호출한다.

  3. 정적 메서드는 인스턴스 프로퍼티를 참조할 수 없는 반면, 프로토타입 메서드는 인스턴스 프로퍼티를 참조할 수 있다.

위 세 가지 내용을 다음과같이 공식처럼 정리해보았다.

  • static이 앞에 있다 → 정적 메서드이다. → 메서드 호출방법: 클래스명.메서드()
  • static이 앞에 없다 → 프로토타입 메서드(instance)이다. → 메서드 호출방법: 인스턴스.메서드()
  • 메서드 내부에서 인스턴스 프로퍼티를 참조해야 한다. → this 사용
  • 메서드 내부에서 인스턴스 프로퍼티를 참조하지 않아도 된다. → this 미사용

클래스에서 정의한 메서드 특징

클래스에서 정의한 메서드는 다음과 같은 특징이 있다.

  • function 키워드를 생략한 축약 표현을 사용한다.
  • 객체 리터럴과는 다르게 메서드 정의 시 콤마가 필요없다.
  • 암묵적으로 strict mode 로 실행된다.
  • for...in문 또는 Object.keys 메서드 등으로 열거할 수 없다.
  • 내부 메서드 [[Constructor]] 를 갖지 않으므로, new 연산자로 호출할 수 없다.

클래스의 인스턴스 생성과정

1️⃣ 인스턴스 생성과 this 바인딩

  • new 연산자와 함께 클래스 호출 → 암묵적으로 빈 객체 생성(클래스가 생성한 인스턴스)
  • 클래스가 생성한 인스턴스의 프로토타입 으로 클래스의 프로토타입 프로퍼티가 가리키는 객체 설정
  • 인스턴스 this에 바인딩
    ⇒ constructor 내부의 this는 클래스가 생성한 인스턴스를 가리킨다.

2️⃣ 인스턴스 초기화

  • constructor 내부코드 실행 → this에 바인딩되어있는 인스턴스 초기화
    (constructor가 인수로 전달받은 초기값으로 인스턴스의 프로퍼티 값을 초기화)
  • 단, constructor가 생략되었다면 이 과정은 생략된다.

3️⃣ 인스턴스 반환

  • 완성된 인스턴스가 this에 바인딩 → this의 암묵적 반환으로 마무리

프로퍼티

인스턴스 프로퍼티

| 인스턴스 프로퍼티는 constructor 내부에서 정의해야한다.

constructor 내부에서 this에 추가한 프로퍼티는 언제나 클래스가 생성한 인스턴스의 프로퍼티가 된다.
클래스는 다른 객체지향 언어처럼 접근 제한자(private, public, protected) 를 지원하지 않기때문에 인스턴스 프로퍼티는 언제나 public하다.

접근자 프로퍼티

| 접근자 프로퍼티는 자체적으로는 값을 가지지않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티이다.

이러한 접근자 프로퍼티는 클래스에서도 사용할 수 있다.

접근자 프로퍼티는 getter 함수와 setter 함수로 구성되어있다.

getter 함수

  • 인스턴스 프로퍼티에 접근할 때마다 프로퍼티 값을 조작하거나 별도의 행위가 필요할 때 사용한다.
  • 메서드 이름 앞에 get 키워드를 사용하여 정의한다.
  • 취득할 때 사용 → 반드시 무언가를 반환 해야한다.

setter 함수

  • 인스턴스 프로퍼티에 값을 할당할 때마다 프로퍼티 값을 조작하거나 별도의 행위가 필요할 때 사용한다.
  • 메서드 이름 앞에 set 키워드를 사용하여 정의한다.
  • 할당할 때 사용 → 반드시 매개변수가 있어야한다.
    (단 하나의 값만 할당받으므로 하나의 매개변수만 선언할 수 있다.)
class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.laseName = lastName
  }
  
  // getter 함수
  get fullName() {
    return `${this.firstName} ${this.lastName}`
  }
  
  // setter 함수
  set fullName(name) {
    [this.firstName, this.lastName] = name.split(' ')
  }
}

// ...

접근자 프로퍼티는 호출하는 것이 아니라 참조(getter)하거나 값을 할당(setter)하는 형식으로 사용한다.

기본적으로 클래스의 메서드는 프로토타입 메서드가 된다.
그러므로 클래스의 접근자 프로퍼티도 프로토타입의 프로퍼티가 된다.

클래스 필드

| 클래스 필드란 클래스 기반 객체지향 언어에서 클래스가 생성할 인스턴스의 프로퍼티를 가리킨다.

클래스 기반 객체지향 언어에서의 this 는 언제나 클래스가 생성할 인스턴스를 가리킨다.

  • 자바스크립트의 클래스 몸체에는 메서드만 선언할 수 있다.
    하지만, 자바스크립트의 클래스 몸체에서도 클래스 필드를 정의할 수 있도록 제안이 되어있으며, Chrome 72 이상 의 브라우저와 버전 12 이상 의 Node.js는 이 제안내용을 미리 구현해 놓았다.
    ⇒ 해당 환경에서는 클래스 몸체에 클래스 필드 정의를 에러발생없이 할 수 있다.

✋🏻 잠깐! 하지만, 2025 9월 현재 찾아본 바에 의하면

2021년 1월, TC39 프로세스의 stage3에 클래스 필드로 정의할 수 있는 새로운 표준 사양이 제안됐었고, 2025년 9월 현재 mdn에 공식적으로 등록되어 사용가능한 상태이다. 🥳👏🏻👏🏻🎉

👉🏻클래스 필드 mdn 보러가기

class Person {
  name = "Lee"
}

const me = new Person()
console.log(me) // Person{name: "Lee"}

이렇게 클래스 몸체에서 클래스 필드를 정의할 때에는 this 에 클래스 필드 바인딩을 해서는 안된다. ❌
⇒ this는 클래스의 constructor 와 메서드 내에서만 유효하다.

  • 클래스 필드를 참조하는 경우에는 this 를 반드시 사용하여야 한다.
    (자바와 같은 클래스 기반 객체지향 언어에서는 생략이 가능하다.)
  • 클래스 필드에 초기값 을 할당하지 않으면 undefined 를 갖는다.
  • 인스턴스 생성시 외부 초기값으로 클래스 필드를 초기화해야 하는 경우에는, constructor 내부에서 클래스 필드를 참조하여 초기화를 진행하여야 한다.
  • 함수는 일급 객체이므로 클래스 필드에 할당할 수 있지만 권장하지는 않는다.

private 필드

24장.클로저-캡슐화와 정보은닉 에서 '인스턴스 메서드와 프로토타입을 활용하여 private 흉내를 낼 수는 있지만 완전하다고 할 수는 없다.' 이런 내용을 살펴본적이 있다.

클래스도 역시나 접근 제한자를 지원하지 않아 인스턴스 프로퍼티는 항상 public 이다.
즉, 클래드 필드 정의 제안을 사용하더라도 기본적으로 public이기때문에 외부에 노출이 그대로 된다.

private 필드의 앞에 # 을 붙여서 사용을 하고, 참조시에도 마찬가지로 # 을 붙여주면 된다.

class Person {
  // private 필드 정의
  #name = ''
  
  constructor(name) {
    this.#name = name
  }
}

const me = new Person('Lee')

// 잘못된 참조 ❌
console.log(me.#name) // SyntaxError
// 올바른 참조 ✅
console.log(me.name) // Lee

public 필드는 어디서든 참조할 수 있지만, private 필드는 클래스 내부 에서만 참조할 수 있다.

또한, private 필드는 반드시 클래스 몸체에 정의해야하며, 직접 constructor 에 정의하면 에러가 발생한다.

접근 가능성publicprivate
클래스 내부
자식 클래스 내부
클래스 인스턴스를 통한 접근

✋🏻 잠깐! 하지만, 2025 9월 현재 찾아본 바에 의하면

2021년 1월, TC39 프로세스의 stage3에 private 필드를 정의할 수 있는 새로운 표준 사양이 제안됐었고, 2025년 9월 현재 mdn에 공식적으로 등록되어 사용가능한 상태이다. 🥳👏🏻👏🏻🎉

👉🏻private 필드 mdn 보러가기

static 필드

정적 메서드는 프로토타입 메서드에서 살펴보았듯이 static 을 앞에 붙여서 정의할 수 있다.
하지만, static 키워드를 사용하여 정적 필드 를 정의할 수는 없었다.

하지만, private 와 마찬가지로 static 또한
2021년 1월, TC39 프로세스의 stage3에 static 필드를 정의할 수 있는 새로운 표준 사양이 제안됐었고, 2025년 9월 현재 mdn에 공식적으로 등록되어 사용가능한 상태이다. 🥳👏🏻👏🏻🎉

class MyMath {
  //static public 필드 정의
  static PI = 22 / 7
  // static private 필드 정의
  static #num = 10
  
  // static 메서드
  static increment() {
    return ++MyMath.#num
  }
}

console.log(MyMath.PI) // 3.142857142857143
console.log(MyMath.increment()) // 11

👉🏻static 필드 mdn 보러가기


상속에 의한 클래스 확장

클래스 상속과 생성자 함수 상속

프로토타입 기반 상속은 프로토타입 체인을 통해 상속받는 개념이였지만, 상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것이다.

extends 키워드

| 상속을 통해 클래스를 확장하기 위해서는 extends 키워드를 사용하여 상속받을 클래스를 정의해야한다.

// 수퍼(부모/베이스) 클래스 (상속받을 클래스)
class Base {}

// 서브(자식/파생) 클래스
class Derived extends Base {}

수퍼클래스와 서브클래스는 인스턴스의 프로토타입 체인뿐만 아니라 클래스간의 프로토타입 체인도 생성한다. ⇒ 프로토타입 메서드, 정적 메서드 모두 상속이 가능하다.

동적 상속

extends 키워드는 클래스뿐만 아니라 생성자 함수를 상속받아 클래스 확장을 할 수 도 있다.
(단, extends 키워드 앞에는 반드시 클래스가 와야한다.)

// 생성자 함수
function Base(a) {
  this.a = a
}

// 생성자 함수를 상속받는 서브 클래스
class Derived extends Base {}

extends 키워드 다음에는 [[Constructor]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 사용할 수 있으며, 이를 통해 동적으로 상속받을 대상을 결정할 수 있다.

서브 클래스의 constructor

앞서 살펴본 바와같이 클래스에서 constructor를 생략하면 다음과 같은 비어있는 constructor가 암묵적으로 정의된다.

constructor() {}

이걸 서브 클래스에서 생략을 하게되면 아래와 같은 형태의 constructor가 암묵적으로 정의된다.
(args는 new 연산자와 함께 클래스를 호출할 때 전달한 인수의 리스트이다.)

constructor(...args) { super(...args) }

여기서 super() 는 수퍼 클래스의 constructor(super-constructor) 이다.

super 키워드

| 함수처럼 호출할 수도 있고 this와 같이 식별자처럼 참조할 수 있는 특수한 키워드이다.

super 의 동작방식은 다음과 같으며 하나씩 살펴보자.

  • super 호출 → 수퍼 클래스의 constructor(super-constructor) 호출
  • super 참조 → 수퍼 클래스의 메서드 호출

1️⃣ super 호출

수퍼 클래스의 constructor 내부에서 추가한 프로퍼티를 그대로 갖는 인스턴스를 생성하면 서브 클래스의 constructor 는 생략할 수 있다. (새로 추가되는 프로퍼티가 없기 때문에)

하지만, 수퍼 클래스의 프로퍼티와 서브 클래스에서 추가한 프로퍼티를 갖는 인스턴스를 생성할 때에는 서브 클래스의 constructor 는 생략할 수 없으며, 수퍼 클래스의 constructor에 해당되는(전달해야하는) 인수가 있다면 서브 클래스의 constructor에서 super 를 통해 전달한다.

super 를 호출 할때에는 몇 가지 주의사항이 있다.
1. 서브 클래스에서 constructor를 생략하지 않는 경우에는 반드시 super 를 호출해야하며, 호출하지 않을 경우 ReferenceError 가 발생한다.
2. 서브 클래스의 constructor에서 this 를 참조하려면 super 를 먼저 참조해야한다. (순서가 있다!)

class Base {}

class Derived extends Base {
  constructor() {
    // 잘못된 순서 ❌
    this.a = 1
    super()
  }
}

class Base {}

class Derived extends Base {
  constructor() {
    // 올바른 순서 ✅
    super()
    this.a = 1
  }
}
  1. super는 반드시 서브 클래스의 constructor 에서만 호출해야하며, 서브 클래스가 아닌 다른 클래스에서 호출 시 SyntaxError 가 발생한다.

2️⃣ super 참조

super 를 참조할 때에도 몇 가지 알아둬야할 내용들이 있다.

  1. 서브 클래스의 프로토타입 메서드 내에서 사용되는 super.메서드명 에서 메서드명은 수퍼 클래스의 프로토타입 메서드를 가리킨다.
  • super참조를 통해 수퍼 클래스의 메서드를 참조하려면, super가 바인딩된 객체 (수퍼 클래스의 prototype 프로퍼티)에 바인딩된 프로토타입을 참조할 수 있어야한다.
  • super 참조는 클래스의 전유물이 아니므로 객체 리터럴에서도 사용할 수 있다.
  1. 서브 클래스의 정적 메서드 내에서의 super.메서드명 에서 메서드명은 수퍼 클래스의 정적 메서드를 가리킨다.
class Base {
  static sayHi() {
    return "Hi!"
  }
}

class Derived extends Base {
  static sayHi() {
    // 여기서의 sayHi는 수퍼 클래스의 정적 클래스인 sayHi 를 가리킨다.
    return `${super.sayHi()} how are you doing?`
  }
}

console.log(Derived.sayHi())

상속 클래스의 인스턴스 생성과정

1️⃣ 서브 클래스의 super 호출

자바스크립트 엔진은 클래스 평가 시, 수퍼/서브 클래스 구분을 위해 base , derived 를 값으로 갖는 내부 슬롯인 [[ConstructorKind]] 를 갖는다.

클래스를 상속받지 않는 클래스는 내부 슬롯([[ConstructorKind]])의 값이 base 로 설정되고, 상속받는 클래스의 내부 슬롯([[ConstructorKind]])의 값은 derived 로 설정된다.

서브 클래스는 직접 인스턴스를 생성하지 않고, 수퍼 클래스에게 인스턴스 생성을 위임하여 상속받는다.
⇒ 서브 클래스의 constructor 에서 꼭 super 로 호출해야한다.
(super 로 호출하지 않으면 인스턴스를 생성할 수 없다.)

2️⃣ 수퍼 클래스의 인스턴스 생성과 this 바인딩

수퍼 클래스의 constructor 내부의 코드가 실행되기 전에 암묵적으로 빈 객체를 생성하는데, 이 빈 객체가 클래스가 생성한 인스턴스이다.
생성된 인스턴스는 this 에 바인딩된다. ⇒ 수퍼 클래스의 constructor 내부의 this 는 생성된 인스턴스를 가리킨다.

3️⃣ 수퍼 클래스의 인스턴스 초기화

수퍼 클래스의 constructor가 실행되면 this 에 바인딩되어있는 인스턴스가 초기화된다.

4️⃣ 서브 클래스 constructor로의 복귀와 this 바인딩

super 호출이 종료되면 흐름은 서브 클래스의 constructor 로 돌아온다.

super 호출이 종료되면서 반환한 인스턴스가 this 에 바인딩되기 때문에 서브 클래스는 별도의 인스턴스를 생성하지 않고 그대로 사용한다.

이런 흐름상의 이유로 super 가 호출되지 않으면 인스턴스 생성도, this 바인딩도 할 수 없다.
앞서 super 키워드 내용을 살펴볼 때, '순서가 있다!' 라고 한 이유가 바로 이때문이다.

5️⃣ 서브 클래스의 인스턴스 초기화

super 호출 이후 서브 클래스의 constructor에 있는 인스턴스의 초기화가 실행된다.

6️⃣ 인스턴스 반환

클래스의 모든 처리가 완료되면 완성된 인스턴스가 바인딩된 this 의 암묵적 반환이 실행된다.


표준 빌트인 생성자 함수 확장

앞서 살펴봤듯이 extends 키워드 다음에는 클래스가 아니더라도 [[Constructor]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 사용할 수 있다.

String, Number, Array 같은 표준 빌트인 객체도 내부 메서드를 갖는 생성자 함수이므로 extends 키워드를 사용하여 확장 할 수 있다.

profile
FE Developer .mjs

0개의 댓글