🔗

클로저와 스코프 체인 — 함수가 외부 변수를 기억하는 원리

Lexical Environment 객체가 만드는 스코프 체인의 내부 구조

function makeCounter() {
  let count = 0;
  return function() {
    return ++count;
  };
}
const counter = makeCounter();
counter(); // 1
counter(); // 2 — count가 살아 있다

makeCounter()는 이미 반환됐는데, 내부 함수가 count를 계속 쓸 수 있다. 이게 클로저.

V8 내부 — Lexical Environment

모든 함수 실행 시 Lexical Environment 객체가 생성된다. 여기에 로컬 변수와 외부 환경(outer) 참조가 저장된다.

makeCounter() 호출 시:
  LexicalEnvironment {
    count: 0,
    outer: → global LexicalEnvironment
  }

반환된 익명 함수:
  [[Environment]]: → makeCounter의 LexicalEnvironment  ← 이게 클로저

익명 함수의 [[Environment]] 내부 슬롯이 makeCounter의 Lexical Environment를 참조한다. 이 참조가 살아 있는 한 count가 GC되지 않는다.

counter()를 호출하면: 자기 Lexical Environment에서 count를 찾고 → 없으면 outer를 따라 올라간다 → makeCounter의 환경에서 count를 찾는다. 이게 스코프 체인.

메모리 누수 주의

클로저가 외부 변수를 참조하면 그 환경이 GC되지 않는다. 이벤트 리스너에 클로저를 등록하고 해제 안 하면 메모리 누수.

// ❌ 누수 — element가 제거되어도 handler가 bigData를 잡고 있다
const bigData = new Array(1000000);
element.addEventListener('click', () => console.log(bigData.length));

// ✅ 해제
const handler = () => console.log(bigData.length);
element.addEventListener('click', handler);
element.removeEventListener('click', handler);  // 해제 → bigData GC 가능

핵심 포인트

1

함수 생성 시 [[Environment]] 슬롯에 현재 Lexical Environment가 저장된다

2

변수 참조 시 자기 환경 → outer → outer... 순으로 스코프 체인을 따라간다

3

외부 변수를 참조하는 함수가 살아 있으면 그 Lexical Environment는 GC되지 않는다

4

이벤트 리스너의 클로저는 반드시 removeEventListener로 해제

사용 사례

데이터 은닉 — 클로저로 private 변수 구현 (모듈 패턴) 커링/부분 적용 — 인자를 미리 고정하는 함수 생성

참고 자료