5.3 ES6 클래스
ES6에서 도입된 class 문법은 프로토타입 기반 상속을 더 명확하고 읽기 쉬운 방식으로 표현합니다. 내부적으로는 여전히 프로토타입 기반으로 동작합니다.
클래스 선언과 표현식
// 클래스 선언
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
// 클래스 표현식 (이름 있는)
const Animal = class AnimalClass {
constructor(name) {
this.name = name;
}
// AnimalClass는 클래스 내부에서만 참조 가능
};
// 클래스 표현식 (이름 없는)
const Vehicle = class {
constructor(make, model) {
this.make = make;
this.model = model;
}
};
// 클래스는 함수입니다
console.log(typeof Person); // "function"
console.log(typeof Animal); // "function"
// 클래스 선언은 호이스팅되지만 TDZ 적용 (함수 선언과 차이)
// const p = new Person2("Alice"); // ReferenceError (TDZ)
// class Person2 {}
const alice = new Person("Alice", 30);
const car = new Vehicle("현대", "아반떼");
console.log(alice.name); // "Alice"
console.log(car.make); // "현대"
constructor: 생성자 메서드
constructor는 new로 인스턴스를 생성할 때 자동으로 호출됩니다. 클래스 당 하나만 존재할 수 있습니다.
class Product {
constructor(name, price, category = "일반") {
// 인스턴스 프로퍼티 초기화
this.name = name;
this.price = price;
this.category = category;
this.createdAt = new Date();
this.id = Product._nextId++;
// 유효성 검사
if (price < 0) throw new RangeError("가격은 0 이상이어야 합니다");
if (!name?.trim()) throw new TypeError("상품명은 필수입니다");
}
}
Product._nextId = 1; // 정적 카운터
const laptop = new Product("노트북", 1200000, "전자제품");
const phone = new Product("스마트폰", 800000, "전자제품");
console.log(laptop.id); // 1
console.log(phone.id); // 2
console.log(laptop.createdAt instanceof Date); // true
// constructor를 생략하면 기본 constructor 사용
class SimpleClass {
// constructor() {} 가 암시적으로 있는 것과 같음
}
const s = new SimpleClass(); // 정상 작동
인스턴스 메서드
클래스 바디에 정의된 메서드는 prototype에 추가됩니다.
class BankAccount {
constructor(owner, balance = 0) {
this.owner = owner;
this._balance = balance;
}
// 인스턴스 메서드 — BankAccount.prototype에 추가됨
deposit(amount) {
if (amount <= 0) throw new Error("입금액은 0보다 커야 합니다");
this._balance += amount;
return this; // 메서드 체이닝 지원
}
withdraw(amount) {
if (amount > this._balance) throw new Error("잔액 부족");
this._balance -= amount;
return this;
}
getBalance() {
return this._balance;
}
toString() {
return `BankAccount[${this.owner}: ${this._balance.toLocaleString()}원]`;
}
}
// 메서드는 prototype에 추가됨 (모든 인스턴스가 공유)
console.log(Object.getOwnPropertyNames(BankAccount.prototype));
// ["constructor", "deposit", "withdraw", "getBalance", "toString"]
const account = new BankAccount("홍길동", 10000);
account.deposit(5000).deposit(3000).withdraw(2000); // 체이닝
console.log(account.getBalance()); // 16000
console.log(`${account}`); // "BankAccount[홍길동: 16,000원]"
정적 메서드 (static)
static 키워드로 정의된 메서드는 인스턴스 없이 클래스 자체에서 호출합니다.
class MathUtils {
// 정적 메서드: MathUtils.clamp(...)로 호출
static clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
static lerp(a, b, t) {
return a + (b - a) * t;
}
static randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
static average(...numbers) {
return numbers.reduce((a, b) => a + b, 0) / numbers.length;
}
}
console.log(MathUtils.clamp(15, 0, 10)); // 10
console.log(MathUtils.lerp(0, 100, 0.5)); // 50
console.log(MathUtils.average(1, 2, 3, 4, 5)); // 3
// 팩토리 메서드 패턴
class Color {
constructor(r, g, b) {
this.r = r; this.g = g; this.b = b;
}
// 정적 팩토리 메서드들
static fromHex(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return new Color(r, g, b);
}
static fromHSL(h, s, l) {
// HSL to RGB 변환 (간략화)
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = l - c / 2;
let r, g, b;
if (h < 60) [r, g, b] = [c, x, 0];
else if (h < 120) [r, g, b] = [x, c, 0];
else if (h < 180) [r, g, b] = [0, c, x];
else if (h < 240) [r, g, b] = [0, x, c];
else if (h < 300) [r, g, b] = [x, 0, c];
else [r, g, b] = [c, 0, x];
return new Color(
Math.round((r + m) * 255),
Math.round((g + m) * 255),
Math.round((b + m) * 255)
);
}
toHex() {
return `#${[this.r, this.g, this.b].map(v => v.toString(16).padStart(2, "0")).join("")}`;
}
toString() {
return `rgb(${this.r}, ${this.g}, ${this.b})`;
}
}
const red = Color.fromHex("#ff0000");
const blue = Color.fromHSL(240, 1, 0.5);
console.log(`${red}`); // "rgb(255, 0, 0)"
console.log(blue.toHex()); // "#0000ff"
getter / setter in class
class Temperature {
#celsius;
constructor(celsius) {
this.celsius = celsius; // setter를 통해 초기화
}
get celsius() {
return this.#celsius;
}
set celsius(value) {
if (typeof value !== "number") {
throw new TypeError("온도는 숫자여야 합니다");
}
if (value < -273.15) {
throw new RangeError("절대 영도보다 낮을 수 없습니다");
}
this.#celsius = value;
}
get fahrenheit() {
return this.#celsius * 9 / 5 + 32;
}
set fahrenheit(value) {
this.celsius = (value - 32) * 5 / 9;
}
get kelvin() {
return this.#celsius + 273.15;
}
get description() {
if (this.#celsius < 0) return "영하";
if (this.#celsius < 10) return "매우 춥다";
if (this.#celsius < 20) return "쌀쌀하다";
if (this.#celsius < 30) return "따뜻하다";
return "덥다";
}
}
const temp = new Temperature(25);
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
console.log(temp.kelvin); // 298.15
console.log(temp.description); // "따뜻하다"
temp.fahrenheit = 32;
console.log(temp.celsius); // 0
console.log(temp.description); // "영하"와 "매우 춥다" 경계: "영하"
클래스 필드 (Class Fields)
ES2022에서 표준화된 클래스 필드를 사용해 인스턴스를 초기화합니다.
class Counter {
// 퍼블릭 클래스 필드 (인스턴스 초기화)
count = 0;
label = "카운터";
// 클래스 필드 화살표 함수 (this 바인딩 고정)
increment = () => {
this.count++;
return this;
};
decrement = () => {
this.count--;
return this;
};
// 정적 클래스 필드
static defaultStep = 1;
static instances = 0;
constructor(initialValue = 0, label = "카운터") {
this.count = initialValue;
this.label = label;
Counter.instances++;
}
toString() {
return `${this.label}: ${this.count}`;
}
}
const c1 = new Counter(10, "점수");
const c2 = new Counter(0, "실수");
c1.increment().increment().increment();
c2.decrement();
console.log(`${c1}`); // "점수: 13"
console.log(`${c2}`); // "실수: -1"
console.log(Counter.instances); // 2
// 클래스 필드 화살표 함수는 구조 분해 후에도 this 유지
const { increment } = c1;
increment();
console.log(c1.count); // 14 (this가 c1을 유지)
typeof class === 'function'
클래스는 함수의 문법적 설탕임을 확인합니다.
class Example {}
console.log(typeof Example); // "function"
console.log(Example.prototype.constructor === Example); // true
// 클래스와 일반 함수의 차이
function OldStyle(x) { this.x = x; }
class NewStyle { constructor(x) { this.x = x; } }
// 1. new 없이 호출 불가 (클래스)
try {
NewStyle(1); // TypeError: Class constructor cannot be invoked without 'new'
} catch (e) {
console.log(e.message);
}
// 2. 클래스는 호이스팅 시 TDZ 적용
// 3. 클래스 내부는 항상 엄격 모드
// 4. 클래스 메서드는 열거 불가 (for...in에서 제외)
console.log(
Object.getOwnPropertyDescriptor(NewStyle.prototype, "constructor")?.enumerable
); // false (열거 불가)
실전 예제: 타입 안전한 컬렉션 클래스
class TypedCollection {
#items = [];
#type;
constructor(type) {
if (typeof type !== "function") {
throw new TypeError("타입 생성자 함수가 필요합니다");
}
this.#type = type;
}
add(item) {
if (!(item instanceof this.#type)) {
throw new TypeError(`${this.#type.name} 타입만 추가할 수 있습니다`);
}
this.#items.push(item);
return this;
}
get(index) {
return this.#items[index] ?? null;
}
remove(item) {
const idx = this.#items.indexOf(item);
if (idx !== -1) this.#items.splice(idx, 1);
return this;
}
get size() { return this.#items.length; }
*[Symbol.iterator]() {
yield* this.#items;
}
static of(type, ...items) {
const collection = new TypedCollection(type);
items.forEach(item => collection.add(item));
return collection;
}
}
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
const users = TypedCollection.of(
User,
new User("Alice", "alice@example.com"),
new User("Bob", "bob@example.com"),
);
users.add(new User("Carol", "carol@example.com"));
try {
users.add({ name: "Invalid" }); // TypeError!
} catch (e) {
console.log(e.message); // "User 타입만 추가할 수 있습니다"
}
console.log(users.size); // 3
for (const user of users) {
console.log(user.name);
}
// Alice, Bob, Carol
고수 팁
팁 1: 정적 초기화 블록 (ES2022)
class DatabaseConfig {
static host;
static port;
static connectionString;
// 복잡한 정적 초기화 로직
static {
const env = typeof process !== "undefined" ? process.env : {};
DatabaseConfig.host = env.DB_HOST ?? "localhost";
DatabaseConfig.port = parseInt(env.DB_PORT ?? "5432");
DatabaseConfig.connectionString =
`postgresql://${DatabaseConfig.host}:${DatabaseConfig.port}/mydb`;
}
}
console.log(DatabaseConfig.connectionString);
// "postgresql://localhost:5432/mydb"
팁 2: 클래스 데코레이터 패턴 (TypeScript/Babel 스타일)
function singleton(Class) {
let instance = null;
return class extends Class {
constructor(...args) {
if (instance) return instance;
super(...args);
instance = this;
}
static getInstance(...args) {
if (!instance) new this(...args);
return instance;
}
};
}
const SingletonLogger = singleton(class Logger {
constructor(name) {
this.name = name;
this.logs = [];
}
log(msg) { this.logs.push(msg); }
});
const l1 = new SingletonLogger("앱");
const l2 = new SingletonLogger("다른이름");
l1.log("첫 번째");
console.log(l2.logs); // ["첫 번째"] — 같은 인스턴스!
console.log(l1 === l2); // true
팁 3: toString, valueOf, Symbol.toPrimitive 오버라이드
class Money {
constructor(amount, currency = "KRW") {
this.amount = amount;
this.currency = currency;
}
toString() { return `${this.amount.toLocaleString()}${this.currency}`; }
valueOf() { return this.amount; } // 숫자 연산에서 사용
[Symbol.toPrimitive](hint) {
if (hint === "number") return this.amount;
if (hint === "string") return this.toString();
return this.amount; // default
}
}
const price = new Money(50000);
const tax = new Money(5000);
console.log(`가격: ${price}`); // "가격: 50,000KRW"
console.log(price + tax); // 55000 (valueOf 사용)
console.log(price > 30000); // true
console.log(`합계: ${price + tax}`); // "합계: 55000"