목차
JavaScript 클래스(class)
class는 객체 생성자, 메서드, 상속 관계를 읽기 쉽게 선언하는 문법이다. 인스턴스는 여전히 프로토타입으로 메서드를 찾는다. 모든 메서드가 각 객체에 복사되는 것은 아니다. 예를 들어 아래 hello()는 Person.prototype에 놓이고 두 인스턴스가 공유한다.
class Person {
constructor(name) {
this.name = name;
}
hello() {
return `Hello, ${this.name}`;
}
}
const kim = new Person("Kim");
const lee = new Person("Lee");
console.log(kim.hello()); // Hello, Kim
console.log(kim.hello === lee.hello); // true
new 없이 Person("Kim")처럼 클래스를 호출하면 TypeError가 난다. constructor는 인스턴스를 만들 때 실행되며 클래스 안에 하나만 선언한다. 생성자를 생략하면 기본 생성자가 제공된다. 원문의 class Foo {}const foo... 예제는 두 문장을 구분해서 읽어야 하고, new Foo() 줄에 적힌 오류 설명은 실제로 Foo() 호출에 해당한다.
클래스 필드와 비공개 필드
오래된 ES6 소개 글에는 클래스 본문에 필드를 선언하면 SyntaxError라고 설명한 자료가 있다. 현재 JavaScript에서는 공개 클래스 필드와 # 비공개 필드를 사용할 수 있다. 단, 특정 오래된 런타임을 지원해야 한다면 그 런타임의 문법 지원을 확인한다. 특정 브라우저 버전 번호를 영구적인 사용 기준으로 두지 않는다.
class Counter {
count = 0;
#step = 1;
static label = "counter";
increment() {
this.count += this.#step;
return this.count;
}
}
const counter = new Counter();
console.log(counter.increment()); // 1
console.log(Counter.label); // counter
console.log(counter.label); // undefined
// counter.#step; // 문법 오류: 클래스 밖에서 접근 불가
count는 인스턴스마다 있고, label은 클래스 자체에 있다. #step은 클래스 외부에서 접근할 수 없는 비공개 요소다. _step처럼 밑줄로 시작하는 이름은 관례일 뿐 접근 제한을 제공하지 않는다. 클래스 필드의 객체 값이 변경 가능하면 const처럼 객체 전체를 고정하는 효과는 없다.
getter와 setter
class Basket {
#items;
constructor(items = []) { this.#items = [...items]; }
get firstItem() { return this.#items[0] ?? null; }
set firstItem(value) {
if (typeof value !== "string" || !value) {
throw new TypeError("비어 있지 않은 문자열이 필요합니다");
}
if (this.#items.length === 0) this.#items.push(value);
else this.#items[0] = value;
}
}
const basket = new Basket(["사과"]);
console.log(basket.firstItem); // 사과
basket.firstItem = "배";
console.log(basket.firstItem); // 배
getter와 setter는 호출할 때 괄호를 붙이지 않고 속성처럼 사용한다. Basket은 배열을 복사해 외부가 원본 목록을 바꾸더라도 내부 목록이 달라지지 않게 했다. setter에서 잘못된 입력을 거부하지만 네트워크 입력을 받는 시스템이라면 경계에서 추가 검증이 필요하다. 계산이 무겁거나 실패가 잦은 작업을 getter에 숨기면 단순한 속성 조회처럼 보이므로 일반 메서드가 더 분명할 수 있다.
상속과 super
class Employee extends Person {
constructor(name, team) {
super(name);
this.team = team;
}
hello() {
return `${super.hello()} (${this.team})`;
}
}
console.log(new Employee("Kim", "개발").hello());
// Hello, Kim (개발)
파생 클래스 생성자에서 this를 쓰기 전에 super(...)로 부모 생성자를 호출한다. super.hello()는 부모 메서드를 호출한다. extends는 재사용에 편리하지만 깊은 상속 계층은 상태와 메서드의 출처를 찾기 어렵게 한다. 단순한 기능 조합이라면 필요한 객체를 필드로 전달하는 구성이 더 읽기 쉬울 수 있다.