클로저 깊이 — 함수가 함수를 기억하는 법
12편의 클로저를 본격적으로. 카운터·IIFE·모듈·메모리 누수까지.
12편에서 살짝 본 클로저를 깊이. 함수가 자기 만들어진 스코프의 변수를 들고 다니는 것 — JS 의 거의 모든 고급 기법이 클로저 위에 서 있습니다. 고급 파트의 시작.
한 줄 정의
클로저 = 함수 + 그 함수가 만들어진 스코프의 변수들. 외부 함수가 끝나도, 내부 함수가 그 변수들을 계속 보유하고 사용할 수 있음.
function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const c = makeCounter();
c(); // 1
c(); // 2
c(); // 3
// makeCounter 가 끝난 지 한참 뒤에도 count 는 살아 있음
// 외부에서는 count 못 봄 — 진짜 private
3가지 일상 패턴
① 카운터 / private 상태
function makeBank(initial = 0) {
let balance = initial;
return {
deposit(n) { balance += n; },
withdraw(n) {
if (n > balance) throw new Error("부족");
balance -= n;
},
getBalance() { return balance; },
};
}
const acc = makeBank(1000);
acc.deposit(500);
acc.getBalance(); // 1500
acc.balance; // undefined ← 직접 접근 불가
② 한 번만 실행 (once)
function once(fn) {
let called = false;
let result;
return function (...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
const initOnce = once(() => expensiveSetup());
initOnce(); // 실행
initOnce(); // 캐시된 결과
initOnce(); // 캐시된 결과
③ partial application / curry
function add(a) {
return function (b) {
return a + b;
};
}
const add5 = add(5); // a=5 를 기억한 함수
add5(3); // 8
add5(10); // 15
// 화살표로
const mul = (a) => (b) => a * b;
const double = mul(2);
double(7); // 14
IIFE — 즉시 실행 함수 표현식 (옛 패턴)
// 옛 var 시절 — 새 스코프를 강제로 만들 때
(function () {
const private_ = "hidden";
// ... 스코프 격리
})();
// 또는
(function () { ... }());
// 현대 JS — let/const + 블록이면 됨
{
const private_ = "hidden";
}
// 모듈(ESM) — 파일 자체가 스코프
IIFE 의 운명. ES6 의 let/const + ESM 모듈 시스템이 등장하면서 새 코드에서는 거의 안 씁니다. 라이브러리의 UMD 빌드 등 호환성 목적이 거의 유일한 사용처.
모듈 패턴 — IIFE + 클로저
// ES 모듈 이전의 캡슐화 표준
const Counter = (function () {
let count = 0; // 외부에서 안 보임
return {
inc() { count++; },
get() { return count; },
};
})();
Counter.inc();
Counter.get(); // 1
Counter.count; // undefined
지금은 ESM 으로 같은 효과 — 파일 내부 변수가 자동으로 캡슐화.
반복문 + 클로저 함정 (3편 다시)
// ❌ var 시절 함정
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 0, 1, 2 가 나오길 기대했지만 → 3, 3, 3
// 이유: var 가 함수 스코프, 모든 콜백이 같은 i 를 봄
// setTimeout 시점에는 i 가 이미 3
// ✅ let — 매 반복마다 새 바인딩
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 0, 1, 2
// ✅ 옛 해결책 — IIFE 로 새 스코프
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => console.log(j), 0);
})(i);
}
React 등 프레임워크에서의 클로저
// React useState — 클로저로 구현
function useState(initial) {
let value = initial;
const setValue = (newValue) => { value = newValue; };
return [() => value, setValue];
}
// (실제 useState 는 더 복잡 — 매 렌더 새 값 캡처)
// 이벤트 핸들러에 props 캡처 — 클로저 함정
function Component({ count }) {
useEffect(() => {
setInterval(() => {
console.log(count); // 첫 렌더의 count 만 기억 (stale)
}, 1000);
}, []); // deps 빈 배열 → 첫 렌더 클로저 유지
}
// 해결 — deps 에 추가 또는 ref 패턴
메모리 누수 — 클로저의 비용
// ❌ 외부 DOM 참조를 잡고 있는 콜백 — 페이지 떠나도 메모리 유지
function bind() {
const el = document.getElementById("big-data");
el.addEventListener("click", () => {
console.log(el.dataset.id); // el 을 클로저로 잡음
});
// el 이 DOM 에서 제거돼도 클로저가 들고 있어서 GC 안 됨
}
// ✅ 필요한 값만 캡처
function bind() {
const el = document.getElementById("big-data");
const id = el.dataset.id; // 작은 값만 캡처
el.addEventListener("click", () => {
console.log(id);
});
}
// ✅ 또는 명시적으로 제거
const handler = () => { ... };
el.addEventListener("click", handler);
// 나중에
el.removeEventListener("click", handler);
메모리 누수 진단. 브라우저 DevTools → Memory → Heap snapshot. 시간 차로 두 번 찍어 비교하면 안 사라지는 객체가 보입니다. SPA(React·Vue) 의 가장 흔한 누수가 정리 안 된 이벤트 리스너.
클로저 vs 클래스 — 캡슐화 비교
// 클로저
function makeUser(name) {
let _name = name;
return {
getName() { return _name; },
setName(n) { _name = n; },
};
}
// 클래스 + #private
class User {
#name;
constructor(name) { this.#name = name; }
getName() { return this.#name; }
setName(n) { this.#name = n; }
}
실전 선택. ① 인스턴스 1~소수 → 클로저(가볍고 직관). ② 다수 인스턴스·상속 → 클래스(메서드가 프로토타입에 한 번만 저장돼 메모리 효율). ③ React 같은 함수형 패러다임 → 클로저 + 훅.
한 줄 정리
- 클로저 = 함수 + 외부 스코프의 변수 기억.
- private 상태·once·partial·메모이즈의 기반.
- let/const + 모듈로 옛 IIFE 의 90% 는 없어졌음.
- 큰 객체를 캡처하면 메모리 누수 — 필요한 값만.
- React 의 stale closure 함정 — deps 또는 ref.
22편 — this 의 정체 (4가지 호출 방식)
전역/메서드/생성자/명시 호출. 화살표 함수의 this 가 다른 이유.
📚 쉽게 배우는 자바스크립트 교재
이전: 20편 정규식 · 현재: 21편 (고급 시작) · 다음 → 22편 this · 진행: 21/26
이전: 20편 정규식 · 현재: 21편 (고급 시작) · 다음 → 22편 this · 진행: 21/26