JavaScript Classes | ES6 Class Syntax Explained

JavaScript Classes | ES6 Class Syntax Explained

이 글의 핵심

From constructor functions to modern class syntax: encapsulation, inheritance, static factories, private fields, and game/e‑commerce examples you can reuse.

Introduction

What is a class?

A class is a template for creating objects. ES6 (ES2015) introduced class syntax.

Before classes (ES5):

// Constructor function
function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.greet = function() {
    console.log(`안녕하세요, ${this.name}입니다.`);
};

const person = new Person("홍길동", 25);
person.greet();  // 안녕하세요, 홍길동입니다.

With classes (ES6+):

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        console.log(`안녕하세요, ${this.name}입니다.`);
    }
}

const person = new Person("홍길동", 25);
person.greet();  // 안녕하세요, 홍길동입니다.

1. Class basics

Defining a class

class Rectangle {
    // Constructor: runs when an instance is created
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }
    
    // Methods
    getArea() {
        return this.width * this.height;
    }
    
    getPerimeter() {
        return 2 * (this.width + this.height);
    }
    
    // Calling other methods
    describe() {
        return `넓이: ${this.getArea()}, 둘레: ${this.getPerimeter()}`;
    }
}

// Create an instance
const rect = new Rectangle(10, 5);
console.log(rect.getArea());       // 50
console.log(rect.getPerimeter());  // 30
console.log(rect.describe());      // 넓이: 50, 둘레: 30

Class expressions

// Anonymous class
const Person = class {
    constructor(name) {
        this.name = name;
    }
    
    greet() {
        console.log(`Hello, ${this.name}!`);
    }
};

// Named class expression
const Person = class PersonClass {
    constructor(name) {
        this.name = name;
    }
};

const person = new Person("홍길동");
person.greet();

2. Getters and setters

Defining getters/setters

class Circle {
    constructor(radius) {
        this._radius = radius;  // “private by convention” (_prefix)
    }
    
    // Getter: read like a property
    get radius() {
        return this._radius;
    }
    
    // Setter: assign like a property
    set radius(value) {
        if (value < 0) {
            throw new Error("반지름은 양수여야 합니다");
        }
        this._radius = value;
    }
    
    // Computed properties
    get area() {
        return Math.PI * this._radius ** 2;
    }
    
    get diameter() {
        return this._radius * 2;
    }
    
    set diameter(value) {
        this._radius = value / 2;
    }
}

// Usage
const circle = new Circle(5);
console.log(circle.radius);  // 5 (getter)
console.log(circle.area);    // 78.53981633974483

circle.radius = 10;  // setter
console.log(circle.area);  // 314.1592653589793

circle.diameter = 20;  // diameter setter
console.log(circle.radius);  // 10

// circle.radius = -5;  // Error: 반지름은 양수여야 합니다

3. Static methods and properties

The static keyword

class MathUtils {
    // Static property
    static PI = 3.14159;
    
    // Static method: callable without an instance
    static add(a, b) {
        return a + b;
    }
    
    static max(...numbers) {
        return Math.max(...numbers);
    }
    
    // Factory-style helper
    static createCircle(radius) {
        return new Circle(radius);
    }
}

// Call static members on the class
console.log(MathUtils.add(10, 20));  // 30
console.log(MathUtils.max(1, 5, 3));  // 5
console.log(MathUtils.PI);  // 3.14159

// Not available on instances
// const util = new MathUtils();
// util.add(1, 2);  // TypeError

Practical example: factory pattern

class User {
    constructor(name, email, role) {
        this.name = name;
        this.email = email;
        this.role = role;
    }
    
    // Static factories
    static createAdmin(name, email) {
        return new User(name, email, "admin");
    }
    
    static createGuest(name) {
        return new User(name, `${name}@guest.com`, "guest");
    }
    
    hasPermission(permission) {
        const permissions = {
            admin: ["read", "write", "delete"],
            user: ["read", "write"],
            guest: ["read"]
        };
        return permissions[this.role].includes(permission);
    }
}

// Usage
const admin = User.createAdmin("관리자", "[email protected]");
const guest = User.createGuest("손님");

console.log(admin.hasPermission("delete"));  // true
console.log(guest.hasPermission("write"));   // false

4. Inheritance

extends

// Base class
class Animal {
    constructor(name) {
        this.name = name;
    }
    
    speak() {
        console.log(`${this.name}이(가) 소리를 냅니다.`);
    }
    
    move() {
        console.log(`${this.name}이(가) 움직입니다.`);
    }
}

// Derived classes
class Dog extends Animal {
    constructor(name, breed) {
        super(name);  // Call parent constructor (required!)
        this.breed = breed;
    }
    
    // Override
    speak() {
        console.log(`${this.name}: 멍멍!`);
    }
    
    fetch() {
        console.log(`${this.name}이(가) 공을 가져옵니다.`);
    }
}

class Cat extends Animal {
    speak() {
        console.log(`${this.name}: 야옹~`);
    }
}

// Usage
const dog = new Dog("바둑이", "진돗개");
dog.speak();  // 바둑이: 멍멍!
dog.move();   // 바둑이이(가) 움직입니다. (inherited)
dog.fetch();  // 바둑이이(가) 공을 가져옵니다.

const cat = new Cat("나비");
cat.speak();  // 나비: 야옹~

// instanceof
console.log(dog instanceof Dog);     // true
console.log(dog instanceof Animal);  // true
console.log(dog instanceof Cat);     // false

The super keyword

class Employee {
    constructor(name, salary) {
        this.name = name;
        this.salary = salary;
    }
    
    getInfo() {
        return `${this.name} - ${this.salary.toLocaleString()}원`;
    }
    
    work() {
        return `${this.name}이(가) 일합니다.`;
    }
}

class Manager extends Employee {
    constructor(name, salary, teamSize) {
        super(name, salary);
        this.teamSize = teamSize;
    }
    
    // Extend parent behavior
    getInfo() {
        const baseInfo = super.getInfo();
        return `${baseInfo} (팀원: ${this.teamSize}명)`;
    }
    
    manageTeam() {
        return `${this.name}이(가) ${this.teamSize}명을 관리합니다.`;
    }
}

const manager = new Manager("김팀장", 5000000, 5);
console.log(manager.getInfo());    // 김팀장 - 5,000,000원 (팀원: 5명)
console.log(manager.work());       // 김팀장이(가) 일합니다. (inherited)
console.log(manager.manageTeam()); // 김팀장이(가) 5명을 관리합니다.

5. Private fields (ES2022+)

# prefix

class BankAccount {
    // Private fields
    #balance;
    
    constructor(owner, balance) {
        this.owner = owner;
        this.#balance = balance;
    }
    
    // Public methods
    deposit(amount) {
        if (amount > 0) {
            this.#balance += amount;
            return true;
        }
        return false;
    }
    
    withdraw(amount) {
        if (amount > 0 && amount <= this.#balance) {
            this.#balance -= amount;
            return true;
        }
        return false;
    }
    
    getBalance() {
        return this.#balance;
    }
    
    // Private method
    #log(message) {
        console.log(`[${this.owner}] ${message}`);
    }
}

const account = new BankAccount("홍길동", 10000);
console.log(account.owner);  // 홍길동
// console.log(account.#balance);  // SyntaxError: Private field

account.deposit(5000);
console.log(account.getBalance());  // 15000

6. Practical examples

Example 1: game characters

class Character {
    constructor(name, hp, attack) {
        this.name = name;
        this.hp = hp;
        this.maxHp = hp;
        this.attack = attack;
    }
    
    takeDamage(damage) {
        this.hp = Math.max(0, this.hp - damage);
        console.log(`${this.name} HP: ${this.hp}/${this.maxHp}`);
        return this.hp;
    }
    
    heal(amount) {
        this.hp = Math.min(this.maxHp, this.hp + amount);
        console.log(`${this.name} 회복! HP: ${this.hp}/${this.maxHp}`);
    }
    
    isAlive() {
        return this.hp > 0;
    }
    
    basicAttack(target) {
        console.log(`${this.name}의 공격!`);
        return target.takeDamage(this.attack);
    }
}

class Warrior extends Character {
    constructor(name, hp, attack, defense) {
        super(name, hp, attack);
        this.defense = defense;
    }
    
    takeDamage(damage) {
        const reduced = Math.max(0, damage - this.defense);
        console.log(`${this.name}이(가) 방어력 ${this.defense}로 데미지 감소!`);
        return super.takeDamage(reduced);
    }
    
    shieldBash(target) {
        console.log(`${this.name}의 방패 강타!`);
        return target.takeDamage(this.attack * 1.5);
    }
}

class Mage extends Character {
    constructor(name, hp, attack, mana) {
        super(name, hp, attack);
        this.mana = mana;
        this.maxMana = mana;
    }
    
    fireball(target) {
        if (this.mana < 20) {
            console.log("마나 부족!");
            return 0;
        }
        
        this.mana -= 20;
        console.log(`${this.name}의 파이어볼! (마나: ${this.mana}/${this.maxMana})`);
        return target.takeDamage(this.attack * 2);
    }
}

// Simple battle
const warrior = new Warrior("전사", 150, 20, 5);
const mage = new Mage("마법사", 100, 30, 50);

console.log("=== 전투 시작 ===");
mage.basicAttack(warrior);
warrior.shieldBash(mage);
mage.fireball(warrior);

console.log("\n=== 전투 결과 ===");
console.log(`${warrior.name}: ${warrior.isAlive() ? "생존" : "사망"}`);
console.log(`${mage.name}: ${mage.isAlive() ? "생존" : "사망"}`);

Example 2: shopping cart

class Product {
    constructor(id, name, price, stock) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.stock = stock;
    }
    
    isAvailable(quantity = 1) {
        return this.stock >= quantity;
    }
    
    decreaseStock(quantity) {
        if (!this.isAvailable(quantity)) {
            throw new Error("재고 부족");
        }
        this.stock -= quantity;
    }
    
    toString() {
        return `${this.name} - ${this.price.toLocaleString()}원 (재고: ${this.stock})`;
    }
}

class Cart {
    constructor() {
        this.items = [];
    }
    
    addItem(product, quantity = 1) {
        if (!product.isAvailable(quantity)) {
            console.log(`${product.name} 재고 부족`);
            return false;
        }
        
        const existing = this.items.find(item => item.product.id === product.id);
        
        if (existing) {
            existing.quantity += quantity;
        } else {
            this.items.push({ product, quantity });
        }
        
        console.log(`${product.name} ${quantity}개 추가`);
        return true;
    }
    
    removeItem(productId) {
        this.items = this.items.filter(item => item.product.id !== productId);
    }
    
    getTotal() {
        return this.items.reduce((total, item) => {
            return total + item.product.price * item.quantity;
        }, 0);
    }
    
    checkout() {
        this.items.forEach(item => {
            item.product.decreaseStock(item.quantity);
        });
        
        const total = this.getTotal();
        this.items = [];
        return total;
    }
    
    printCart() {
        console.log("=== 장바구니 ===");
        this.items.forEach(item => {
            console.log(`${item.product.name} × ${item.quantity} = ${(item.product.price * item.quantity).toLocaleString()}원`);
        });
        console.log(`총액: ${this.getTotal().toLocaleString()}원`);
    }
}

// Usage
const laptop = new Product(1, "노트북", 1200000, 5);
const mouse = new Product(2, "마우스", 30000, 10);

const cart = new Cart();
cart.addItem(laptop, 1);
cart.addItem(mouse, 2);
cart.printCart();

const total = cart.checkout();
console.log(`결제 완료: ${total.toLocaleString()}원`);

7. Common mistakes

Mistake 1: forgetting super()

// ❌ Wrong
class Child extends Parent {
    constructor(name, age) {
        // Missing super()!
        this.age = age;  // ReferenceError
    }
}

// ✅ Correct
class Child extends Parent {
    constructor(name, age) {
        super(name);
        this.age = age;
    }
}

Mistake 2: arrow functions as methods (sometimes)

// ⚠️ Arrow as field (different this semantics; often intentional for callbacks)
class Person {
    constructor(name) {
        this.name = name;
    }
    
    greet = () => {
        console.log(`Hello, ${this.name}!`);
    }
}

// ✅ Ordinary methods (prototype methods; usual choice)
class Person {
    constructor(name) {
        this.name = name;
    }
    
    greet() {
        console.log(`Hello, ${this.name}!`);
    }
}

Mistake 3: calling without new

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

// ❌ No new
// const person = Person("홍길동");  // TypeError

// ✅ Use new
const person = new Person("홍길동");

8. Practice problems

Problem 1: Stack class

class Stack {
    constructor() {
        this.items = [];
    }
    
    push(item) {
        this.items.push(item);
    }
    
    pop() {
        if (this.isEmpty()) {
            throw new Error("Stack is empty");
        }
        return this.items.pop();
    }
    
    peek() {
        if (this.isEmpty()) {
            return null;
        }
        return this.items[this.items.length - 1];
    }
    
    isEmpty() {
        return this.items.length === 0;
    }
    
    get size() {
        return this.items.length;
    }
}

// Tests
const stack = new Stack();
stack.push(1);
stack.push(2);
stack.push(3);
console.log(stack.peek());  // 3
console.log(stack.pop());   // 3
console.log(stack.size);    // 2

Problem 2: Timer class

class Timer {
    constructor() {
        this.startTime = null;
        this.elapsed = 0;
        this.running = false;
    }
    
    start() {
        if (this.running) return;
        
        this.running = true;
        this.startTime = Date.now() - this.elapsed;
    }
    
    stop() {
        if (!this.running) return;
        
        this.running = false;
        this.elapsed = Date.now() - this.startTime;
    }
    
    reset() {
        this.startTime = null;
        this.elapsed = 0;
        this.running = false;
    }
    
    getTime() {
        if (this.running) {
            return Date.now() - this.startTime;
        }
        return this.elapsed;
    }
    
    toString() {
        const ms = this.getTime();
        const seconds = Math.floor(ms / 1000);
        const minutes = Math.floor(seconds / 60);
        const remainingSeconds = seconds % 60;
        
        return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
    }
}

// Test
const timer = new Timer();
timer.start();
setTimeout(() => {
    console.log(timer.toString());  // 0:02
    timer.stop();
}, 2000);

Summary

Key takeaways

  1. Basics: class, constructor, methods
  2. Getters/setters: property-like access with validation
  3. static: class-level helpers and factories
  4. Inheritance: extends, super(), overrides
  5. Private fields (ES2022+): # for true encapsulation

Best practices

  1. ✅ Initialize in constructor
  2. ✅ Use getters/setters for controlled access
  3. ✅ Always call super() in derived constructors
  4. ✅ Use static for utilities and factories
  5. ✅ Use # fields to hide internal state

Next steps

  • JavaScript 모듈
  • JavaScript 에러 처리
  • JavaScript 디자인 패턴

  • Python 클래스 | 객체지향 프로그래밍(OOP) 완벽 정리
  • C++ 클래스와 객체
  • Java 클래스와 객체 | OOP, 상속, 인터페이스
  • Kotlin 클래스와 객체 | 클래스, 상속, 인터페이스
  • C++ struct vs class