클래스

345·2023년 7월 4일

모던 JavaScript

목록 보기
20/23

동일한 종류의 객체를 여러 개 생성해야 할 때, new 연산자와 생성자 함수를 사용하기도 하지만 클래스 문법을 사용할 수도 있습니다.

✅ 클래스

자바스크립트에서 클래스는 함수의 한 종류입니다.
함수처럼 클래스도 다른 표현식 내부에서 정의, 전달, 반환, 할당할 수 있죠.
다만 블록 내의 본문을 실행하는 일반적 함수와는 다른 문법과 기능을 가집니다.

클래스 Class 로 만든 객체(인스턴스) 의 프로토타입은 Class.prototype 입니다.

기본문법은 이렇습니다.

class MyClass {
  // 여러 메서드를 정의할 수 있음
  // 클래스 바디 --
  
  // 생성자 constructor
  constructor() { ... } 
  
  // 메서드
  // 메서드 사이에 쉼표를 쓰지 않음
  method1() { ... } 
  method2() { ... }
  method3() { ... }
  ...
}
  • 생성자 constructor 필요
    • new 연산자에 의해 자동 호출, 객체 초기화 기능
    • 생성자 안에 생성될 객체의 프로퍼티 정의
  • 메서드를 클래스 내부에 정의 가능
    • 메서드 사이 쉼표 없음
    • 메서드는 열거 불가능 (enumerable: false)
  • 클래스 바디 부분은 원래 사용하지 않았으나, 클래스 필드 문법이 등장하며 쓰기도 합니다

생성자는 특수한 메서드로 객체 생성과 객체 멤버 변수 초기화를 담당합니다.
메서드는 각종 기능을 구현하고, 클래스 필드에 선언한 변수는 객체 자체에 엮인 것으로 취급합니다.

class User {

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

  sayHi() {
    alert(this.name);
  }

}

// 사용법:
let user = new User("John");
user.sayHi();

classnew Function 과 비슷합니다.
클래스가 하는 일은 다음과 같다고 볼 수 있습니다.

  1. User 라는 함수를 만듦
  2. 함수 본문을 constructor 의 내용으로 채움
  3. User.prototype 에 메서드 추가

아니 그럼 그냥 생성자 함수랑 뭐가 다르지❓❓

일단, 클래스에는 [[IsClassConstructor]]: true 라는 특수 내부 프로퍼티가 존재합니다.
이걸 활용하여 new 연산자와 함께 호출하지 않으면 에러가 나도록 합니다.

또한 클래스는 항상 엄격 모드로 실행됩니다.
생성자 안 코드 전체는 자동으로 엄격 모드가 적용됩니다.


getter 와 setter

클래스에서도 접근자를 사용할 수 있습니다.
getter 와 setter 는 User.prototype 에 정의됩니다.

class User {

  constructor(name) {
    // setter를 활성화
    this.name = name;
  }

  get name() {
    return this._name;
  }

  set name(value) {
    if (value.length < 4) {
      alert("이름이 너무 짧습니다.");
      return;
    }
    this._name = value;
  }

}

let user = new User("보라");
alert(user.name); // 보라

user = new User(""); // 이름이 너무 짧습니다.

클래스 필드 선언

그냥 필드에 프로퍼티명=값 이런 방식으로 선언 가능합니다.

class User {
	name = "JJ";
	sayHello = () => { alert(this + "hello"); }
}

이 프로퍼티는 생성한 객체(인스턴스)에 바로 바인딩되어, prototype 에는 존재하지 않습니다.
클래스 필드 선언으로 만든 프로퍼티는 자식 클래스의 인스턴스에 상속됩니다.

그래서 클래스 필드에 만든 화살표 함수등이 인스턴스 생성마다 계속 만들어져서
메모리를 많이 차지하게 되므로, 특수한 경우를 제외하면 쓰지 않는 편이 좋을 것 같습니다.


트랜스파일러를 사용하면 클래스 필드에 선언한 내용은 생성자 내부에 선언한 것으로 변환된다고 합니다.
클래스 필드의 초기화 순서를 생각할 때 이를 참고하면 좋을 듯 합니다.


🧩 클래스 상속

다른 클래스를 상속받는 것도 가능합니다.
extends 키워드로 상속하며, 이는 해당 클래스의 .prototype[[Prototype]] 을 지정한다고 볼 수 있습니다.

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  run(speed) {
    this.speed = speed;
    alert(`${this.name} 은/는 속도 ${this.speed}로 달립니다.`);
  }
  stop() {
    this.speed = 0;
    alert(`${this.name} 이/가 멈췄습니다.`);
  }
}

let animal = new Animal("동물");

class Rabbit extends Animal {
  hide() {
    // Animal 의 멤버 변수 name 을 사용
    alert(`${this.name} 이/가 숨었습니다!`);
  }
}

let rabbit = new Rabbit("흰 토끼");

rabbit.run(5); // 흰 토끼 은/는 속도 5로 달립니다.
rabbit.hide(); // 흰 토끼 이/가 숨었습니다!

즉, 위 코드에선 Rabbit 클래스가 Animal 클래스를 상속하므로
Rabbit.prototype.[[Prototype]]Animal.prototype 이 됩니다.
rabbit 은 Rabbit.prototype 에서 메서드를 찾고, 거기 없으면 Rabbit.prototype 의 프로토타입인 Animal.prototype 을 탐색합니다.

📌 extends 는 두 가지 동작을 합니다.

1. RabbitAnimal 상속하게 함 ( Rabbit[[Prototype]]Animal 이 됨 )
➡️ Rabbit 내부에서 Animal 의 필드(멤버 변수) 사용 가능

2. Rabbit.prototypeAnimal.prototype 을 상속하게 함
➡️ 일반 객체에서 일어나는 상속, 부모 메서드 사용


메서드 오버라이딩

RabbitAnimal 을 상속하지만,
Animal 에 정의된 메서드를 Rabbit 에서 자체적으로 다시 정의하면
rabbit 객체는 Rabbit 에 정의된 메서드를 우선적으로 사용합니다.

super ❓

super 라는 키워드는 상속하는 클래스를 의미합니다.
하위 클래스의 메서드내에서 상위 클래스의 인스턴스처럼 다뤄지죠.
super() 는 부모 생성자를 호출하는데, 이는 자식 생성자 내부에서만 사용 가능합니다.
super 로 상위 클래스의 메서드도 호출 가능합니다. (ex. super.stop())


⚡ 생성자 오버라이딩

자식 클래스의 생성자가 없다면 기본적으로 부모 클래스의 생성자를 호출합니다.
자식에 빈 생성자를 만들고 그 안에서 super(...) 를 호출하는 것이죠.
자식 클래스에 자체 생성자가 없다면 부모걸 호출 ( super() ) 하면 되니까 문제가 없지만...

자식에게도 자체 생성자가 있다면❓
자식 생성자 내 최상단에 super(...) 를 호출하지 않으면 에러가 납니다.
super(...)this 사용 전에 무조건 호춯해야합니다.

왜 this 사용 전에 부모의 생성자를 호출해야 할까❓

일반 클래스와 상속 클래스(자식)의 동작 방식은 다릅니다.
일반 클래스가 new 연산자로 실행되면 빈 객체를 만들고 이를 this 로 지정하는 과정을 거칩니다.

그러나, 상속 클래스는 이 일련의 과정을 부모의 생성자한테 맡겨버립니다.
반드시 부모의 생성자를 호출해야 this 가 될 객체가 만들어지는 것이죠.
따라서, 적절하게 super(...) 를 넣어줘야 에러가 나지 않습니다.


super 키워드와 HomeObject

super 를 쓰면 현재 객체의 프로토타입에서 메서드를 탐색합니다.
즉 클래스로 따지면 Rabbit 클래스 내에서 superAnimal.prototype 에 접근하는 것이죠.
(그래서 필드 변수를 가져오려면 getter/setter 써야 undefined 안 나오고 결과가 잘 나옵니다. 필드 변수는 prototype 에 없으니까요)

[[HomeObject]] 는 함수 전용 특수 내부 프로퍼티로, 클래스나 객체의 메서드인 함수는 이 프로퍼티에 해당 객체를 저장합니다.
[[HomeObject]] 는 함수가 자신이 정의된 클래스, 객체를 기억하는 것이죠.

super 는 이 프로퍼티를 이용해 부모 프로토타입과 메서드를 찾습니다.
[[HomeObject]] 에 기록된 객체를 찾아, 그의 프로토타입을 탐색합니다.

[[HomeObject]] 는 메서드가 객체를 기억하게 합니다.
한 번 바인딩되면 변경되지 않죠.(직접 접근할 방법이 없기 때문에...)
이는 함수와 객체의 연관성을 높이므로 함수의 자유도를 파괴하는데요,
함수가 객체에 얽매이지 않고 자유롭기 위해 [[HomeObject]]super 내에서만 유효합니다.
그래서 super 를 사용하지 않으면 함수의 자유성이 보장되죠.

❗ 그런데, 함수를 복사하면 이 [[HomeObject]] 값도 그대로 복사되어 전달됩니다.
그래서 super 사용할 때 프로토타입이 현 객체가 아니라 복사된 메서드의 [[HomeObject]] 를 기준으로 찾아지는 결과가 나오기도 합니다...

[[HomeObject]] 는 클래스와 일반 객체의 메서드에서 정의됩니다.
객체 메서드에서 [[HomeObject]] 가 제대로 정의되게 하려면 함수 프로퍼티 문법이 아니라 객체 메서드 문법을 사용해야 합니다.

// 함수 프로퍼티 문법
// 이러면 HomeObject 가 설정되지 않아서 상속이 제대로 동작하지 않음
eat: function() {}

// 객체 메서드 문법
eat() {}

화살표 함수

📌 화살표 함수는 자체적으로 superthis 를 갖지 않습니다.
(그래서 화살표 함수에서 superthis 를 사용하면 외부 스코프를 참조하죠.)

따라서 내부 함수가 컨텍스트를 이어받기 원한다면 화살표 함수를 사용합니다.


🔔 정적 프로퍼티와 정적 메서드

보통 자바 등에서 정적인 무언가는 클래스 자체와 연결되어 인스턴스를 호출하지 않고도 클래스 자체만으로 호출하여 사용가능한 대상을 의미합니다.
자바스크립트에서는 prototype 에 메서드를 설정했지만, 클래스 자체에 메서드를 설정하는 것도 가능합니니다.
이런걸 정적 메서드라고 합니다.

정적 메서드는 메서드 선언 문법 앞에 static 키워드를 붙여 만듭니다.
특정 객체가 아닌 클래스에 속한 함수를 구현할 때 사용하는데, 객체보다 클래스의 관점에서 사용할 때나(두 인스턴스 비교...) 데이터베이스 CRUD 수행 기능 관련 메서드에도 사용합니다.

class User {
  static staticMethod() {
    alert(this === User);
  }
}

User.staticMethod(); // true

이렇게 클래스 자체에 함수를 정의하여 사용합니다.
이는 클래스에 메서드를 프로퍼티처럼 할당하는것과 동일합니다.

이런 정적 메서드는 인스턴스가 참조할 수 없습니다.
인스턴스는 User.prototype 을 참고하기 때문이죠... User 가 아니라❗

정적 메서드의 this 는 클래스( User )이므로 정적 메서드 내의 this()new User().
즉, 새로운 인스턴스를 생성합니다.


정적 프로퍼티는 최근에 추가된 문법인데요,
일반 클래스 프로퍼티(클래스 필드 선언) 와 유사한데 앞에 static 이 붙습니다.
역시나 인스턴스에는 없고 클래스 자체에 있는 프로퍼티가 됩니다.

class Article {
  static publisher = "Ilya Kantor";
}

alert( Article.publisher ); // Ilya Kantor

상속

정적 메서드, 정적 프로퍼티도 상속됩니다.
Animal 을 상속한 Rabbit 클래스가
Animal 의 정적 메서드와 프로퍼티를 사용 가능합니다.

왜냐하면 Rabbit[[Prototype]]Animal 이기 때문입니다.
extendsRabbit[[Prototype]]Animal 로 하고
Rabbit.prototype[[Prototype]]Animal.prototype 으로 합니다.
그리고 Rabbit 의 인스턴스 rabbit 의 [[Prototype]]Rabbit.prototype 이 됩니다.

따라서 정적 프로퍼티는 정적 메서드에서 사용가능합니다.


🦎 private, protected

객체지향은 내부와 외부를 분리하는 것이 중요합니다.
내부의 동작을 외부에선 신경쓰지 않게 해야 하죠.

객체지향에서 프로퍼티와 메서드는 두 그룹으로 분류합니다.

  1. 내부 인터페이스: 클래스 내 메서드간에는 접근 가능하지만 클래스 외부에서는 접근 불가능
  2. 외부 인터페이스: 클래스 외부(인스턴스나... static한 것들)에서도 접근 가능

자바스크립트에는 두 가지 타입의 프로퍼티, 메서드가 존재합니다.

  1. public: 어디서든 접근 (외부 인터페이스는 이걸로 구현)
  2. private: 클래스 내부에서만 접근 (내부 인터페이스는 이걸로 지정❗)

아무것도 안 붙이면 기본적으로 public 이 되어, 외부에서 접근 가능합니다.
자바스크립트는 protected (자신 외에 자손의 접근을 허용) 를 지원하지 않지만 모방해서 사용가능합니다.


protected 프로퍼티

protected 인 프로퍼티 앞엔 _ 이 붙습니다.
외부 접근이 불가능한 프로퍼티나 메서드임을 표시하는 것이죠.
이제 gettersetter 를 이용하여 접근하도록 하면 됩니다.


private 프로퍼티

private 는 프로퍼티 앞에 # 을 붙입니다.
이러면 클래스 내에서만 접근 가능하며 this 로도 접근할 수 없게 됩니다.
this["prop"] 도 안 됩니다.


🖍️ 내장 클래스 확장하기

ArrayMap 같은 내장 클래스를 상속할 수도 있습니다.

class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

let filteredArr = arr.filter(item => item >= 10);
alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false

PowerArray 클래스로 만든 객체는 배열과 동일하지만, isEmpty 라는 메서드가 추가됩니다.
그런데, PowerArray 인스턴스에 filter 메서드를 적용하여 얻은 반환값 또한 PowerArray 입니다.

이는 filter 메서드가 반환할 객체의 타입을 객체의 constructor 프로퍼티를 사용하여 결정하기 때문입니다. 이 때 constructor 에는 객체 자신을 생성한 주체(PowerArray)가 지정되죠.
그걸 기반으로 새로운 배열을 만들기 때문에 Array 가 아닌 PowerArray 가 됩니다.

이를 변경하기 위해서는 내장메서드에 getter 를 추가합니다.
이러면 생성자에 get [Symbol.species] 의 반환값을 사용하므로 변경할 수 있습니다.

static get [Symbol.species]() {
    return Array;
  }

내장 객체와 정적 메서드 상속

일반적으로 한 클래스가 다른 클래스를 상속받으면 정적 메서드와 일반 메서드 모두를 상속받습니다.
이는 extends 가 클래스끼리 상속, prototype 끼리 상속 체이닝을 해주므로 가능합니다.

그런데 내장 클래스의 경우, prototype 에는 상속 링크가 걸리지만 클래스 자체에는 상속 링크가 걸리지 않습니다.
따라서 내장 클래스는 정적 메서드를 상속받지 못합니다.


객체의 클래스 확인

클래스를 통해 만든 객체를 인스턴스라고 합니다.
instanceof 연산자를 사용하면 객체가 특정 클래스에 속하는지 아닌지 확인할 수 있습니다.
이 연산자는 프로토타입 체인을 올라가면서 상속이나 인스턴스 여부를 확인합니다.
객체의 __proto__ 체인을 올라가며 Class.prototype 과 일치하는 게 있는지 찾습니다.

obj instanceof Class

obj 가 Class 의 인스턴스이거나, Class 를 상속받은 클래스의 인스턴스라면 true 를 반환합니다.

let arr = [1, 2, 3];
alert( arr instanceof Array ); // true
alert( arr instanceof Object ); // true

믹스인과 다중상속

자바스크립트는 단일상속만을 허용하지만 믹스인을 사용하면 다중상속을 구현할 수 있습니다.
믹스인은 클래스에 구현된 메서드를 담고 있는 클래스라고 정의하며, 이 믹스인을 포함하여 행동을 더해주는 용도로 사용합니다.

  • 믹스인을 프로토타입에 넣기
// 믹스인
let sayHiMixin = {
  sayHi() {
    alert(`Hello ${this.name}`);
  },
  sayBye() {
    alert(`Bye ${this.name}`);
  }
};

// 사용법:
class User {
  constructor(name) {
    this.name = name;
  }
}

// User.prototype 에 sayHiMixin 의 메서드 복사
Object.assign(User.prototype, sayHiMixin);

// User 가 sayHiMixin 의 메서드 사용 가능
new User("Dude").sayHi(); // Hello Dude!

위처럼 메서드를 포함하고 있는 믹스인을 프로토타입에 복사해서 넣어주는 방식으로 구현할 수 있습니다.

profile
기록용 블로그 + 오류가 있을 수 있습니다🔥

0개의 댓글