JavaScript Closure 정리
Closure란
보통 "함수가 리턴한 함수"라고 표현한다. 좀 더 정확하게는 독립적인 변수(free variable)를 참조하는 함수이며, closure 안에 선언된 함수는 선언될 당시의 환경(lexical environment)을 기억한다.
function getClosure() {
var freeVar = 'independent';
return function () {
return freeVar;
};
}
var closure = getClosure();
console.log(closure()); // 'independent'
getClosure는 이미 실행이 끝났지만, 리턴된 익명함수는 여전히 freeVar에 접근할 수 있다. 이 익명함수가 바로 closure이다.
목적 1: 변수 숨기기 (Private Variable)
JavaScript에서 객체지향 프로그래밍은 prototype을 통해 객체를 다루는 방식이다. 그런데 prototype 기반으로 객체를 다룰 때, private variable에 대한 접근 권한 문제가 생긴다.
private variable이란 외부에서 접근이 불가능하고, 값 변경이 불가능한 변수를 말한다. closure를 활용하면 이 문제를 해결할 수 있다.
function Counter() {
var count = 0; // private variable
return {
increment: function () {
count++;
},
getCount: function () {
return count;
},
};
}
var counter = Counter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined (외부에서 직접 접근 불가)
count는 외부에서 직접 접근할 수 없고, increment와 getCount를 통해서만 다룰 수 있다.
목적 2: 반복문에서 변수값 캡처
var i;
for (i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
이 코드는 0~9가 아닌 10이 10번 찍힌다. 흐름은 이렇다:
setTimeout안의 익명함수가 0.1초마다 Task Queue에 쌓인다.- for문이 끝나면
i는 이미 10이 된 상태다. - Call Stack이 비워진다.
- Event Loop를 통해 Task Queue에 있던 콜백들이 Call Stack으로 이동한다.
- 이때
i를 참조하면 이미 10이므로, 10이 10번 출력된다.
IIFE + Closure로 해결
var i;
for (i = 0; i < 10; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 100);
})(i);
}
여기서 사용된 패턴이 IIFE(Immediately Invoked Function Expression), 즉시 실행 함수 표현식이다.
IIFE란
함수를 정의하자마자 바로 실행하는 패턴이다.
(function () {
console.log('즉시 실행');
})();
구조를 분해하면:
(function() { ... })→ 함수를 괄호로 감싸서 표현식으로 만든다.function으로 시작하면 JS 엔진이 "함수 선언문"으로 해석하기 때문에 괄호가 필요하다.()→ 그 표현식을 바로 호출한다.
(i)의 역할
(function (j) {
// j는 이 스코프 안의 private variable
setTimeout(function () {
console.log(j);
}, 100);
})(i); // ← for문의 현재 i 값을 인자로 넘김
(i)는 IIFE를 호출할 때 넘기는 인자다. 풀어쓰면 이렇게 된다:
var myFunc = function (j) {
setTimeout(function () {
console.log(j);
}, 100);
};
myFunc(i); // i를 인자로 넘김
IIFE는 이 두 단계를 한 줄로 합친 것이다. for문의 현재 i 값이 매개변수 j로 복사되어 들어간다. i=0일 때 j=0, i=1일 때 j=1... 각 반복마다 그 시점의 값이 캡처된다. j는 IIFE 스코프 안의 private variable이고, setTimeout의 콜백은 이 j를 참조하는 closure이다.
사용 시 주의사항
1. 메모리 관리
closure는 private variable을 참조하고 있으므로, 해당 변수가 가비지 컬렉션 대상이 되지 않는다. 즉 메모리에 계속 남아있다.
사용이 끝난 closure는 null을 할당하여 참조를 제거해줘야 한다.
function createClosure() {
var heavyData = new Array(1000000).fill('data');
return function () {
return heavyData.length;
};
}
var closure = createClosure();
console.log(closure()); // 1000000
// 더 이상 closure를 사용하지 않는다면
closure = null;
closure = null을 하면 익명함수에 대한 참조가 끊기고, 익명함수가 참조하던 heavyData도 더 이상 참조하는 곳이 없어진다. 그러면 가비지 컬렉터가 메모리를 회수할 수 있게 된다.
2. Scope Chain 검색 비용
closure는 private variable에 접근하기 위해 scope chain을 따라 올라가야 한다. 현재 스코프에 없는 변수를 외부 스코프에서 찾는 과정이므로, 로컬 변수에 바로 접근하는 것보다 추가적인 비용이 발생한다.
정리
- Closure는 선언 당시의 lexical environment를 기억하는 함수다.
- private variable을 만들어 변수를 은닉할 수 있다.
- 반복문에서 IIFE + closure 패턴으로 변수값을 캡처할 수 있다.
- 메모리 누수 방지를 위해 사용이 끝난 closure는
null을 할당하여 참조를 제거한다.