1. 개요

클로저는 “함수가 자신이 정의된 스코프를 기억한다”는 것뿐인데, 실전에서 헷갈리는 건 대부분 딱 하나의 패턴에서 나온다. 반복문 안에서 비동기 콜백을 만들 때다. var로 선언한 반복 변수가 클로저에 잡히면 예상과 다른 값이 찍힌다. 이 글은 클로저가 뭔지 짧게 정리하고, 그 버그가 왜 나는지, let이 왜 고치는지를 본다.


2. 핵심 내용

2-1. 함수는 태어난 스코프를 기억한다

함수를 정의하면 그 함수는 자기가 정의될 당시 접근 가능했던 변수들의 참조를 계속 들고 있는다. 함수가 실행되는 시점이 아니라 정의되는 시점의 스코프다.

function makeCounter() {
  let count = 0;
  return function () {
    count += 1;
    return count;
  };
}
const counter = makeCounter();
counter(); // 1
counter(); // 2

makeCounter는 끝났지만 count는 사라지지 않는다. 반환된 함수가 count를 참조하는 한, 자바스크립트 엔진은 그 변수를 메모리에 남겨둔다. 이게 클로저다. 특별한 문법이 아니라 함수와 렉시컬 스코프의 당연한 결과다.

2-2. for 루프에서 var 로 만드는 흔한 버그

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 출력: 3, 3, 3

기대한 건 0, 1, 2다. 하지만 var는 함수 스코프(또는 전역 스코프)를 가진다. 반복문이 세 번 돌아도 i는 하나뿐인 변수를 계속 재사용한다. setTimeout 콜백 세 개가 모두 같은 i를 클로저로 잡고 있다가, 루프가 다 끝나고 i가 3이 된 뒤에야(콜백은 동기 코드가 다 끝난 뒤 돈다) 실행되니 셋 다 3을 본다.

2-3. let 이 고치는 이유

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 출력: 0, 1, 2

let은 블록 스코프다. 정확히는, for문에서 let을 쓰면 엔진이 반복마다 새로운 바인딩을 만든다. 매 반복이 자기만의 i를 갖게 되고, 클로저는 각자 자기 반복의 i를 가리킨다. 변수 하나를 공유하는 게 아니라 반복 횟수만큼 변수를 새로 만드는 셈이다.

2-4. var 를 써야 한다면

즉시실행함수(IIFE)로 매 반복마다 새 스코프를 강제로 만드는 게 let 이전 시대의 해법이었다.

for (var i = 0; i < 3; i++) {
  (function (j) {
    setTimeout(() => console.log(j), 0);
  })(i);
}

지금은 그냥 let을 쓰면 된다. 이 버그를 알아두면 좋은 이유는, 코드 리뷰에서 오래된 코드나 트랜스파일 결과물을 볼 때 왜 저렇게 IIFE로 감쌌는지 이해할 수 있기 때문이다.


3. 마무리

요약

  • 클로저는 함수가 정의 시점의 스코프를 계속 참조하는 것뿐이다
  • var는 함수 스코프라 반복문에서 변수 하나를 모든 콜백이 공유한다
  • let은 반복마다 새 바인딩을 만들어서 각 콜백이 자기 몫의 값을 가진다
  • 오래된 코드의 IIFE 패턴은 let 없던 시절 이 문제를 우회하던 방법이다

다음은 this 는 누가 정하나 이다.