[모딥다] 25장. 클래스

vanLan·2026년 2월 20일

모딥다

목록 보기
18/25
post-thumbnail

25장. 클래스

JS는 프로토타입 기반 객체지향 언어임.
클래스는 완전히 새로운 개념이 아니라 기존 프로토타입 기반 상속을 더 명료하게 사용하기 위해 ES6 부터 도입된 문법적 설탕에 가까움.
하지만, 클래스는 생성자 함수 기반의 객체 생성 방식보다 견교하고 명료함.
특히 extendssuper 키워드는 상속 관계 구현을 더욱 간결하고 명료하게 하므로, 새로운 객체 생성 매커니즘으로 보는것이 타당함.

📁 생성자 함수 vs 클래스

1. 공통점

표면적인 문법은 다르지만, JS엔진 내부에서는 결국 동일한 매커니즘으로 동작.

  • 둘다 JS의 프로토타입 기반으로 동작.
  • 동일한 구조와 메서드를 가진 여러 개의 객체(인스턴스)를 효율적으로 생성하기 위해 사용.
  • 생성된 인스턴스에 대해 instanceof 연산자를 사용하여 출처확인 가능.
  • 자식 객체가 부모 객체의 속성과 메서드를 물려받아 재사용 가능하게 함. ( 객체지향의 상속 구현 )

2. 차이점

클래스는 개발자의 실수를 줄여주고 더 직관적이고 안전한 코드를 작성할 수 있도록 여러 가지 제약과 편의 기능을 추가함.

구분생성자함수클래스
new 키워드 호출new 연산자 없이 일반 함수로 호출 가능new 연산자 없이 호출시 타입 에러(TypeError)가 발생함
호이스팅함수 선언문으로 작성 시 호이스팅이 발생하여 선언 전에 호출 가능호이스팅이 발생하지만, TDZ의 영향을 받아 선언 전에 접근 시 참조에러(ReferenceError) 발생.
Strict Mode기본적으로 적용되지 않음. (필요시 파일 최상단이나 함수 내부에 'use strict'를 명시해야 함.)클래스 내부 코드는 암묵적으로 항상 엄격모드로 실행됨.
매서드 열거 여부프로토타입에 추가한 메서드는 기본적으로 열거 가능 (enumerable: true)하여 for ...in 루프에 노출됨.클래스 내부에 정의된 메서드는 기본적으로 열거 불가. (enumerable: false)
상속 구현 방식Object.create(Parent.prototype)로 체인을 연결하고, 자식 생성자에서 Parent.call(this)로 부모의 this를 바인등하는 등 수동 작업이 많음extends 키워드로 상속을 선언하고, super()를 호출하여 부모의 생성자와 메서드에 매우 쉽게 접근

📁 클래스 정의

  • class 키워드를 사용하여 정의함. (파스칼 케이스를 사용하는 것이 일반적이나 사용하지 않아도 에러 발생 X)

    class Person {}
  • 일반적이진 않지만 함수와 마찬가지로 표현식 정의도 가능

    // 익명 클래스 표현식
    const Person = class {};
    
    // 기명 클래스 표현식
    const Person = class MyClass {};
  • 클래스에서 정의할 수 있는 메서드는 constructor(생성자), 프로토타입 메서드, 정적 메서드 세가지가 있음.

    // 클래스 선언문
    class Person {
      // 생성자
      constructor(name) {
        // 인스턴스 생성 및 초기화
        this.name = name;
      }
      
      // 프로토타입 메서드
      sayHi() {
        console.log(`Hi! My name is ${this.name}`);
      }
      
      // 정적 매서드
      static sayHello() {
        console.log('Hello!');
      }
    }
    
    // 인스턴스 생성
    const me = new Person('Kim');
    
    // 인스턴스 프로퍼티 참조
    console.log(me.name);  // Kim
    // 프로토타입 메서드 호출
    me.sayHi();  // Hi! My name is Kim
    // 정적 메서드 호출
    Person.sayHello();  // Hello!

📁 클래스 호이스팅

  • 클래스는 함수로 평가되기 때문에, 클래스 선언문으로 정의한 클래스는 함수 선언문과 같이 소스코드 평가 과정(런타임 이전)에 먼저 평가되어 함수 객체를 생성함.

  • 이때 클래스가 평가되어 생성된 함수 객체는 생성자 함수로서 호출할 수 있는 함수, 즉 constructor임.

  • 생성자 함수로써 호출할 수 있는 함수는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성됨. ( 프로토타입과 생성자 함수는 언제나 쌍으로 존재하기 때문 )

  • 단, 클래스는 클래스 정의 이전에 참조 불가. (클래스 선언문 이전에 TDZ에 빠지기 때문)

    const Person = '';
    
    {
      console.log(Person);
      // ReferenceError: Cannot access 'Person' before initialization
      
      // 클래스 선언문
      class Person {}
    }

📁 인스턴스 생성

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

  • 생성자 함수와 달리 클래스는 인스턴스를 생성하는 것이 유일한 존재 이유이므로 반드시 new 연산자와 함께 호출.

    class Person {}
    
    const me = Person();
    // TypeError: Class constructor Person cannot be invoked without 'new'
  • 기명 함수 표현식과 마찬가지로 클래스 표현식에서 사용한 클래스 이름은 외부 코드에서 접근 불가.

    const Person = class MyClass {};
    
    const me = new Person();
    
    console.log(MyClass);  // ReferenceError: MyClass is not defined
    
    const you = new MyClass();  // ReferenceError: MyClass is not defined

📁 메서드

1. constructor

  • 인스턴스를 생성하고 초기화하기 위한 특수한 메서드. (이름 변경 X)
    class Person {
      // 생성자
      constructor(name) {
        // 인스턴스 생성 및 초기화
        this.name = name;
      }
    }
  • 클래스는 평가되어 함수 객체가 되기에, 클래스 또한 함수 객체의 고유의 프로퍼티를 모두 갖고 있음.
  • 모든 함수 객체가 가지고 있는 prototype 프로퍼티가 가리키는 프로토타입 객체의 constructor 프로퍼티는 클래스 자신을 가리키고 있음. ( 클래스가 인스턴스를 생성하는 생성자 함수라는 것을 의미 )
  • constructor 내부에서 this에 추가한 프로퍼티는 인스턴스 프로퍼티가 됨. ( 생성자 함수와 마찬가지로 this는 클래스가 생성한 인스턴스를 가리킴 )
  • constructor는 메서드로 해석되는 것이 아닌, 클래스가 평가되어 생성된 함수 객체 코드의 일부가 됨.
  • 생성자 함수와의 차이점:
    • 클래스 내에 오직 한 개만 존재.

    • 생략 가능 ( 빈 constructor가 암묵적으로 정의됨. )

    • 프로퍼티가 추가되어 초기화된 인스턴스를 생성하려면 constructor 내부에서 this에 인스턴스 프로퍼티를 추가.

      class Person {
        constructor(name, address) {
          // 고정값으로 인스턴스 초기화
          this.example = example;
          
          // 인수로 인스턴스 초기화
          this.name = name;
          this.address = address;
        }
      }
      
      const me = new Person('Lee', 'Seoul');
      console.log(me);  // Person { example: "example", name: "Lee", address: "Seoul" }
    • constructor는 별도의 반환문을 갖지 않아야함. ( new 연산자와 함께 클래스가 호출되면 생성자 함수가 동일하게 암묵적으로 this(인스턴스를 가리킴)를 반환하기 때문 )

2. 프로토타입 메서드

  • 생성자 함수와 다르게 클래스는 prototype 프로퍼티에 메서드를 추가하지 않아도 기본적으로 프로토타입 메서드가 됨.

    class Person {
      // 생성자
      constructor(name) {
        // 인스턴스 생성 및 초기화
        this.name = name;
      }
      
      // 프로토타입 메서드
      sayHi() {
        console.log(`Hi! My name is ${this.name}`);
      }
    }
    
    const me = new Person('Kim');
    me.sayHi();  // Hi! My name is Kim
  • 생성자 함수와 마찬가지로 클래스가 생성한 인스턴스는 프로토타입 체인의 일원이 됨.

3. 정적 메서드

  • 인스턴스를 생성하지 않아도 호출할 수 있는 메서드.

  • staic 키워드를 붙이면 정적 메서드가 된됨.

    class Person {
      // 생성자
      constructor(name) {
        // 인스턴스 생성 및 초기화
        this.name = name;
      }
      
      // 정적 메서드
      static sayHi() {
        console.log('Hi');
      }
    }
    
    Person.sayHi();  // Hi!

4. 정적 메서드와 프로토타입 메서드의 차이

구분정적 메서드프로토타입 메서드
저장위치Parent(생성자 함수 객체자체)Parent.prototype
this 바인딩클래스 자체를 가리킴생성된 인스턴스를 가리킴
체인구조클래스 자체에 속하므로, 인스턴스의 프로토타입 체인에는 존재하지 않음. (상속시 부모 클래스 자체가 자식 클래스의 프로토타입이 됨.)인스턴스 -> Class.prototype -> Object.prototype 순으로 탐색

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

  • function 키워드를 생략한 메서드 축약 표현 사용.
  • 객체 리터럴과 다르게 클래스에서 메서드를 정의할 때 콤마 사용 X.
  • 암묵적 strict mode로 실행.
  • for ...in문이나 Object.keys 메서드 등으로 열거 불가.
  • 내부 메서드 [[Construct]]를 갖지 않는 non-constructor. ( new 연산자와 함꼐 호출 불가 )

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

1. 인스턴스 생성과 this 바인딩

  • new 연산자와 함께 클래스 호출 -> 빈 객체 생성(완성되지 않은 인스턴스) -> 생성한 인스턴스의 프로토타입으로 클래스의 prototype 프로퍼티가 가리키는 객체가 설정됨 -> 인스턴스는 this에 바인딩 됨. -> 고로 this는 인스턴스를 가리키게 됨.

2. 인스턴스 초기화

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

3. 인스턴스 반환

  • 클래스의 모든 처리가 끝나면 완성된 인스턴스(바인딩된 this)가 암묵적으로 반환됨.

📁 프로퍼티

1. 인스턴스 프로퍼티

  • constructor 내부에서 정의 해야함.
  • constructor 내부에서 this에 추가한 프로퍼티는 언제나 클래스가 생성한 인스턴스의 프로퍼티가 됨.

2. 접근자 프로퍼티

  • 자체적으로 값([[Value]] 내부슬롯)을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티
  • get, set 키워드를 동일하게 사용
  • 클래스 메서드는 기본적으로 프로토타입 메서드 이므로, 접근자 프로퍼티 또한 인스턴스 프로퍼티가 아닌 프로토타입의 프로퍼티임.

3. 클래스 필드 정의 제안

  • 클래스 필드: 클래스가 생성할 인스턴스의 프로퍼티를 가리키는 용어.

  • 초기 ES6 클래스 문법에서는 클래스 몸체 안에 메서드만 선언할 수 있었으나, ES13 이후 부터는 클래스 필드를 도입하여 constructor 없이도 몸체에 직접 변수 선언이 가능함.

    // 초기 ES6
    class Player {
      constructor() {
        this.hp = 100;  // 인스턴스 필드
      }
    }
    Player.maxLevel = 99;  // 정적 필드 (밖에서 정의 해야 함)
    
    // ES13 이후
    class Player {
      hp = 100;  // Public 인스턴스 필드
      
      status() {
        console.log(hp);  // ReferenceError: hp is not defined
        console.log(this.hp);
      }
    }
  • 인스턴스를 생성할 때 외부 초기값으로 클래스 필드를 초기화할 필요가 있다면 constructor에서 인스턴스 프로퍼티를 정의하고, 필요가 없다면 클래스 필드 정의 제안을 사용하는것이 기본적임.

4. private 필드 정의 제안

  • private 필드의 선두에 #을 붙여 사용. ( 참조 시에도 #을 붙여주어야 함 )

  • public 필드와 다르게 private 필드는 클래스 내부에서만 참조할 수 있음. (접근자 프로퍼티를 통해 간접적으로 접근하는 방법은 유효 )

  • private 필드는 반드시 클래스 몸체에 정의해야 함.

    class Person {
      // private 필드 정의
      #name = '';
      
      constructor(name) {
        this.#name = name;
      }
      
      get name() {
        return this.#name.trim();
      }
    }
    
    const me = new Person(' Kim ');
    console.log(me.name);  // Kim

5. static 필드 정의 제안

  • ES22 이후 부터 정적 필드를 정의할 수 있게 됨.
  • 단, static 필드는 인스턴스 프로퍼티가 아닌, 클래스의 프로퍼티임.
    class MyMath {
      // static public 필드 정의
      static PI = 22 / 7;
      
      // static private 필드 정의
      static #num = 10;
      
      // static 메서드
      static increment() {
        return ++MyMath.#num;
      }
    }

📁 상속에 의한 클래스 확장

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

  • 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것.

    class Animal {
      constructor(age, weight) {
        this.age = age;
        this.weight = weight;
      }
      
      eat () { return 'eat'; }
      move () { return 'move'; }
    }
    
    class Bird extends Animal {
      fly () { return 'fly'; }
    }
    
    const bird = new Bird(1, 5);
    
    console.log(bird);  // Bird { age: 1, weight: 5 }
    console.log(bird instance Bird);  // true
    console.log(bird instance Animal);  // true
    
    console.log(bird.eat());  // eat
    console.log(bird.move());  // move
    console.log(bird.fly());  // fly

2. extends 키워드

  • 상속을 통해 클래스를 확장하려면 extends 키워드를 사용하여 상속받을 클래스를 정의

    // 수퍼(베이스/부모) 클래스
    class Base {}
    
    // 서브(파생/자식) 클래스
    class Derived extends Base {}

3. 동적 상속

  • extends 키워드는 클래스 뿐 아니라 생성자 함수를 상속받아 클래스를 확장할 수 있음. ( 단, 상속받는 주체는 클래스여야 함. )
  • 상속받을 주체는 클래스 뿐 아니라 [[Construct]] 냅부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식이 가능.
  function Base1() {}
  
  class Base2 {}
  
  let condition = true;
  
  class Derived extends (condition ? Base1 : Base2) {}
  
  const derived = new Derived();
  console.log(derived);  // Derived {}
  
  console.log(derived instanceof Base1);  // true
  console.log(derived instanceof Base2);  // false

4. 서브클래스 constructor

  • 서브클래스 또한 constructor 생략시 암묵적으로 정의됨.

    // 수퍼 클래스
    class Base() {}
    
    // 서브 클래스
    class Dervied extends Base {
      // 암묵적 정의 예시
      constructor(...args) { super(...args); }
    }
    
    const derived = new Derived();
    console.log(derived);

5. super 키워드

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

🔹 super 호출

  • super를 호출하면 수퍼클래스의 constructor를 호출.

  • 주의사항:

    • 서브클래스에서 constructor를 생략하지 않는 경우 반드시 super를 호출해야 함.
    • 서브클래스의 constructor에서 super를 호출하기 전에는 this를 참조할 수 없음.
    • super는 반드시 서브클래스의 constructor에서만 호출
    class Animal {
      constructor(name) {
        this.name = name;
      }
    }
    
    class Dog extends Animal {
      constructor(name, breed) {
        // 1. 부모 클래스의 constructor(name)를 호출합니다.
        // 이 작업이 끝나야만 자식 클래스에서 'this'를 사용할 수 있습니다.
        super(name); 
        
        // 2. 자식 클래스만의 고유 속성을 추가합니다.
        this.breed = breed;
      }
    
      bark() {
        console.log(`${this.name}(${this.breed})가 멍멍 짖습니다!`);
      }
    }
    
    const myDog = new Dog("초코", "푸들");
    myDog.bark(); // 초코(푸들)가 멍멍 짖습니다!

🔹 super 참조

  • 메서드 내에서 super를 참조하면 수퍼클래스의 메서드를 호출할 수 있음.

    class Animal {
      constructor(name) {
        this.name = name;
      }
      
      getName() {
        return this.name;
      }
    }
    
    class Dog extends Animal {
      constructor(name, breed) {
        // 1. 부모 클래스의 constructor(name)를 호출합니다.
        // 이 작업이 끝나야만 자식 클래스에서 'this'를 사용할 수 있습니다.
        super(name); 
        
        // 2. 자식 클래스만의 고유 속성을 추가합니다.
        this.breed = breed;
      }
      
      bark () {
        console.log(`${super.getName()}(${this.breed})가 멍멍 짖습니다.`)
      }
    }
    
    const myDog = new Dog("초코", "푸들");
    myDog.bark(); // 초코(푸들)가 멍멍 짖습니다!

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

🔹 서브클래스의 super 호출

  • JS엔진은 클래스를 평가할 때 수퍼클래스와 서브클래스를 구분하기 위해 "base" 또는 "derived"를 값으로 갖는 [[ConstructorKind]] 내부슬롯을 갖음.
  • 다른 클래스를 상속받지 않는 클래스(생성자 함수)는 "base"로 설정, 상속 받는경우 "derived"로 설정됨.
  • 이를 통해 수퍼클래스와 서브클래스는 new 연산자와 함께 호출되었을 때 동작이 구분됨.
  • 서브클래스는 자신이 직접 인스턴스를 생성하지 않고, 수퍼클래스에 인스턴스 생성을 위임. ( 서브클래스의 constructor에서 반드시 super를 호출해야 하는 이유 )

🔹 수퍼클래스의 인스턴스 생성과 this 바인딩

  • 수퍼클래스는 인스턴스를 생성하지만, new 연산자와 함께 호출된 클래스가 서브 클래스 이므로, new.target이 가리키는 서브클래스가 생성한 것으로 처리됨.

🔹 수퍼클래스의 인스턴스 초기화

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

🔹 서브클래스 constructor로의 복귀와 this 바인딩

  • super의 호출이 종료되고, 서브클래스의 constructor로 복귀. 이때 super가 반환한 인스턴스가 this에 바인딩 됨. 서브클래스는 별도의 인스턴스를 생성하지 않고 super가 반환한 인스턴스를 this에 바인딩 하여 그대로 사용.
  • 위와 같이 동작하기 때문에 super가 호출되지 않으면 인스턴스가 생성되지 않으며, this 바인딩도 불가하기 때문에, 서브클래스에서 super 호출전 this를 참조할 수 없는 이유임.

🔹 서브클래스의 인스턴스 초기화

  • super 호출 이후, 서브클래스의 constructor 내부의 인스턴스 초기화가 실행. this에 바인딩 되어있는 인스턴스에 프로퍼티를 추가하고 constructor가 인수로 전달받은 초기값으로 인스턴스 프로퍼티를 초기화한 함.

🔹 인스턴스 반환

  • 클래스의 모든 처리가 끝나면 완성된 인스턴스(바인딩된 this)가 암묵적으로 반환됨.

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

  • extends 키워드의 상속의 주체는 클래스뿐만이 아니라 [[Construct]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 사용할 수 있으므로, String, Number, Array와 같은 표준 빌트인 객체도 상속 확장하여 사용이 가능.
profile
프론트엔드 개발자를 꿈꾸는 이

0개의 댓글