notes
24장. 클로저
모던 JS Deep Dive — 클로저의 렉시컬 환경·상태 은닉 핵심 정리.
참고: 이웅모 《모던 자바스크립트 Deep Dive》 24장 · [MDN Closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures)
함수가 자기 정의 위치의 환경을 들고 다니는 현상. “끝날 때 다 죽는다”가 아니라, 누군가 참조하는 환경 레코드는 산다. 23의 외부 렉시컬 환경 참조가 주인공이다.
flowchart LR
DEF[함수 정의 평가] --> ENV["[[Environment]] = 그 위치 LE"]
CALL[나중에 호출] --> SEARCH[식별자 검색]
SEARCH --> ENV
ENV --> OUTER[외부 환경으로 이어짐]
24.1 렉시컬 스코프
식별자 해석은 정의 위치의 스코프 체인을 따른다. 호출 위치가 아니다. 클로저는 이 규칙의 자연스러운 결과다.
const x = 1;
function outer() {
const x = 10;
function inner() {
console.log(x); // 10
}
return inner;
}
const f = outer();
f(); // 여전히 10 — 호출 장소가 전역이어도
24.2 함수 객체의 [[Environment]]
함수가 평가·생성될 때, 자신이 태어난 렉시컬 환경을 [[Environment]] 내부 슬롯에 저장한다.
호출 시 함수 실행 컨텍스트의 “외부 렉시컬 환경 참조”가 이 슬롯을 본다.
그래서 함수는 코드 + 출생지의 환경 포인터다.
24.3 클로저와 렉시컬 환경
좁은 정의: 외부 함수보다 오래 살아남아 외부 상태를 참조하는 함수.
넓은 감각: 모든 함수는 [[Environment]]를 가지므로 “닫혀 있다”. 다만 실무에서 말하는 클로저는 외부 지역이 수명 연장되는 패턴이다.
function makeCounter() {
let n = 0;
return function () {
n += 1;
return n;
};
}
const c1 = makeCounter();
const c2 = makeCounter();
console.log(c1(), c1(), c2()); // 1 2 1 — 환경이 각각
WHY: n은 makeCounter가 끝난 뒤에도 c1/c2가 붙잡아 산다. 두 카운터는 다른 환경.
24.4 클로저의 활용
- 콜백·이벤트 핸들러가 바깥 변수 사용
- 함수형: partial / curry / once
- 모듈 패턴(14): 비공개 상태 + 공개 API
- React 등에서 이펙트·핸들러가 props/state를 캡처 (stale closure 주의)
function once(fn) {
let called = false;
let result;
return function (...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
// 부분 적용
function multiply(a, b) { return a * b; }
function partial(fn, a) {
return function (b) { return fn(a, b); };
}
const double = partial(multiply, 2);
console.log(double(5)); // 10
24.5 캡슐화와 정보 은닉
외부에서 직접 못 만지는 상태를 클로저로 가둔다. class #private(25) 이전의 고전 무기.
const Person = (function () {
let age = 0; // 약식 — 인스턴스별이 아님에 주의
return function (name) {
this.name = name;
this.getAge = () => age;
this.setAge = (n) => { age = n; };
};
})();
인스턴스마다 비공개가 필요하면 생성자 안에서 let을 두고 메서드를 그 스코프에 닫거나, WeakMap/# 필드를 쓴다.
function Person(name) {
let age = 0;
this.name = name;
this.getAge = () => age;
this.setAge = (n) => { age = n; };
}
24.6 자주 발생하는 실수
var 루프 + 클로저
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 3 3 3 — i 하나
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 0 1 2 — 반복별 바인딩
과도한 캡처·누수
큰 객체를 클로저가 붙잡으면 GC가 못 걷는다. 핸들러 제거·nulling·약한 참조(WeakRef 노트) 검토.
stale closure
오래된 값을 닫아 둔 콜백이 최신 state를 못 봄. 의존 배열·ref·함수형 업데이트로 해결하는 패턴이 프레임워크마다 있다.
예제 — 왜 이렇게 쳐 봤냐
function createStore(initial) {
let state = initial;
const listeners = new Set();
return {
getState: () => state,
setState: (next) => {
state = typeof next === 'function' ? next(state) : next;
listeners.forEach((l) => l(state));
},
subscribe: (l) => {
listeners.add(l);
return () => listeners.delete(l);
},
};
}
WHY: state·listeners가 클로저로 숨고, API만 공개. 미니 스토어.
헷갈리던 장면 / 함정
- 클로저 = 버그 라는 미신 — 도구다. 실수는 수명·공유·stale
- 화살표/일반 함수 this와 클로저를 한 문제로 섞기
- “모든 외부 변수를 복사한다” — 참조로 환경을 붙잡음
- 디버거에서 Closure 스코프에 불필요한 값이 보이는 것 = 캡처 단서
표
질문 → 자답
Q. 클로저를 없앨 수 있나?
A. 함수가 있는 한 [[Environment]]는 있다. “안 쓰는” 건 외부 상태를 캡처하지 않는 설계.
Q. 메모리 누수인가? A. 필요 없는 참조를 붙잡으면 누수. 필요한 상태 유지면 정상.
Q. class private과 차이?
A. #은 인스턴스별 필드 문법. 클로저는 스코프 기반. 둘 다 은닉 수단.
웹 보강 메모
MDN Closures의 “practical closures”와 루프 예제를 다시 치고, 레포에서 addEventListener 클로저·구독 해제가 짝인지 검색했다.
실무로 바로
- 루프 콜백: let/const
- 구독·리스너: unsubscribe 반환
- 큰 데이터: 필요한 스칼라만 캡처
- 모듈 경계: ESM + 클로저/
#
“모든 함수가 클로저” vs “실무 클로저”
스펙 감각으로는 함수마다 [[Environment]]가 있으니 닫혀 있다. 대화·코드리뷰에서 클로저라고 부르는 지점은 대개 외부 지역 상태가 수명 연장되며 유용/위험한 패턴이다. 둘 다 틀리지 않다. 문맥을 밝히자.
반환하지 않아도 클로저
function setup(btn) {
let clicks = 0;
btn.addEventListener('click', () => {
clicks += 1;
console.log(clicks);
});
}
리스너가 살아 있는 한 setup의 clicks 환경도 산다. return이 없어도 클로저.
커링 스케치
const curry2 = (fn) => (a) => (b) => fn(a, b);
const add = (a, b) => a + b;
const add10 = curry2(add)(10);
console.log(add10(5)); // 15
각 화살표가 바깥 a를 붙잡는다. 일급(18) + 클로저.
디버깅 팁
- 개발자 도구 Scope에 Closure로 보이는 객체 = 붙잡은 환경
- 불필요한 키가 보이면 캡처 범위를 줄일 단서
- 리스너 누수 의심 시 getEventListeners(브라우저) / 구독 카운트
정보 은닉 선택지 비교
여기서 잠깐 💁♂️
25 class는 프로토타입·은닉·상속을 문법으로 묶는다. 클로저는 그 아래 수명 모델이다.