[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
- Singleton: Single instance
- Factory: Encapsulate object creation
- Module: Separate private/public
- Observer: Notify state changes
- Proxy: Control access
- Strategy: Swap algorithms
Next Steps
- TypeScript Getting Started
- React Design Patterns
- Node.js Design Patterns