반복문 안에서 만든 콜백이 마지막 값만 출력합니다. var와 let의 클로저 캡처는 왜 다르게 보이나요?
잠깐, 답변을 보기 전에
먼저 내 말로 설명해 볼까요?
답변과 점검 포인트 펼치기답변 접기
구두 답변
정의를 외우기보다, 원리와 조건을 연결해 설명해 보세요.
클로저는 함수를 만들 때의 변수 값을 사진처럼 복사하는 기능이 아니라, 함수가 참조하는 바깥 렉시컬 환경을 계속 사용할 수 있게 하는 구조입니다. 그래서 반복문 변수의 선언 방식이 결과를 바꿉니다.
클로저가 붙잡는 바인딩
const varFns = [];
for (var i = 0; i < 3; i++) varFns.push(() => i);
const letFns = [];
for (let j = 0; j < 3; j++) letFns.push(() => j);
console.log(varFns.map(fn => fn())); // [3, 3, 3]
console.log(letFns.map(fn => fn())); // [0, 1, 2]
var i는 이 코드가 놓인 하나의 상위 환경에 바인딩 하나만 만들고 세 함수가 그 같은 바인딩을 공유합니다. 함수 안에 넣었다면 그 환경이 함수 스코프이고, 일반 스크립트 최상위라면 전역 환경, ES module 최상위라면 모듈 환경입니다. 반복이 끝난 뒤 그 바인딩의 값이 3이므로 나중에 콜백을 호출하면 모두 3을 읽습니다. 반면 for의 let j는 반복마다 별도의 환경과 바인딩을 제공하므로 각 콜백이 자기 반복의 j를 참조합니다. 핵심은 클로저가 현재 값을 복제하는지가 아니라 어떤 바인딩을 공유하는지입니다.
반복 변수와 리스너 수명
var를 꼭 써야 한다면 반복마다 값을 함수 인자로 넘겨 새 매개변수 바인딩을 만드는 방식으로 고정할 수 있습니다. 다만 현대 JavaScript에서는 이 목적에 let을 쓰는 것이 단순합니다. 콜백을 즉시 실행하는 코드와 나중에 실행하는 코드를 구분해야 하며, 클로저가 큰 객체나 이벤트 대상까지 계속 참조하면 작업이 끝난 뒤에도 메모리가 유지될 수 있으므로 수명도 함께 관리하겠습니다.
선택 기준과 검증
반복별 let 바인딩은 숫자 바인딩을 분리할 뿐, 그 바인딩이 가리키는 객체를 깊게 복사하지는 않습니다. 따라서 각 콜백이 같은 설정 객체를 읽는다면 별도의 공유 상태 문제가 남습니다. 이벤트 리스너는 함수 참조를 보관해 제거하고, 최상위 var 예제는 함수 스코프라고 단정하지 않겠습니다. 일반 스크립트에서는 전역 환경의 바인딩을, ES module에서는 모듈 환경의 바인딩을 반복 전체가 공유하며, var의 함수 스코프는 함수 내부에서 적용됩니다.
바인딩을 캡처한다는 의미는 let n=0을 읽는 함수를 만든 뒤 n을 5로 바꾸면 그 함수가 5를 읽는다는 것입니다. 함수 생성 당시의 스냅샷이 필요하면 별도 매개변수나 불변 값으로 전달해야 합니다. const 객체도 바인딩 재대입만 막으므로 객체 속성 변경은 다른 클로저에서 보일 수 있습니다. 변수 선언과 데이터의 불변성을 같은 개념으로 설명하지 않겠습니다.
이벤트 리스너를 해제할 때는 등록에 사용한 함수 객체를 다시 전달해야 합니다. 같은 소스의 화살표를 새로 만들어 removeEventListener에 넘기면 다른 객체여서 제거되지 않습니다. once나 AbortSignal을 사용할 수 있는 이벤트 API에서는 수명을 명시하고, 오래 살아 있는 큐가 캡처한 DOM·배열을 붙잡는 경로를 heap snapshot으로 확인하겠습니다. 클로저 자체가 누수인 것이 아니라 더 이상 필요 없는 소유 경로가 남았는지가 판단 기준입니다.
득점 포인트
이해가 드러나는 지점을 확인해 보세요.
- 클로저가 값이 아닌 바인딩을 보존함을 설명한다.
- var 단일 바인딩과 let 반복별 바인딩을 대비한다.
- 콜백 제거와 캡처 객체의 수명까지 확인한다.
감점 포인트
놓치기 쉬운 조건과 오해를 짚어 보세요.
- 클로저가 생성 순간의 값을 항상 복사한다고 말한다.
- let이 전역 스코프라서 반복 결과가 다르다고 설명한다.
- 리스너를 제거하지 않아도 클로저가 객체를 계속 보유하지 않는다고 말한다.
더 파고들 거리
하나 더 질문하며 이해의 범위를 넓혀 보세요.
- forEach 매개변수와 for 문의 let은 callback마다 어떻게 다른 바인딩을 만들며 객체 공유는 어디에 남나요?
- 클로저가 DOM을 보유해 생기는 누수를 어떤 리스너 수명으로 줄일까요?
- 클로저의 private 상태와 class private field를 테스트합니다. 내부 표현을 고정하지 않고 어떤 외부 계약을 검증하나요?
득점·감점 포인트는 학습용 자기 점검 기준이며, 특정 회사의 실제 채점표가 아닙니다.
원리부터 공부하고 싶다면
답변을 보충하는 개념·예제·슈도코드를 읽어 보세요.