자바스크립트 교재 · 25편 / 26편

디자인 패턴 5종 — Singleton·Observer·Strategy

자주 만나는 패턴 5개의 JS 식 구현.

고급읽는 시간 8분2026-05-17
Singleton·Observer·Factory·Module·Strategy 5개 패턴이 카드로 정리된 도식

디자인 패턴은 자주 만나는 문제의 표준 해결책. 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

© 2026 주나이테크(주) @JUNAITECH