JavaScript에서 클로저는 생성 당시의 외부 환경(lexical scope)를 기억하고 있는 함수와 그 외부 환경을 가리킨다. 일반적인 경우 함수가 생성될 때 함께 생성된 함수의 환경은 함수의 실행이 종료되면서 GC에 의해 메모리에서 제거돼야 한다. 하지만 생성된 함수의 레퍼런스를 함수 외부에서 참조하고 있는 경우에는 함수의 외부 환경이 유지되고 이 때 이 함수와 외부 환경을 클로저라 부르는 것이다.
왜 사용하는가?
정보의 은닉
클로저를 사용하는 주된 이유 중 한가지는 정보의 은닉이다. 클로저 내부의 데이터는 클로저 함수 외에는 접근할 수 있는 방법이 없으므로 데이터를 은닉하기에 적합하다.
아래 코드는 클로저를 이용해 카운터를 만든 예시이다.
function Counter() {
let counter = 0;
const inc = () => ++counter;
const dec = () => --counter;
return {
inc,
dec,
};
}
const counter = Counter();
위 코드는 counter.inc()
와 counter.dec()
를 이용해 counter값을 변경할 수 있지만 다른 방법으로 값을 변경할 수 있는 방법이 없다. 하지만 Counter()
함수를 호출할 때마다 inc와 dec 함수를 매번 새로 생성해줘야 하기 때문에 메모리 관점에서 불리한 면이 있다. 따라서 객체 생성이 빈번한 경우에는 es6의 class나 constructor functions
을 이용하는 것이 좋다. 심지어 Hash name을 이용하면 외부에서 정보의 은닉도 가능하다.
스코프 생성
클로저가 필요한 또 다른 경우는 임의로 스코프를 만들어줘야 하는 경우이다. 아래는 그에대한 아주아주 유명한 예시이다.
for (var i = 0; i < 10; i++) {
setTimeout(() => console.log(i), 100);
}
위 코드는 언뜻 보면 0.1초마다 0~9까지의 숫자를 출력할 것으로 보인다. 하지만 실제로는 10만 10번 출력하는 것을 볼 수 있다. 첫 번쨰 timeout
의 함수가 실행되기도 전에 for
문은 실행이 종료되어 i
값이 10이 되어있기 때문이다. 이는 setTimeout
에 인자로 넘겨주는 함수를 새로운 스코프 안에서 실행시켜주면 된다.
for (var i = 0; i < 10; i++) {
setTimeout(
(
(j) => () =>
console.log(j)
)(i),
100 * i
);
}
하지만 위 경우는 es6에서 블록 스코프가 생긴 이후로 클로저를 쓸 필요가 없어졌다. 단순히 블록 스코프를 갖는 let을 사용하면 원하는대로 동작하는 코드를 작성할 수 있다.
for (let i = 0; i < 10; i++) {
setTimeout(() => console.log(i), 100 * i);
}
결론
글을 쓰면서 느낀 점은 지금같이 es6가 대세가 된 이후로는 꼭 클로저를 사용해야 하는 경우가 이제는 없다는 것이다. 개인적으로는 위의 Counter
같이 아주 간단한 데이터 + 함수의 object
를 생성하는 경우를 제외하고는 클로저를 쓸 일은 없다고 생각한다.
참고 문서
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures