디자인 패턴 5종 — Singleton·Observer·Strategy
자주 만나는 패턴 5개의 JS 식 구현.
디자인 패턴은 자주 만나는 문제의 표준 해결책. JS 는 다른 언어보다 가볍게 — 클로저·고차 함수·일급 함수 덕에 패턴이 짧아집니다. 25편은 매일 만나는 5종을 정리.
① Singleton — 인스턴스가 단 하나
// 옛 방식 — 클래스 + 정적 캐시
class Config {
static #instance;
static getInstance() {
if (!Config.#instance) Config.#instance = new Config();
return Config.#instance;
}
}
// 현대 JS — 모듈 자체가 싱글톤
// config.js
const config = {
apiUrl: process.env.API_URL,
timeout: 5000,
};
export default config;
// 어디서 import 해도 같은 객체
import config from "./config.js";
현대 JS 에서 Singleton. ESM 모듈은 한 번 로드되면 모듈 평가 결과가 캐시됨 — 어디서 import 해도 같은 export. 별도 패턴 없이 "그냥 모듈로 두면 싱글톤" 이 됨. 클래스로 만드는 옛 방식은 거의 안 씀.
② Observer (Pub/Sub) — 이벤트 시스템
class EventBus {
#listeners = new Map();
on(event, fn) {
if (!this.#listeners.has(event)) this.#listeners.set(event, new Set());
this.#listeners.get(event).add(fn);
return () => this.off(event, fn); // unsubscribe 반환
}
off(event, fn) {
this.#listeners.get(event)?.delete(fn);
}
emit(event, ...args) {
this.#listeners.get(event)?.forEach(fn => fn(...args));
}
}
const bus = new EventBus();
const unsub = bus.on("login", user => console.log("환영", user.name));
bus.emit("login", { name: "준성" }); // "환영 준성"
unsub(); // 구독 해제
React 의 상태 라이브러리(zustand·jotai), DOM 의 addEventListener, Node 의 EventEmitter — 전부 이 패턴. JS 의 가장 보편적 비동기 패턴.
③ Factory — 객체 생성을 함수로 추상화
// 직접 new 대신 팩토리 함수
function createUser(role) {
switch (role) {
case "admin":
return { role, perms: ["read", "write", "admin"] };
case "user":
return { role, perms: ["read", "write"] };
case "guest":
return { role, perms: ["read"] };
default:
throw new Error("unknown role");
}
}
const a = createUser("admin");
const g = createUser("guest");
// 더 복잡한 예 — DB 클라이언트 팩토리
function createDbClient({ kind, ...config }) {
if (kind === "postgres") return new PgClient(config);
if (kind === "mysql") return new MySqlClient(config);
if (kind === "sqlite") return new SqliteClient(config);
throw new Error(`unknown kind: ${kind}`);
}
Factory 의 가치. 호출자가 new 를 직접 안 써도 됨 → 구현 클래스를 자유롭게 바꿀 수 있음. 의존성 주입(DI) 의 기초.
④ Module — 묶음 (ESM 으로 거의 표준화)
// 옛 IIFE 모듈
const Counter = (function () {
let count = 0;
return {
inc() { count++; },
get() { return count; },
};
})();
// 현대 ESM (19편) — 파일 자체가 모듈
// counter.js
let count = 0;
export function inc() { count++; }
export function get() { return count; }
// 사용
import { inc, get } from "./counter.js";
21편의 IIFE·모듈 패턴이 ESM 등장 후 거의 안 씀. ESM 자체가 깔끔한 캡슐화.
⑤ Strategy — 알고리즘을 함수로 주입
// 정렬 알고리즘을 외부에서 주입
function sortBy(arr, strategy) {
return [...arr].sort(strategy);
}
const byPrice = (a, b) => a.price - b.price;
const byName = (a, b) => a.name.localeCompare(b.name);
const byNewest = (a, b) => b.createdAt - a.createdAt;
sortBy(products, byPrice);
sortBy(products, byName);
sortBy(products, byNewest);
// 더 복잡한 — 결제 처리
const paymentStrategies = {
card: (amount) => chargeCard(amount),
bank: (amount) => transferBank(amount),
crypto: (amount) => crypto.charge(amount),
};
function pay(amount, method) {
const strategy = paymentStrategies[method];
if (!strategy) throw new Error(`unknown: ${method}`);
return strategy(amount);
}
Strategy 가 JS 에서 가장 자연스러운 이유. 함수가 일급 객체(값처럼 다룰 수 있음) 라서, 다른 언어처럼 별도 인터페이스/클래스 만들 필요 없음. 함수를 인자로 받기 = 즉 Strategy.
보너스 패턴들 — 짧게
Decorator (16편 TS 와 다름)
// 함수를 감싸 기능 추가
function withLogging(fn) {
return function (...args) {
console.log("호출:", fn.name, args);
return fn(...args);
};
}
const safeAdd = withLogging(add);
Adapter
// 옛 API 를 새 API 모양으로
class NewLogger {
log(level, msg) { console.log(`[${level}] ${msg}`); }
}
// 옛 인터페이스에 맞추는 어댑터
function oldLoggerAdapter(newLogger) {
return {
info: (m) => newLogger.log("info", m),
warn: (m) => newLogger.log("warn", m),
error: (m) => newLogger.log("error", m),
};
}
Memoize
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const fibo = memoize(function fib(n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
});
한 표 — 5종 요약
| 패턴 | 의도 | 현대 JS 의 형태 |
|---|---|---|
| Singleton | 인스턴스 1개 | ESM 모듈 자체 |
| Observer | 이벤트 발행/구독 | EventTarget·EventEmitter·zustand |
| Factory | 객체 생성 추상화 | 팩토리 함수 + 인터페이스 |
| Module | 캡슐화·재사용 | ESM 파일 |
| Strategy | 알고리즘 교체 | 고차 함수 (함수를 인자로) |
패턴은 도구지 목적 아님. 패턴 이름을 외워 적용하기 위해 코드를 비틀지 마세요. 같은 문제를 두 번째 만났을 때 비로소 패턴이 의미 있음. 첫 번째는 그냥 단순한 코드로.
26편 — 테스트 jest (시리즈 마지막)
왜 테스트, 첫 단위 테스트, mock 한 줄, CI 연결.
이전: 24편 제너레이터 · 현재: 25편 (고급) · 다음 → 26편 테스트 (시리즈 마지막) · 진행: 25/26