클래스 (Class)

2025. 4. 13. 23:38·Develop/Javascript

자바스크립트는 프로토타입 기반 객체 지향 언어입니다.

 

하지만 클래스 기반 객체 지향 언어에 익숙한 사용자들에게는 프로토타입이 낯설었고, 이를 위해 새로운 객체 생성 메커니즘으로 자바스크립트에서도 ES6부터 클래스를 지원해주기 시작했습니다. 그렇다면 ES6부터 지원해주는 클래스는 단지 프로토타입의 문법적 설탕(기존에 존재하는 문법을 더욱 편리하고 직관적으로 만든 것)일까요? 

 

그렇지는 않습니다. 클래스와 생성자 함수는 둘 다 인스턴스를 생성하기는 하지만, 클래스는 생성자 함수보다 더욱 엄격하며 몇가지 추가적인 기능들을 제공하기도 합니다. 따라서 문법적 설탕이라기보다는 새로운 객체 생성 메커니즘으로 보는게 더욱 옳습니다.

오늘은 자바스크립트의 클래스에 대해 알아봅시다.

참고로,  클래스와 생성자 함수는 구분해서 인식해야합니다. 생성자 함수는 클래스가 도입되기 이전부터 존재하던 개념입니다.

 

 


클래스 (Class)

클래스는 오직 인스턴스를 만들기 위해 도입된 문법이므로, 일반 함수처럼 사용이 불가합니다.

따라서 반드시 new 키워드를 붙여서 호출해야합니다. 

//클래스 선언문
class Person {}

//익명 클래스 표현식
const Person = class {};

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

//인스턴스 생성
const me = new Person();

 

위의 예시를 보면 클래스를 표현식으로 정의할 수 있는데, 이는 클래스 또한 일급 객체라는 의미입니다.

더 나아가, 클래스도 결국 함수 객체며, 클래스 자체가 생성자 함수라고 볼 수 있습니다. 코드 평가 과정을 거쳐 함수 객체가 됩니다.

이런 이유로, 인스턴스의 프로토타입의 constructor 프로퍼티에는 클래스(즉, 생성자 함수)가 들어갑니다. 

하지만 앞으로 설명상의 편의를 위해, 클래스는 생성자 함수라고 부르지 않고 클래스라고 부르겠습니다. 

클래스는 호이스팅이 일어나지 않는다? 
 : 클래스에도 let, const와 마찬가지로 TDZ가 적용되어 호이스팅이 적용되지 않는 것처럼 보이는 것 뿐이다.

 

엄밀히 말하면 호이스팅이 일어나지 않을 수가 없죠. (코드 평가 과정을 거치니까)


클래스의 메서드 (생성자, 프로토타입 메서드, 정적 메서드)

클래스 몸체에는 3가지 종류의 메서드를 선언할 수 있습니다. 

 

1. 생성자(constructor) 

먼저 확실히 짚고 가자면, 지금 언급하는 클래스의 constructor는 프로로토타입의 constructor 프로퍼티와 다른 개념입니다. 

또한 생성자 함수와도 다른 개념이에요. 클래스를 통해 생성하는 인스턴스의 생성 및 초기화를 담당하는 특별한 메서드를 의미합니다.

class Person {
  constructor(name, address) {
    // 인수로 인스턴스 초기화
    this.name = name;
    this.address = address;
  }
}

// 인수로 초기 값을 전달한다. 초기 값은 constructor에 전달된다.
const me = new Person('Lee', 'Seoul');
console.log(me); // Person { name: "Lee", address: "Seoul" }

 

- constructor 함수는 존재하지 않아도 됩니다. (그럼 암묵적으로 빈 객체를 생성하는 constructor를 만듬)

- constructor 함수는 특정값을 return 하는 것을 권장하지 않습니다. 암묵적으로 this (생성 및 초기화된 객체)를 반환하기 때문입니다.

- constructor 함수 내부에서 프로퍼티 추가 및 초기화가 진행되어야 합니다. (문법)

 

그런데 신기한 점이 하나 있습니다. 인스턴스를 생성한 뒤에 해당 인스턴스를 로그로 찍어보면 constructor라는 메서드가 보이지 않아요.

(물론 __proto__ 의 constructor는 보입니다. 하지만 이건 다른 개념.. 이제 더 반복 언급하지 않겠습니다)

 

그 이유는, 정확히는 constuctor는 평가 과정에서 메서드로 해석되지 않고 함수 내부의 코드로 변환되기 때문입니다.

평가과정을 거치면서 constructor에 기술된 동작을 하는 함수 객체(즉, 클래스)가 생성됩니다. 

 

2. 프로토타입 메서드

인스턴스를 통해 호출할 수 있는 메서드입니다.

class Person {
  // 생성자
  constructor(name) {
    // 인스턴스 생성 및 초기화
    this.name = name;
  }

  // 프로토타입 메서드
  sayHi() {
    console.log(`Hi! My name is ${this.name}`);
  }
}

const me = new Person('Lee');
me.sayHi(); // Hi! My name is Lee

 

생성자함수를 통해 인스턴스를 생성할 때는 .prototype. 을 명시해주어야 했는데, 클래스의 프로토타입 메서드는 명시하지 않아도 됩니다.

 

3. 정적 메서드

클래스를 통해 바로 호출이 가능한 메서드입니다. 

static 키워드만 붙이면 됩니다. 

class Person {
  // 생성자
  constructor(name) {
    // 인스턴스 생성 및 초기화
    this.name = name;
  }

  // 정적 메서드
  static sayHi() {
    console.log('Hi!');
  }
}

// 정적 메서드는 클래스 이름으로 호출
Person.sayHi(); // Hi!

 


프로토타입 메서드 vs 정적 메서드 (언제 사용하는가?)

this를 사용 여부에 따라 나눠서 사용하면 됩니다. 

 

우선 두 메서드는 자신이 속해있는 프로토타입 체인도 다르고, 호출 방식도 다릅니다. 

그 중 가장 큰 차이점은 인스턴스 프로퍼티를 참조할 수 있는가 없는가입니다.

 

따라서 인스턴스에 초기화된 값을 활용해야하는 경우에는 프로토타입 메서드로 활용하고,

클래스를 하나의 네임스페이스로 사용하여 메서드를 모아놓기 위해서는 정적 메서드를 활용하는 것이 좋습니다. 


클래스의 프로퍼티

2가지 종류의 프로퍼티가 있습니다. 

1. 인스턴스 프로퍼티

constructor 내부에서 정의되는 프로퍼티입니다. 위에서 봤으니 생략하겠습니다.

 

2.접근자 프로퍼티 

다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티입니다.

자체적으로는 값([[Value]] 내부 슬롯)을 갖지 않습니다. 클래스만의 개념은 아니고, 객체 리터럴로 정의할 때도 사용할 수 있었습니다.

정의 방식을 객체 리터럴이나 클래스나 같아요.

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  // getter 함수
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  // setter 함수
  set fullName(name) {
    [this.firstName, this.lastName] = name.split(' ');
  }
}

const me = new Person('Ungmo', 'Lee');

// 데이터 프로퍼티를 통한 접근
console.log(`${me.firstName} ${me.lastName}`); // Ungmo Lee

// 접근자 프로퍼티를 통한 저장 (setter 호출)
me.fullName = 'Heegun Lee';
console.log(me); // Person { firstName: "Heegun", lastName: "Lee" }

// 접근자 프로퍼티를 통한 참조 (getter 호출)
console.log(me.fullName); // Heegun Lee

// 접근자 프로퍼티는 get/set 등을 갖는 프로퍼티 디스크립터를 가짐
console.log(Object.getOwnPropertyDescriptor(Person.prototype, 'fullName'));

 

단순하게 앞에 get, set을 정의해주면 됩니다. 다만 유의할 점은 다음과 같아요.

getter 함수는 반드시 무언가를 반환해야하고, setter 함수는 반드시 매개변수가 1개 존재해야한다.

 


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

조금 더 깊게 들어가서, 클래스에 new 연산자를 사용하여 인스턴스를 생성하는 과정을 알아봅시다.

 

인스턴스 생성과 this 바인딩

1. 빈 객체인 인스턴스를 생성합니다. 

2. 인스턴스를 프로토타입(클래스의 prototype 프로퍼티가 가리키는 객체)과 연결합니다. 

3. this에 인스턴스를 바인딩합니다.

 

인스턴스 초기화

4. constructor 내부 코드를 실행합니다 (프로퍼티 추가 및 초기화)

 

인스턴스 반환

5. this를 암묵적으로 반환합니다. (즉, 완성된 인스턴스를 반환합니다.)


클래스 필드 

다른 언어에서는 클래스의 멤버(필드)를 정의하는 방식은 단순했어요. 그냥 클래스 몸체 내부에 변수처럼 사용하면 됐죠. (this도 없이요!)

다만 자바스크립트의 경우에는 constructor 내부에서 this를 통해 프로퍼티를 정의하는 방식을 사용해야만 했는데, 이제는 안 그래도 됩니다. 자바스크립트에서 클래스 필드를 사용하자는 제안이 TC39 프로세스를 통해 stage4로 받아들여졌기 때문입니다.

 

모든 클래스 필드는 인스턴스 프로퍼티(프로토타입 프로퍼티가 아닌)가 된다라는 사실과, 자바스크립트에서 몇가지 유의사항만 알아둡시다.

1. 클래스필드에는 this를 사용할 수 없다.

2. 클래스 필드를 참조하는 경우에는 this를 반드시 사용해야한다.

3. 클래스 필드에 함수를 할당하는 것은 권장되지 않는다. (인스턴스 메서드가 되므로)

class Person {
  // 클래스 필드
  name = 'Lee';

  // 클래스 필드에 함수 할당 (인스턴스 메서드 - 권장되지 않음)
  getName = function () {
    return this.name;
  }

  // 또는 이렇게 화살표 함수로도 가능
  // getName = () => this.name;
}

const me = new Person();

console.log(me);           // Person { name: 'Lee', getName: [Function: getName] }
console.log(me.getName()); // Lee

 


private 필드 

오직 클래스 내부에서만 접근할 수 있는 필드입니다. 

 

이 또한 표준사항으로 제안되고, 받아들여졌습니다. 따라서 이젠 private 필드를 사용할 수 있습니다. (기본적으로는 public 이었음)

참고로 타입스크립트에서는 public, private 뿐만 아니라 protected를 모두 지원합니다.

class Person {
  // private 필드 선언
  #name;

  constructor(name) {
    // private 필드 초기화
    this.#name = name;
  }

  // private 필드 접근용 public 메서드
  getName() {
    return this.#name;
  }
}

const me = new Person('Lee');

// 외부에서 직접 접근하면 에러!
console.log(me.#name); // ❌ SyntaxError: Private field '#name' must be declared in an enclosing class

// 접근자 메서드를 통해 간접 접근
console.log(me.getName()); // ✅ Lee

 

!주의! 
타입스크립트에서는 private라는 키워드를 직접 사용해서 선언할 수 있으나, 자바스크립트에서는 불가능합니다.
오직 #라는 prefix를 붙임으로써 가능합니다. 헷갈리지 마세요! (public, protected 키워드도 타입스크립트에서만 가능)

 

 


static 필드 

이제 정적 필드도 정의할 수 있습니다. (마찬가지로 최신 브라우저, node.js에서 2021년 이후부터 사용가능해졌습니다.)

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 ✅
console.log(MyMath.increment()); // 12 ✅

 

 


상속에 의한 클래스 확장 

클래스 개념에서는 상속이 필수죠, 자바스크립트에서는 이를 클래스 확장한다고 표현하곤 합니다. 

그 이유는 실제로 프로토타입 기반 상속은 프로토타입 체인을 통해 다른 객체의 자산을 상속받는 개념이지만

상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하기 때문입니다.

 

클래스가 등장하기 이전에는 의사 클래스 상속 패턴이 필요했는데, 이제 더이상 필요없으니 살펴보진 않겠습니다.

 


extends 키워드

extends 키워드를 통해 상속을 받습니다. 해당 키워드는 수퍼클래스와 서브클래스 간의 상속 관계를 설정하는 역할이에요.

참고로, extends 키워드 다음에는 클래스뿐만 아니라 [[Constructor]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 사용할 수 있습니다. 이를 통해 클래스가 생성자 함수를 상속 받을 수도 있고, 표준 빌트인 생성자 함수를 상속 받을 수도 있어요.

 

아래의 예시는 이를 이용해, 동적으로 상속받을 대상을 결정하는 예시입니다.

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

 

위의 예시를 보면 서브클래스인 Derived에서 constructor를 생략했는데, 사실 아래와 같은 constructor가 암묵적으로 정의됩니다.

//Base1으로 예시
class Base1{
  constructor(){}
}

class Derived extends (condition ? Base1 : Base2) {
  constructor(...args) {
    super(...args);
  }
}

 

super 키워드나, 서브클래스가 저런 식으로 생성자를 암묵적 정의하는 이유는 바로 살펴봅시다!

 

 

super 키워드

super는 호출하거나, 참조할 수 있는 특별한 키워드입니다. 워딩에서 느껴지듯이 부모 클래스와 연관되어 있어요.

 

1. super 호출

수퍼클래스의 constructor(super-constructor)를 호출합니다.

 

주의 사항은 다음과 같습니다. 

1. 반드시 서브클래스의 constructor에서는 super를 호출할 것

2. 서브클래스의 constructor에서는 super를 호출하기 전에는 this를 참조할 수 없음

3. 서브클래스의 constructor에서만 super를 호출할 것 

// 슈퍼클래스
class Base {
  constructor(a, b) {
    this.a = a;
    this.b = b;
  }
}

// 서브클래스
class Derived extends Base {
  constructor(a, b, c) {
    super(a, b); // 부모 생성자 호출
    this.c = c;  // 서브클래스 고유 속성 초기화
  }
}

const derived = new Derived(1, 2, 3);
console.log(derived); // Derived { a: 1, b: 2, c: 3 }

 

2. super 참조

메서드 내에서 super를 참조하여 수퍼클래스의 메서드를 호출할 수 있습니다.

단, 반드시 메서드 축약 표현으로 정의된 함수만이 super 참조를 할 수 잇습니다. 

 

그 이유를 간략히만 설명하면 다음과 같습니다.

메서드 축약 표현으로 정의된 함수가 [[HomeObject]] 를 가지기 때문인데, 이 내부 슬롯에서는 자신을 바인딩하고 있는 객체를 가리킵니다. 따라서 call 메서드를 통해 this를 전달해야할 때, Base.prototype이 아닌 인스턴스를 가리켜야하므로(인스턴스의 프로퍼티를 넘겨서 부모 메서드에서 사용할 수 있기 때문입니다) 바인딩된 객체인 인스턴스를 가리키는 [[HomeObject]] 가 존재해야만 합니다. 

const base = {
  name: 'Lee',
  sayHi() {
    return `Hi! ${this.name}`;
  }
};

const derived = {
  __proto__: base, // base를 prototype으로 상속
  sayHi() {
    return `${super.sayHi()}. How are you doing?`; // super를 통해 base.sayHi 호출
  }
};

console.log(derived.sayHi()); // Hi! Lee. How are you doing?

 

 


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

이젠 상속받는 클래스(자식 클래스, 서브클래스)가 인스턴스를 생성하는 과정을 살펴봅시다.

하나 알아둬야할 것은, 서브클래스는 자신이 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임합니다.

// 슈퍼 클래스
class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }

  toString() {
    return `width = ${this.width}, height = ${this.height}`;
  }
}

// 서브 클래스
class ColorRectangle extends Rectangle {
  constructor(width, height, color) {
    super(width, height); // 부모 생성자 호출
    this.color = color;
  }

  // 메서드 오버라이딩
  toString() {
    return `${super.toString()}, color = ${this.color}`;
  }
}

// 인스턴스 생성
const colorRectangle = new ColorRectangle(2, 4, 'red');

console.log(colorRectangle); // ColorRectangle { width: 2, height: 4, color: 'red' }
console.log(colorRectangle.getArea()); // 8
console.log(colorRectangle.toString()); // width = 2, height = 4, color = red

 

1. 서브클래스 super 호출

수퍼클래스에 인스턴스 생성을 위임하기 때문에, 만약 super 호출이 존재하지 않으면 에러가 발생합니다.

 

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

수퍼클래스가 인스턴스를 직접 생성하지만, new 키워드가 붙은 곳은 서브클래스였습니다. new 연산자와 함께 호출된 함수를 가리키는 new.target 은 서브클래스를 가리키게 되며, 따라서 인스턴스는 new.target이 가리키는 서브클래스가 생성한 것으로 처리됩니다.

 

이로 인해 생성된 인스턴스의 프로토타입은 수퍼클래스의 prototype 프로퍼티가 가리키는 객체가 아닌 서브클래스의 prototype 프로퍼티가 가리키는 객체가 됩니다.

 

this는 당연히 생성된 인스턴스에 바인딩되고요.

 

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

서브클래스로부터 받아온 인자값으로 수퍼클래스의 constructor가 실행되며 this에 바인딩된 인스턴스가 초기화(프로퍼티 생성 및 초기화)됩니다.

 

4. 서브클래스 constructor로 복귀 후 this 바인딩

이제 다시 서브클래스의 나머지 constructor 코드가 실행이 됩니다. 이때 서브클래스는 별도의 인스턴스를 만들지 않고, super가 반환한 인스턴스를 this에 바인딩하여 그대로 사용합니다. (이런 원리로 인해 super 호출이 먼저 일어나야했던 겁니다)

 

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

오직 서브클래스에만 존재하는 프로퍼티가 추가되고, 초기화됩니다.

 

6. 인스턴스 반환

암묵적으로 this를 반환하며 끝납니다.

 

 


 

클래스도 여기까지 다루겠습니다. 

유저와 맞닿는 프로젝트에서는 클래스를 사용할 일이 당장 많지는 않지만 언젠가 라이브러리를 만들거나 구조를 많이 짜야할때는 코드 재사용성의 측면에서 클래스를 많이 활용할 것 같기도 하네요. 긴 글 읽어주셔서 감사합니다 !

 

 

 

 

 

참고 서적 ) 모던 자바스크립트 Deep Dive (저자-이웅모)

'Develop > Javascript' 카테고리의 다른 글

ESM과 CJS 그리고 모듈 객체  (0) 2025.09.22
이벤트 (Event)  (0) 2025.05.25
프로토타입 (ProtoType)  (0) 2025.02.11
클로저 (Closure)  (0) 2025.02.06
실행 컨텍스트 (Execution Context)  (0) 2025.02.05
'Develop/Javascript' 카테고리의 다른 글
  • ESM과 CJS 그리고 모듈 객체
  • 이벤트 (Event)
  • 프로토타입 (ProtoType)
  • 클로저 (Closure)
ocahs
ocahs
개발 내용을 담습니다.
  • ocahs
    ocahs 개발 블로그
    ocahs
  • 전체
    오늘
    어제
    • 분류 전체보기 (47)
      • Develop (47)
        • Frontend (25)
        • Javascript (7)
        • Algorithm (14)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    비트 연산자 활용 예시
    Working Set Model
    Promise
    번들러
    line sweeping
    요청의 역사
    JS
    재귀타입
    vite
    promise reject
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
ocahs
클래스 (Class)
상단으로

티스토리툴바