[2026] JavaScript Design Patterns | Singleton, Factory, Observer Patterns

[2026] JavaScript Design Patterns | Singleton, Factory, Observer Patterns

이 글의 핵심

JavaScript design patterns: Singleton, Factory, Observer patterns. Learn principles, code implementation, and practical applications with real-world examples.

Introduction

What are Design Patterns?

Design Patterns are proven solution templates with names for recurring design problems. They help teams communicate efficiently with phrases like “let’s use a factory.” Why Learn Patterns:

  • Proven Solutions: Already validated approaches
  • Code Quality: Easier maintenance
  • Communication: Common language among developers
  • Reusability: Applicable to various situations

1. Singleton Pattern

Concept

Creates only one instance and makes it globally accessible.

Implementation

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, and perform branching with conditionals. Understand the role of each part as you examine the code.

class Database {
    constructor() {
        if (Database.instance) {
            return Database.instance;
        }
        
        this.connection = null;
        Database.instance = this;
    }
    
    connect() {
        if (!this.connection) {
            this.connection = "DB Connected";
            console.log(this.connection);
        }
        return this.connection;
    }
}
// Usage
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2);  // true (same instance)
db1.connect();  // DB Connected
db2.connect();  // (already connected)

Modern Approach (ES6 Module)

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, and perform branching with conditionals. Understand the role of each part as you examine the code.

// database.js
class Database {
    constructor() {
        this.connection = null;
    }
    
    connect() {
        if (!this.connection) {
            this.connection = "DB Connected";
        }
        return this.connection;
    }
}
export default new Database();
// main.js
import db from './database.js';
db.connect();

2. Factory Pattern

Concept

Encapsulates object creation logic for flexible object instantiation.

Implementation

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, handle errors for stability, and perform branching with conditionals. Understand the role of each part as you examine the code.

class User {
    constructor(name, role) {
        this.name = name;
        this.role = role;
    }
    
    getPermissions() {
        return [];
    }
}
class Admin extends User {
    getPermissions() {
        return ['read', 'write', 'delete'];
    }
}
class Guest extends User {
    getPermissions() {
        return ['read'];
    }
}
class Member extends User {
    getPermissions() {
        return ['read', 'write'];
    }
}
// Factory
class UserFactory {
    static createUser(name, role) {
        switch (role) {
            case 'admin':
                return new Admin(name, role);
            case 'member':
                return new Member(name, role);
            case 'guest':
                return new Guest(name, role);
            default:
                throw new Error(`Unknown role: ${role}`);
        }
    }
}
// Usage
const admin = UserFactory.createUser("John", "admin");
console.log(admin.getPermissions());  // ['read', 'write', 'delete']
const guest = UserFactory.createUser("Guest", "guest");
console.log(guest.getPermissions());  // ['read']

3. Module Pattern

Concept

Uses encapsulation to distinguish between private variables and public methods.

Implementation (IIFE)

The following is a detailed implementation code using JavaScript. Understand the role of each part as you examine the code.

const Counter = (function() {
    // Private variable
    let count = 0;
    
    // Private function
    function log() {
        console.log(`Current count: ${count}`);
    }
    
    // Public API
    return {
        increment() {
            count++;
            log();
        },
        decrement() {
            count--;
            log();
        },
        getCount() {
            return count;
        }
    };
})();
// Usage
Counter.increment();  // Current count: 1
Counter.increment();  // Current count: 2
console.log(Counter.getCount());  // 2
console.log(Counter.count);  // undefined (private)

Modern Approach (ES6 Class)

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality. Understand the role of each part as you examine the code.

class Counter {
    #count = 0;  // Private field
    
    increment() {
        this.#count++;
        this.#log();
    }
    
    decrement() {
        this.#count--;
        this.#log();
    }
    
    getCount() {
        return this.#count;
    }
    
    #log() {
        console.log(`Current count: ${this.#count}`);
    }
}
const counter = new Counter();
counter.increment();  // Current count: 1
console.log(counter.getCount());  // 1
// console.log(counter.#count);  // SyntaxError (private)

4. Observer Pattern

Concept

Notifies subscribers of object state changes.

Implementation

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, and process data with loops. Understand the role of each part as you examine the code.

class Subject {
    constructor() {
        this.observers = [];
    }
    
    subscribe(observer) {
        this.observers.push(observer);
    }
    
    unsubscribe(observer) {
        this.observers = this.observers.filter(obs => obs !== observer);
    }
    
    notify(data) {
        this.observers.forEach(observer => observer.update(data));
    }
}
class Observer {
    constructor(name) {
        this.name = name;
    }
    
    update(data) {
        console.log(`${this.name} received notification:`, data);
    }
}
// Usage
const subject = new Subject();
const observer1 = new Observer("Observer1");
const observer2 = new Observer("Observer2");
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify("New data!");
// Observer1 received notification: New data!
// Observer2 received notification: New data!
subject.unsubscribe(observer1);
subject.notify("Another data");
// Observer2 received notification: Another data

Real-world Example: Event System

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, implement logic through functions, process data with loops, and perform branching with conditionals. Understand the role of each part as you examine the code.

class EventEmitter {
    constructor() {
        this.events = {};
    }
    
    on(event, listener) {
        if (!this.events[event]) {
            this.events[event] = [];
        }
        this.events[event].push(listener);
    }
    
    off(event, listener) {
        if (!this.events[event]) return;
        this.events[event] = this.events[event].filter(l => l !== listener);
    }
    
    emit(event, data) {
        if (!this.events[event]) return;
        this.events[event].forEach(listener => listener(data));
    }
    
    once(event, listener) {
        const wrapper = (data) => {
            listener(data);
            this.off(event, wrapper);
        };
        this.on(event, wrapper);
    }
}
// Usage
const emitter = new EventEmitter();
function onUserLogin(user) {
    console.log(`${user.name} logged in`);
}
emitter.on('login', onUserLogin);
emitter.on('login', (user) => {
    console.log(`Welcome, ${user.name}!`);
});
emitter.emit('login', { name: 'John' });
// John logged in
// Welcome, John!
// once: Execute only once
emitter.once('logout', (user) => {
    console.log(`${user.name} logged out`);
});
emitter.emit('logout', { name: 'John' });  // John logged out
emitter.emit('logout', { name: 'John' });  // (not executed)

5. Proxy Pattern

Concept

Controls access to objects or provides additional functionality.

Implementation (ES6 Proxy)

The following is a detailed implementation code using JavaScript. Handle errors for stability and perform branching with conditionals. Understand the role of each part as you examine the code.

const user = {
    name: "John",
    age: 25,
    email: "[email protected]"
};
const handler = {
    get(target, prop) {
        console.log(`Reading ${prop}`);
        return target[prop];
    },
    set(target, prop, value) {
        console.log(`Setting ${prop} to ${value}`);
        
        // Validation
        if (prop === 'age' && typeof value !== 'number') {
            throw new TypeError("Age must be a number");
        }
        
        target[prop] = value;
        return true;
    }
};
const proxyUser = new Proxy(user, handler);
console.log(proxyUser.name);  // Reading name -> John
proxyUser.age = 26;  // Setting age to 26
// proxyUser.age = "26";  // TypeError

Real-world Example: Caching Proxy

The following is a detailed implementation code using JavaScript. Implement logic through functions and perform branching with conditionals. Understand the role of each part as you examine the code.

function createCachedFunction(fn) {
    const cache = new Map();
    
    return new Proxy(fn, {
        apply(target, thisArg, args) {
            const key = JSON.stringify(args);
            
            if (cache.has(key)) {
                console.log("Returning from cache");
                return cache.get(key);
            }
            
            console.log("Computing...");
            const result = target.apply(thisArg, args);
            cache.set(key, result);
            return result;
        }
    });
}
// Usage
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}
const cachedFib = createCachedFunction(fibonacci);
console.log(cachedFib(10));  // Computing....-> 55
console.log(cachedFib(10));  // Returning from cache -> 55

6. Strategy Pattern

Concept

Encapsulates algorithms to allow runtime selection.

Implementation

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality. Understand the role of each part as you examine the code.

// Strategies
class CreditCardStrategy {
    pay(amount) {
        console.log(`Paying ${amount} with Credit Card`);
    }
}
class PayPalStrategy {
    pay(amount) {
        console.log(`Paying ${amount} with PayPal`);
    }
}
class CryptoStrategy {
    pay(amount) {
        console.log(`Paying ${amount} with Cryptocurrency`);
    }
}
// Context
class PaymentContext {
    constructor(strategy) {
        this.strategy = strategy;
    }
    
    setStrategy(strategy) {
        this.strategy = strategy;
    }
    
    executePayment(amount) {
        this.strategy.pay(amount);
    }
}
// Usage
const payment = new PaymentContext(new CreditCardStrategy());
payment.executePayment(10000);  // Paying 10000 with Credit Card
payment.setStrategy(new PayPalStrategy());
payment.executePayment(20000);  // Paying 20000 with PayPal

7. Real-world Example: State Management

The following is a detailed implementation code using JavaScript. Define classes to encapsulate data and functionality, and process data with loops. Understand the role of each part as you examine the code.

class Store {
    constructor(initialState = {}) {
        this.state = initialState;
        this.listeners = [];
    }
    
    getState() {
        return this.state;
    }
    
    setState(newState) {
        this.state = { ...this.state, ...newState };
        this.notify();
    }
    
    subscribe(listener) {
        this.listeners.push(listener);
        return () => {
            this.listeners = this.listeners.filter(l => l !== listener);
        };
    }
    
    notify() {
        this.listeners.forEach(listener => listener(this.state));
    }
}
// Usage
const store = new Store({ count: 0, user: null });
const unsubscribe = store.subscribe((state) => {
    console.log("State changed:", state);
});
store.setState({ count: 1 });
// State changed: { count: 1, user: null }
store.setState({ user: { name: "John" } });
// State changed: { count: 1, user: { name: "John" } }
unsubscribe();  // Unsubscribe

Summary

Key Takeaways

  1. Singleton: Single instance
  2. Factory: Encapsulate object creation
  3. Module: Separate private/public
  4. Observer: Notify state changes
  5. Proxy: Control access
  6. Strategy: Swap algorithms

Next Steps


... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3