생성 패턴 - 모듈 패턴
이전에 알아봤던 생성자 패턴은 생성자를 사용하는 로직을 말했다. 모듈 패턴도 말 그대로 모듈을 사용하는 로직을 말한다.
아래에 많은 설명을 했지만, 결론은 대부분의 상황에서 ESM (import/export)과 클래스(#)를 사용하는 것이 가장 표준 적이고 편하다.
모듈 패턴
1. 객체를 활용하는 모듈 패턴
ES6의 ESM에서는 파일 자체가 독립된 스코프를 가지기 때문에 import 할 수 있는 파일이 곧 모듈이지만, ES6 이전에는 파일이 아닌 함수를 기준으로 모듈을 정립했다. 함수를 기준으로 모듈을 구현한 이유는 자바스크립트의 스코프가 워낙 넓기 때문이였다.
클로저(Closure)
자바스크립트의 기본 스코프는 전역이다. 다른 파일에서 변수를 선언하더라도 script 태그에서 파일을 가져와 사용하면 script 내부에서 사용하는 변수와 같은 스코프를 가진다. 그래서 변수의 관리가 힘들고 최악의 경우 메모리가 덮어씌워져도 알 수 없다.
<script>
var num = 1;
</script>
<script url="./myFunc.js"></script> // 10으로 저장되어있는 num이 덮어씌워짐
<script>
console.log(num); // 10 출력
</script>
이와 같은 상황에서 모듈을 쓰기 위해서 사람들은 자바스크립트의 클로저(Closure) 특성을 사용했다. 클로저란 선언될 당시의 주변 환경(렉시컬 스코프)을 기억하여, 함수가 밖에서 실행될 때도 그 환경에 접근할 수 있는 기능을 말한다. 즉 자바스크립트에서 함수가 선언되면, 그 함수는 태어난 곳의 상위 스코프를 기억하며, 또한 함수 내부에서만 선언되었던 지역 변수도 기억하고 있다(이게 핵심이다.).
// 전역 스코프 기억
var foo = 'foo';
var bar = 'bar';
function fooBar() {
console.log(foo, bar); // foo bar 출력
};
// 지역 변수 기억
function createCounter() {
let count = 0; // 이 변수는 함수가 끝나면 사라져야 하지만...
return function() {
count++; // 내부 함수가 이 변수를 계속 기억(참조)하고 있습니다.
console.log(count);
};
}
const myCounter = createCounter();
myCounter(); // 1
myCounter(); // 2 (count는 메모리에 살아있음!)
여기에 대해 귀여운 비유가 하나 있는데, 자바스크립트 함수는 태어날 때 주변 환경을 담은 배낭(스코프)를 하나 메고 태어나 어디로 가든 그 배낭 속 내용을 꺼내 쓸 수 있다는 것이다.

클로저는 **함수 + 렉시컬 스코프(배낭)**의 조합입니다. 함수가 선언된 위치를 기억하기 때문에, 부모 함수가 죽어도 그 자식은 부모의 유산(변수)을 계속 쓸 수 있는 마법 같은 기능이죠.
IIFE(Immediately Invoked Function Expression, 즉시 실행 함수)
자바스크립트에서는 ES6에 ESM이 도입되기 전에는 IIFE(즉시 실행 함수)를 이용해 모듈을 구현했다. IIFE는 정의되자마자 즉시 실행되는 함수 표현식을 말한다. IIFE를 사용할 때는 함수를 선언하자마자 즉시 실행시킨 결과를 변수에 저장한다. IIFE 함수는 보통 객체를 리턴하며, 객체에 저장된 값이나 함수를 호출하는 형태로 많이 사용한다.
IIFE에서도 클로저가 적용되어, 함수가 선언되자마자 실행되어 사라져도 그 메모리를 변수에 저장해 놓으면 함수가 알고 있었던 정보도 함께 저장된다. 이 성질을 이용해서 IIFE를 모듈로서 여러 곳에서 사용하되, 스코프를 신경쓰지 않고 사용할 수 있게 되었다.
const myFunc = (function() {
var num = 10;
function printNum() {
console.log(num);
}
return {
print: printNum
};
}());
var num = 1;
myFunc.print(); // 10 출력
클로저와 가비지 컬렉터(GC)
자바스크립트 엔진은 어떤 변수가 어디에서도 참조되지 않으면 메모리에서 지워버린다. 메모리에서 지워버리는 역할을 하는 도구를 가비지 컬렉터(GC)라고 한다. 클로저를 사용하는 경우 내부 함수가 변수를 계속 참조하고 있기 때문에, GC는 변수를 계속 메모리에 남겨둔다. 따라서 클로저를 너무 남발하면 메모리 사용량이 늘어날 수 있다는 점을 유의해야 한다.
2. ESM을 활용하는 모듈 패턴
ESM이 도입되고 나서는 파일 단위로 import, export 하여 모듈 패턴을 사용할 수 있게 되었다. ESM을 사용하는 방법은 간단하기 때문에 다양한 import, export 형식을 정리 해 보았다.

export
// 1. Named Export (여러 개 내보내기 가능)
export const name = 'Daniel';
export function add(a, b) { return a + b; }
// 2. Default Export (파일 당 하나만 가능)
const logger = (msg) => console.log(msg);
export default logger;
import
// 1. Named Import (중괄호 필요, 이름이 같아야 함)
import { name, add } from './module.js';
// 2. Default Import (중괄호 없음, 이름 자유롭게 지정)
import myLog from './logger.js';
// 3. 전체 가져오기 (별칭 지정)
import * as Utils from './utils.js';
Utils.add(1, 2);
3. 모듈 패턴의 캡슐화
모듈 패턴의 핵심은 정보를 숨기는 것, 즉 캡슐화이다. 외부에서는 핵심 로직이나 변수를 수정하지 못하게 막고, 내가 허락한 기능만 밖으로 내보낼 때 사용하는 것이 바로 모듈 패턴이다.
ES10 이후의 클래스에는 접근제한자(#)가 있지만, 이전에는 접근제한자가 없어 엄밀히 비공개라는 개념이 존재하지 않았다. 그래서 비공개 개념을 구현하기 위해 IIFE의 내부 변수로만 값을 정의하고, return 하는 객체에는 변수를 제외하여 외부에서의 접근을 제한했다.
const counterModule = (function() {
// 🔒 비공개 변수 (Private): 외부에서 접근 불가
let count = 0;
// 🔒 비공개 함수: 내부 로직용
function log(message) {
console.log(`[로그]: ${message} (현재 값: ${count})`);
}
// 🔓 공개 인터페이스 (Public): 외부로 노출할 객체 반환
return {
increase: function() {
count++;
log("값이 증가했습니다.");
},
decrease: function() {
count--;
log("값이 감소했습니다.");
},
getValue: function() {
return count;
}
};
})();
// --- 사용 예시 ---
counterModule.increase(); // [로그]: 값이 증가했습니다. (현재 값: 1)
counterModule.increase(); // [로그]: 값이 증가했습니다. (현재 값: 2)
console.log(counterModule.getValue()); // 2
// 🚫 직접 접근 시도
console.log(counterModule.count); // undefined (숨겨져 있음!)
// counterModule.log("해킹!"); // TypeError: counterModule.log is not a function
또 다른 외부 접근 제한 법은 바로 WeakMap 객체를 사용하는 것이였다.
const MyModule = (function() {
// 🔒 이 금고(WeakMap)는 모듈 내부에 꽁꽁 숨겨져서 밖에서는 절대 안 보입니다.
const privateData = new WeakMap();
class Person {
constructor(name) {
// 'this'(생성된 객체)를 열쇠로 해서 데이터를 금고에 넣습니다.
privateData.set(this, { name: name });
}
getName() {
// 금고에서 이 객체(this)의 데이터를 꺼내옵니다.
return privateData.get(this).name;
}
}
return Person;
})();
const me = new MyModule("Daniel");
console.log(me.getName()); // "Daniel"
console.log(me.name); // undefined (속성으로 존재하지 않음)
// 외부에서는 privateData라는 WeakMap에 접근할 방법이 아예 없음!
지금은 클래스에 접근제한자가 도입되었기 때문에 단순히 값을 숨기기 위해서는 WeakMap을 사용하는 것 보다 접근제한자를 사용하는 것이 훨씬 편하지만, 메모리 누수를 방지하기 위해서 WeakMap을 사용하는 경우도 있다.
자바스크립트의 객체는 C++에서 힙(Heap)에 할당된 객체의 포인터와 비슷하다(Button* btn = new Button();). 객체는 메모리 주소를 통해 관리되고, 이 주소를 참조하는 방식에 따라 강한 참조와 약한 참조로 나뉜다.
강한 참조는 일반적인 변수나 Array, Map, Set이 객체를 참조할 때 발생하는 것으로, 두 곳 이상에서 객체를 참조하는 경우 한 곳에서 객체를 참조하지 않는다고 해도 객체는 메모리에 남아있다. 따라서 객체를 삭제하고 싶을 때는 객체를 참조하는 모든 곳에서 제거해야한다.
약한 참조는 WeakMap, WeakSet, WeakRef을 사용할 때 발생하는 특수한 참조이다. 약한 참조는 다른 곳에서 객체의 강한 참조가 남아있지 않으면, GC가 저장된 객체를 수거해간다.
// Map(강한 참조) 예시
// 1. Map 생성 및 버튼(객체) 생성
let strongMap = new Map();
let btn = { label: "전송 버튼" }; // C++의 new와 비슷한 객체 생성
// 2. Map에 데이터 저장 (강한 참조 발생)
strongMap.set(btn, "버튼 관련 비밀 데이터");
// 3. 외부에서 참조 끊기 (DOM에서 제거하고 변수를 null로 만드는 상황)
btn = null;
// --- 가비지 컬렉션이 일어났다고 가정 ---
// 4. 결과 확인
console.log(strongMap.size); // 출력: 1
// btn 변수는 null이 되었지만, strongMap이 객체를 꽉 잡고 있어서
// 메모리상에는 { label: "전송 버튼" } 객체가 여전히 살아있습니다.
// 이게 바로 자바스크립트식 '메모리 누수'의 전형적인 모습입니다.
// WeakMap(약한 참조) 예시
// 1. WeakMap 생성 및 버튼 생성
let weakMap = new WeakMap();
let btn = { label: "전송 버튼" };
// 2. WeakMap에 데이터 저장 (약한 참조 발생)
weakMap.set(btn, "버튼 관련 비밀 데이터");
// 3. 외부에서 참조 끊기
btn = null;
// --- 가비지 컬렉션이 일어나는 순간 ---
// 4. 결과 확인
// WeakMap은 size 속성이 없고 내부를 들여다볼 수(Iteration) 없습니다.
// 왜냐하면 GC가 '언제' 지울지 알 수 없기 때문에 결과가 일관되지 않기 때문이죠.
// 하지만 실제 메모리상에서 { label: "전송 버튼" } 객체는 깔끔하게 삭제됩니다.
위 코드에서 Map을 쓰는 경우에는 button이 화면에서 사라져도 Map이 객체를 강하게 참조 하고 있어 메모리에 계속 남아 메모리 누수가 발생한다. 하지만 WeakMap을 썼다면 btn = null이 되는 순간 GC가 메모리에서 지워버린다.
자바스크립트는 메모리 해제를 GC가 자동으로 수행하기 때문에 사실 메모리 누수에 대해서는 대부분 신경쓰지 않아도 된다. 하지만 아주 거대한 데이터를 다루거나 DOM 요소를 수만 개 만들 때 처럼 특수한 상황에서는 메모리 누수를 신경쓰는 것이 좋고, 이 때 유용한 도구가 바로 WeakMap이다.
모듈 패턴의 변형
믹스인 가져오기 변형
함수가 알고 있었던 정보에는 함수의 매개변수도 포함되는데, 이 성질을 활용하여 모듈 패턴을 믹스인 가져오기 변형으로 확장할 수 있다. IIFE를 실행할 때 더 윗계층에 있는 변수를 매개변수에 넣어 호출하면 전역의 정보와 함수 내부의 정보를 서로 ‘믹스’하여 모듈로써 활용할 수 있게 된다.
const myModule = (function($, _) {
// 전역의 jQuery를 $라는 이름의 '지역 변수'로 가져와서 섞음(Mixin)
const privateVar = "안녕";
return {
render: function() {
// 내부 변수와 외부 라이브러리 정보를 '믹스'하여 사용
const minNum = _.min([10, 5, 20]);
$('#display').text(`${privateVar}, 최소값은 ${minNum}`);
}
};
})(jQuery, _); // 실제 전역 변수를 주입!
myModule.render();
노출 모듈 패턴
노출 모듈 패턴은 모듈의 return 문에 함수를 정의하는 것이 아니라, 함수를 먼저 정의한 후 객체의 값으로 넣어 객체를 return하는 것으로 바꾼 패턴을 말한다. 노출 모듈 패턴을 쓰면 내부 함수나 변수명 변경이 상대적으로 쉽고(객체의 키는 고정되므로), 모듈 내 함수나 변수명이 노출되지 않는다는 장점이 있다.
// 일반 모듈 패턴
const myModule = (function() {
let privateVar = "비밀";
return {
publicMethod: function() {
console.log(privateVar);
}
};
})();
// 노출 모듈 패턴
const myRevealingModule = (function() {
let privateVar = "비밀";
// 모든 로직을 일단 안에서 정의 (C++의 cpp 파일처럼)
function publicMethod() {
console.log(privateVar);
}
// 마지막에 공개할 것들만 이름표를 붙여서 내보냄 (C++의 헤더 파일처럼)
return {
greet: publicMethod // 외부에서는 greet이라는 이름으로 사용
};
})();
노출 모듈 패턴의 단점은 외부에서 모듈의 함수를 변경할 수 없다는 것이다. (핫 픽스 시 모듈의 함수를 덮어쓰는 상황이 있나보다) 외부에서 모듈의 함수를 변경해도, 모듈 내부에서 참조하고 있는 함수는 클로저의 특성으로 인해 변경되지 않는다.
const myModule = (function() {
let privateCount = 0;
function printCount() {
console.log(privateCount);
}
function increment() {
privateCount++;
printCount(); // 내부에서 직접 printCount를 호출함
}
return {
// 내부 함수 printCount의 '참조(주소)'를 이름표(key)에 붙여서 내보냄
exposedPrint: printCount,
increment: increment
};
})();
// 외부에서 공개된 함수를 수정함
myModule.exposedPrint = function() {
console.log("수정된 함수입니다!");
};
myModule.exposedPrint(); // 출력: "수정된 함수입니다!"
myModule.increment(); // 출력: 1 (여전히 '기존' printCount가 실행됨!)
이런 부분에서 노출 모듈 패턴으로 만들어진 모듈은 기존 모듈 패턴 보다 경직된 구조인 것을 참고하자.