모듈 패턴 — 캡슐화와 네임스페이스

IIFE 기반 고전 모듈 패턴부터 Revealing Module, ES Module까지 JavaScript 모듈 패턴의 진화를 살펴보고, 캡슐화·싱글턴·의존성 주입 전략을 실용 예제와 함께 정리합니다.

· 7 min read · PALDYN Team

지난 글에서 어댑터·퍼사드·프록시 패턴을 살펴봤습니다. 이번에는 **모듈 패턴(Module Pattern)**을 정리합니다. 모듈 패턴은 JavaScript에서 캡슐화, 네임스페이스 오염 방지, 의존성 관리를 동시에 해결하기 위해 등장했으며, ES2015 이전과 이후로 구현 방식이 크게 달라졌습니다.

모듈 패턴의 진화

왜 모듈 패턴이 필요했나

ES2015 이전 브라우저 JavaScript는 모든 코드가 전역 스코프를 공유했습니다. var counter = 0;을 파일 A에 쓰면 파일 B에서도 그 counter를 덮어쓸 수 있었습니다. 라이브러리 충돌, 예측 불가 사이드 이펙트가 빈번했습니다. 모듈 패턴은 **IIFE(즉시 실행 함수)**를 사용해 스코프를 격리하는 방법으로 이 문제를 해결했습니다.


IIFE 모듈 패턴

가장 단순한 형태입니다. 함수를 즉시 실행해 내부 변수는 외부에서 접근할 수 없고, 반환 객체만 노출합니다.

const Logger = (function () {
  const PREFIX = '[APP]'; // private

  function formatMessage(level, msg) {
    return `${PREFIX} [${level.toUpperCase()}] ${msg}`;
  }

  return {
    info:  (msg) => console.log(formatMessage('info', msg)),
    warn:  (msg) => console.warn(formatMessage('warn', msg)),
    error: (msg) => console.error(formatMessage('error', msg)),
  };
})();

Logger.info('서버 시작');  // [APP] [INFO] 서버 시작
Logger.PREFIX;            // undefined — 접근 불가

PREFIXformatMessage는 클로저로 보호되어 외부에서 접근할 수 없습니다.

증강 모듈 패턴 (Augmenting)

기존 모듈에 기능을 추가할 때, 모듈 객체를 매개변수로 받아 확장 후 반환합니다.

const Logger = (function (mod) {
  mod.debug = (msg) => console.debug(`[DEBUG] ${msg}`);
  return mod;
})(Logger || {});

파일 로드 순서에 관계없이 안전하게 모듈을 확장할 수 있습니다.


Revealing Module 패턴

IIFE 모듈의 단점은 반환 객체 안에서 인터페이스를 다시 정의해야 한다는 것입니다. Revealing Module은 내부에서 함수를 정의하고, 반환 시 어느 것을 공개할지만 명시합니다.

const CartModule = (function () {
  let items = [];

  function add(product) {
    items.push(product);
    _notify();
  }

  function remove(id) {
    items = items.filter(p => p.id !== id);
    _notify();
  }

  function getItems() {
    return [...items]; // 방어적 복사
  }

  function _notify() { // private — 반환 안 함
    console.log('장바구니 업데이트:', items.length);
  }

  // 공개 API만 명시적으로 노출
  return { add, remove, getItems };
})();

CartModule.add({ id: 1, name: '키보드', price: 80000 });
CartModule.getItems(); // [{ id: 1, ... }]
CartModule._notify;    // undefined

내부 함수 포인터를 반환하기 때문에 내부 함수끼리 상호 참조 시 public 이름이 아닌 내부 이름을 사용합니다—덕분에 반환 객체 수정 없이 내부 구현을 바꿀 수 있습니다.


ES Module — 현대 표준

ES2015부터 언어 수준에서 모듈을 지원합니다. 파일 자체가 모듈 스코프를 가지므로 IIFE가 불필요합니다.

Revealing Module vs ES Module 코드 비교

Named Export와 Default Export

// math.js
export const PI = 3.14159;

export function add(a, b) { return a + b; }

export function multiply(a, b) { return a * b; }

export default class MathHelper {
  square(n) { return n * n; }
}
// main.js
import MathHelper, { PI, add, multiply } from './math.js';

console.log(PI);               // 3.14159
console.log(add(2, 3));        // 5

const helper = new MathHelper();
console.log(helper.square(4)); // 16

Live Binding — ES Module의 핵심

ES Module의 import는 값 복사가 아닌 라이브 바인딩입니다.

// counter.js
export let count = 0;
export function increment() { count++; }

// main.js
import { count, increment } from './counter.js';

console.log(count); // 0
increment();
console.log(count); // 1 — 원본 변경이 반영됨!

CommonJS(require)는 값을 복사하지만, ES Module은 원본 변수의 참조를 유지합니다.


모듈 패턴 — 실용 전략

싱글턴 모듈

ES Module은 같은 경로를 여러 번 import해도 모듈 평가는 한 번만 실행됩니다. 이 특성을 이용해 싱글턴을 자연스럽게 구현합니다.

// store.js — 싱글턴 상태 관리
const state = { user: null, theme: 'dark' };
const listeners = new Set();

export function getState() { return { ...state }; }

export function setState(patch) {
  Object.assign(state, patch);
  listeners.forEach(fn => fn(state));
}

export function subscribe(fn) {
  listeners.add(fn);
  return () => listeners.delete(fn); // unsubscribe 반환
}

store.js는 앱 전체에서 딱 한 번 초기화되고, 어디서 import해도 같은 state 객체를 공유합니다.

의존성 주입과 팩토리 모듈

테스트 가능성을 높이려면 의존성을 하드코딩하지 않고 주입합니다.

// userService.js
export function createUserService({ httpClient, logger }) {
  return {
    async getUser(id) {
      logger.info(`getUser(${id})`);
      return httpClient.get(`/users/${id}`);
    },
    async createUser(data) {
      logger.info('createUser');
      return httpClient.post('/users', data);
    },
  };
}

// 프로덕션
const userService = createUserService({ httpClient, logger });

// 테스트
const mockService = createUserService({
  httpClient: { get: async () => ({ id: 1 }) },
  logger:     { info: () => {} },
});

네임스페이스 배럴(Barrel) 패턴

기능별 디렉토리의 진입점 index.js에서 하위 모듈을 모아 다시 내보냅니다.

// services/index.js — 배럴
export { UserService } from './user.js';
export { AuthService } from './auth.js';
export { PaymentService } from './payment.js';

// 사용처
import { UserService, AuthService } from './services';

개별 파일 구조를 외부에 노출하지 않고, 진입점 하나로 API를 통일합니다.


IIFE vs Revealing vs ES Module 선택 기준

상황추천
레거시 브라우저, 번들러 없음IIFE / Revealing Module
공개 API가 명확한 라이브러리Revealing Module
현대 프로젝트 (Node.js, 번들러)ES Module
트리쉐이킹이 중요한 라이브러리ES Module (Named Export 선호)

ES Module은 정적 분석이 가능하기 때문에 번들러가 사용하지 않는 export를 제거(트리쉐이킹)할 수 있습니다. 새 프로젝트라면 항상 ES Module을 선택하세요.


지난 글: 어댑터·퍼사드·프록시 패턴 — 구조 패턴의 세 가지 얼굴

다음 글: 미디에이터·믹스인 패턴 — 협력과 조합


읽어주셔서 감사합니다. 😊