클로저는 프론트엔드 기술면접을 준비하다 보면 단골로 등장하는 주제다. 그만큼 중요하기도 하지만 헷갈리기도 한다.
MDN Web Docs에선 다음과 같이 정의하고 있다.
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.
클로저는 함수와 그 주위의 상태(어휘적 환경)의 조합이다. 다시 말해, 클로저는 내부 함수에서 외부 함수의 스코프에 접근할 수 있게 한다. 자바스크립트에서 클로저는 함수가 만들어질 때마다 함수 생성 시점에 생성된다.
간단하게 말해서 함수가 자신이 생성된 당시의 어휘적 환경(Lexical Scope)을 기억하고 있는 것이다. 이 때, 어휘적 환경에 자신의 스코프뿐만 아니라 외부 함수의 스코프도 포함되는데, 이에 따라서 자연스럽게 외부 함수의 변수에도 접근할 수 있게 되는 것이다.
클로저는 주로 프라이빗 메소드를 흉내내기 위해 사용한다. 아래의 예제를 보자.
const counter = (() => {
let privateCounter = 0;
const changeBy = (val) => {
privateCounter += val;
};
return {
increment: () => {
changeBy(1);
},
decrement: () => {
changeBy(-1);
},
value: () => {
return privateCounter;
},
};
})();
console.log(counter.value()); // 0.
counter.increment();
counter.increment();
console.log(counter.value()); // 2.
counter.decrement();
console.log(counter.value()); // 1.
counter 함수 안의 privateCounter과 changeBy는 반환된 함수들로만 접근할 수 있어 private member처럼 작동하게 된다. 반환된 함수들은 public method처럼 작동하게 된다.
여기서 코드를 조금 바꿔보자.
const makeCounter = () => {
let privateCounter = 0;
const changeBy = (val) => {
privateCounter += val;
};
return {
increment: () => {
changeBy(1);
},
decrement: () => {
changeBy(-1);
},
value: () => {
return privateCounter;
},
};
};
const counter1 = makeCounter();
const counter2 = makeCounter();
console.log(counter1.value()); // 0.
counter1.increment();
counter1.increment();
console.log(counter1.value()); // 2.
counter1.decrement();
console.log(counter1.value()); // 1.
console.log(counter2.value()); // 0.
같은 makeCounter를 이용해서 counter1과 counter2를 만들었다. 하지만 counter1의 public method를 이용해 privateCounter를 변경해도 counter2의 privateCounter는 변하지 않는다. 이것을 통해 위에서 이야기한 '자신이 생성된 당시의 어휘적 환경'을 기억한다는 것이 어떤 의미인지 알 수 있다.