1. 개요
클로저는 “함수가 자신이 정의된 스코프를 기억한다”는 것뿐인데, 실전에서 헷갈리는 건 대부분 딱 하나의 패턴에서 나온다. 반복문 안에서 비동기 콜백을 만들 때다. var로 선언한 반복 변수가 클로저에 잡히면 예상과 다른 값이 찍힌다. 이 글은 클로저가 뭔지 짧게 정리하고, 그 버그가 왜 나는지, let이 왜 고치는지를 본다.
2. 핵심 내용
2-1. 함수는 태어난 스코프를 기억한다
함수를 정의하면 그 함수는 자기가 정의될 당시 접근 가능했던 변수들의 참조를 계속 들고 있는다. 함수가 실행되는 시점이 아니라 정의되는 시점의 스코프다.
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2makeCounter는 끝났지만 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, 2let은 블록 스코프다. 정확히는, 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 는 누가 정하나 이다.