클로저가 가장 처음 등장했을 때의 개념으로 판단하자면 자바스크립트의 모든 함수는 클로저이다. 함수를 만들고 그 함수 내부의 코드가 탐색하는 스코프를 함수 생성 당시의 lexical scope로 고정하면 바로 클로저가 되는 것이기 때문이다.
그러나 실제로 자바스크립트의 모든 함수를 전부 클로저라고 부르지는 않는다.
일반적으로 외부 접근을 제한하는 비공개 변수를 가질 수 있는 환경에 있는 경우를 클로저라고 한다. 그러한 경우에 이전 편에서 언급한 클로저를 사용하는 이유에 맞게 활용이 가능하기 때문이다.
클로저의 용도 요약 : 정보 은닉과 캡슐화
var counter = (function() {
var privateCounter = 0; ┐
function changeBy(val) { │ increment, decrement, value
privateCounter += val; │ 세 메소드가 공유하는 private items
} ┘
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
};
})();
console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1
익명함수가 즉시 실행되며 counter
변수에 함수의 반환 값이 할당된다.
익명함수의 반환 값인 세 함수는 동일한 lexical environment를 공유하고 있으며 privateCounter
와changeBy()
에 유일하게 접근할 수 있는 클로저이다.
counter.increment
counter.decrement
counter.value
privateCounter
changeBy
IIFE대신 기명함수를 이용하면 독립성을 유지하는 다수의 카운터를 생성할 수 있다.
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var counter1 = makeCounter();
var counter2 = makeCounter();
alert(counter1.value()); /* 0 */
counter1.increment();
counter1.increment();
alert(counter1.value()); /* 2 */
counter1.decrement();
alert(counter1.value()); /* 1 */ ┐ counter1과 counter2는
alert(counter2.value()); /* 0 */ ┘ 서로 다른 독립된 클로저를 가진다.
각 클로저는 그들 고유의 클로저를 통한 privateCounter
변수의 다른 버전을 참조한다.
하나의 클로저에서 변수 값을 변경해도 다른 클로저의 값에는 영향을 주지 않는다.
Currying은 여러 개의 인자를 받는 함수를 단일 인자를 받는 함수의 체인을 이용하는 방식으로 바꾸는 것을 의미한다. 기명함수를 이용한 위의 예시에 currying을 접목해 클로저 함수의 외부 함수를 템플릿처럼 활용할 수 있다.
function greetCurried (greeting) {
return function (name) {
console.log(greeting + ", " + name);
}
}
greetCurried("Hi there")("Howard"); //"Hi there, Howard"
let greetHello = greetCurried("Hello");
greetHello("Heidi"); //"Hello, Heidi"
greetHello("Eddie"); //"Hello, Eddie"
let greetGoodmorning = greetCurried("Good morning");
greetGoodmorning('wendy'); // Good morning, wendy
greetGoodmorning('honey'); // Good morning, honey
클로저에 대한 이해가 부족한 상태라면 반복문과 클로저가 함께 쓰였을 때 예상과 다른 결과가 발생하게 된다. 이전 편에 첫번째로 제시되었던 문제를 다시 가져왔다.
function count() {
var i;
for (i = 0; i < 5; i++) {
console.log(i)
setTimeout(function() {
console.log(i)
}, i*1000)
}
}
count();
작성자의 의도 💭 : 0,1,2,3,4가 1초 간격으로 출력되겠군 !
실제 출력결과 : 5,5,5,5,5
setTimeout뿐 아니라 이벤트 리스너 등 반복문 순회 이후 실행될 수 있는 클로저 함수는 유의해야 한다. 의도대로 코드가 작동하게 하려면 아래와 같은 방법으로 해결할 수 있다.
원리
setTimeout함수 바깥으로 새로운 스코프를 만들고 IIFE로i
를 인자로 넘긴다.
i
증가 시마다 고정된countingNumber
에 대한 독립적인 클로저 함수가 만들어진다.
독립된 lexical environment를 가지기에 각 시점의countingNumber
에 접근하여 출력할 수 있다.
function count() {
var i;
for (i = 0; i < 5; i++) {
(function(countingNumber) {
setTimeout(function() {
console.log(countingNumber)
}, i*1000)
})(i);
}
}
count(); //0,1,2,3,4
(+) setTimeout이 아닌 경우 새로운 스코프를 만들지 않고 IIFE를 적용하는 것만으로 문제가 해결되기도 한다. 그러나 setTimeout은 콜백으로 넘기는 함수 자체에 IIFE를 적용하면 두번째 인자로 주어지는 딜레이가 작동하지 않게 되므로 바깥에 새로운 스코프를 만들어 적용할 수 밖에 없다.
원리
var는 함수 수준, let은 블록 수준의 스코프를 가진다.
즉 let키워드가 사용된 블록에 대해 lecxical envireonment가 새롭게 생성된다.
반복문이 실행될 때마다 클로저가 생성되므로 i는 각 시점의 i에 접근하여 증가하는 숫자를 출력할 수 있다.
function count() {
'use strict';
for (let i = 0; i < 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}
}
count(); //0,1,2,3,4
참고자료
JavaScript 클로저(Closure)
자바스크립트 공부 // 스코프(Scope), 클로저(Closure), 즉시실행함수(IIFE)