모듈 패턴 — 캡슐화와 네임스페이스
IIFE 기반 고전 모듈 패턴부터 Revealing Module, ES Module까지 JavaScript 모듈 패턴의 진화를 살펴보고, 캡슐화·싱글턴·의존성 주입 전략을 실용 예제와 함께 정리합니다.
지난 글에서 어댑터·퍼사드·프록시 패턴을 살펴봤습니다. 이번에는 모듈 패턴(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 — 접근 불가
PREFIX와 formatMessage는 클로저로 보호되어 외부에서 접근할 수 없습니다.
증강 모듈 패턴 (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가 불필요합니다.
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을 선택하세요.
지난 글: 어댑터·퍼사드·프록시 패턴 — 구조 패턴의 세 가지 얼굴
다음 글: 미디에이터·믹스인 패턴 — 협력과 조합
읽어주셔서 감사합니다. 😊