본문으로 건너뛰기
김신건의 로그

[Javascript] 클로저

· 수정 · 📖 약 1분 · 596자/단어 #javascript #closure #lexical-environment #fundamentals
closure, 클로저, 클로져, 클로저(Closure), JavaScript closure, JS closure

정의

클로저 (Closure): 함수가 자신이 정의된 시점의 외부 변수들을 계속 참조할 수 있는 성질.

함수가 일급 객체 (일급 함수) 인 언어의 자연스러운 결과. 메커니즘은 Lexical Environment + 함수 객체의 [[Environment]] 내부 슬롯.

자세한 메커니즘은 함수는 끝났는데 변수가 살아있다, Lexical Environment와 클로저의 메커니즘 글 참고.

핵심 특성

측면동작
참조 시점정의 위치 (Lexical), 호출 위치가 아님
수명클로저 함수가 살아있는 한 외부 변수도 살아있음
격리호출마다 새 LE → 독립된 상태
메모리 위치힙 (heap), 콜 스택이 아님

가장 단순한 예

function makeCounter() {
  let count = 0; // 외부 변수
  return () => ++count; // 안쪽 함수가 count 를 "닫아서 들고 다님"
}

const a = makeCounter();
const b = makeCounter();

a(); // 1
a(); // 2
b(); // 1, b 의 count 는 독립

makeCounter() 가 반환된 뒤에도 count 가 살아남는다. 안쪽 함수가 count 를 참조하기 때문.

왜 GC 되지 않는가

makeCounter 의 실행 컨텍스트는 콜 스택에서 사라졌지만, 변수가 실제로 사는 Lexical Environment 는 힙에 있는 객체. 누군가 (= 반환된 함수) 가 참조하는 한 살아남는다.

참조 사슬:

const a (Root)

반환된 화살표 함수
  ↓ [[Environment]] 슬롯
makeCounter 의 LE
  ↓ Environment Record
count: N

이 사슬이 끊어져야 GC 회수.

사용 패턴

1. 사적 상태 (Private State)

ES Modules 이전부터 정통 패턴.

const counter = (function () {
  let count = 0; // 외부에서 절대 접근 불가
  return {
    inc: () => ++count,
    get: () => count,
    reset: () => (count = 0),
  };
})();

counter.inc(); // 1
counter.count; // undefined, 직접 접근 X

2. 콜백이 외부 컨텍스트 기억

function attachLogger(label) {
  return (event) => console.log(`[${label}]`, event);
}

const log = attachLogger('user');
button.onclick = log; // log 는 호출 시 label='user' 를 기억

3. 이터레이터 / 제너레이터의 상태 보존

function naturals() {
  let n = 1;
  return { next: () => ({ value: n++, done: false }) };
}

const it = naturals();
it.next().value; // 1
it.next().value; // 2

4. 부분 적용 (Partial Application) / 커링

const multiply = (a) => (b) => a * b;
const double = multiply(2);
double(5); // 10

함정

for + var + 클로저

const fns = [];
for (var i = 0; i < 3; i++) {
  fns.push(() => console.log(i));
}
fns[0](); // 3
fns[1](); // 3
fns[2](); // 3

var i 는 함수 스코프 → LE 가 루프 전체에 하나만. 세 콜백 모두 같은 i 를 본다.

let 으로 해결:

for (let i = 0; i < 3; i++) {
  fns.push(() => console.log(i));
}
// 0, 1, 2

스펙상 for + let 은 매 iteration 마다 별도 LE 를 만든다 (ECMA-262 §14.7.4.3).

메모리 누수

이벤트 핸들러나 setTimeout 콜백에 클로저를 넣을 때 큰 객체를 무의식 중에 보존할 수 있다.

function attachHandler() {
  const hugeData = new Array(1_000_000).fill('x');
  document.getElementById('btn').addEventListener('click', () => {
    console.log('clicked'); // hugeData 안 씀
  });
  // 모던 엔진(V8)은 escape analysis 로 hugeData 회수,
  // 그러나 eval/new Function 가능성이 있으면 LE 통째로 보존
}

큰 데이터는 명시적으로 해제 (data = null).

클로저 ≠ 익명 함수

흔한 오해. 익명 함수, 화살표 함수, 일반 함수 모두 클로저가 될 수 있고, 안 될 수도 있다.

클로저는 함수 + 그 함수가 정의된 시점의 외부 변수들 의 묶음.

외부 변수를 참조하지 않으면 단순한 함수일 뿐.

참고

이 글의 용어 (3개)
[Javascript] Lexical Environmentjavascript
정의 Lexical Environment (어휘적 환경) 는 ECMAScript 명세 (§9.1) 가 정의한 내부 객체. 함수가 호출될 때마다 새로 만들어지며, 이 호출의 변수들…
고차 함수javascript
- 고차함수란 하나 이상의 함수를 인자로 받거나 반환하는 함수를 말합니다. - 일급 함수라는 특성을 활용해 실제로 함수를 다루는 연산을 수행하는 함수입니다. 즉, 아래 조건 중 …
일급 함수javascript
- 함수를 값처럼 다루어 변수에 담거나 다른 함수의 인자로 전달하거나 함수의 반환값으로 사용할 수 있는 함수. 일급 함수의 이러한 특성을 활용하여 를 구현할 수 있습니다. - 함…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기