icon

메티의 블로그

디자인패턴: Module
디자인패턴: Module

디자인패턴: Module

summary
이 글에서는 모듈 패턴의 개념과 역사, 그리고 ES6 이후 JavaScript에서의 모듈 기능 지원에 대해 설명하고 있습니다. 또한, 모듈 패턴의 장단점과 트리 쉐이킹에 대한 내용을 다루고 있습니다.
Tags
Design Pattern
view_count
날짜
Aug 13, 2025
상태
공개

Module 패턴

Module 패턴이란?

모듈 패턴은 하나의 진입접 객체를 통해 관련된 변수와 함수를 묶어 관리하여, 내부 구현을 스코프 안에 감추고 외부 노출할 API 만 반환하게하는 패턴입니다. 이렇게 되면, 진입점 객체를 제외한 내부 변수들은 이를 사용하는 외부에서 사용할 수 없게 됩니다.

왜 사용하나요?

모듈 패턴은 프로그래밍 언어가 모듈 기능을 지원하지 않을 때, 전역 네임스페이스의 오염을 방지하기 위해 사용하는 디자인 패턴입니다. 네임스페이스를 지원하지 않는 언어에서 네임스페이스와 같은 변수 접근 제어를 구현하기 위해 사용합니다.

ES6 이전 JS 의 모듈 패턴

ES6 이후의 JS 는 모듈 기능을 지원(네임스페이스 구분 기능 지원)하기 때문에 언어적으로 편리하게 모듈 패턴을 사용할 수 있지만, ES6 이전 JS 는 모듈을 지원하지 않아서 클로저를 이용해 네임스페이스 구분을 했었습니다. 예전 JS 자료들을 읽다보면 자주 보이는 그 유명한 IIFE(Immediately Invoked Function Expresstion) 가 모듈 패턴을 구현하는 핵심 도구였습니다.
var CounterModule = (function () { // private var count = 0; function changeBy(val) { count += val; } // public API return { increment: function () { changeBy(1); }, decrement: function () { changeBy(-1); }, value: function () { return count; } }; })(); CounterModule.increment(); console.log(CounterModule.value()); // 1 console.log(CounterModule.count); // undefined (private)
IIFE를 사용한 모듈 패턴 예시, IIFE 는 클로저를 통해 여러 함수들이 스코프 내 프라이빗 변수들을 관리 하는 등으로 활용되곤 했습니다.

현재 JS 의 모듈

현재 JS 에서는 모듈 기능을 자체적으로 지원(ESM)해줍니다. 모듈 기능은 엔진 내부적으로 지원하는 기능으로, IIFE 형태로 변환되는 것이 아닙니다. JS 엔진이 모듈을 해석할 때는 모듈 스코프 기준으로 실행 컨텍스트가 생성됩니다. (렉시컬 환경이 모듈 스코프)
그렇다면 CommonJS 나 AMD 같은 구버전 모듈 지원 방식은 어떻게 구현했던 것 일까요?
CommonJS: 런타임에서 IIFE 로 래핑
AMD: 외부 라이브러리 (RequireJS) 사용하여 구현
const privateValue = 'This is a value private to the module!' export function add(x, y) { return x + y } export function multiply(x) { return x * 2 } export function subtract(x, y) { return x - y } export function square(x) { return x * x }
ESM 방식의 모듈 export, math.js 파일에 정의 되어있다고 가정합니다. privateValue 는 타 모듈에서는 접근 할 수 없습니다.
import { add, multiply, subtract, square } from './math.js' // expected-error: ... console.log(privateValue)
math.js 모듈에서 가져온 API 들 입니다. math.js 내의 privateValue 는 접근 할 수 없습니다.
이외에 export 된 변수 이름이 로컬 변수와 겹칠 때는 as 로 이름을 재정의 할 수도 있습니다.
import { add as addValues, multiply as multiplyValues, subtract, square, } from './math.js' function add(...args) { return args.reduce((acc, cur) => cur + acc) } function multiply(...args) { return args.reduce((acc, cur) => cur * acc) } /* From math.js module */ addValues(7, 8) multiplyValues(8, 9) subtract(10, 3) square(3) /* From index.js file */ add(8, 9, 2, 10) multiply(8, 9, 2, 10)

export default

export default 를 통해 모듈 객체를 export 하는 것은 모듈당 하나만 가능하며, 이는 모듈이 하나의 주된 기능을 제공할 때 의미론적으로 표현하기 위해 사용합니다.
// 📁 user.js export default class User { constructor(name) { this.name = name; } }
user.js 파일에는 User 라는 클래스 하나만 제공하겠다는 의미론적인 표현을 사용하기 위해 default 를 사용합니다.
/ 📁 main.js import User from './user.js'; new User('John');
default 로 가져올 때는 구조분해 할당 없이 바로 사용합니다.
export default function add(x, y) { return x + y } export function multiply(x) { return x * 2 } export function subtract(x, y) { return x - y } export function square(x) { return x * x }
그러므로 export default 와 export 를 섞어 사용하는 것도 문법적인 문제가 있는건 아니지만, 어떤 사람은 헷갈릴 수도 있습니다.
export default { add, multiply, sort }
같은 맥락으로 이런식으로 export default 시 여러 기능을 묶어 export 하는 것도 사실상 좋은 방법은 아닙니다.
심지어 위와 같은 export default { add, multiply, sort } 방식은 트리쉐이킹 관점에서 불리합니다. export 만 하면 트리쉐이킹 시 import 받지 않은 것은 함께 번들링 되지 않지만, export default 시 모든 것을 다 번들링 하기 때문에, 번들 크기가 불필요하게 커질 수 있습니다.