클로저는 mdn 사이트에 따르면 함수와 함수가 선언된 어휘적 환경의 조합이라고 한다. 정의만 봐서는 정말 무슨말인지 이해하기가 어렵다. 실제로 클로저 관련 글을 쓰기 위해 공부하면서도 감은 오는거 같은데, 이게 뭐지? 싶은 느낌이 많이 온다.
mdn 사이트를 기본으로 다른 블로그에서도 클로저에 대해 찾아보는데, 결국 클로저라는 개념을 본인이 이해할 수 있는 개념으로 정립하는게 중요하다고 한다.
function init() {
var name = "Mozilla"; // name은 init에 의해 생성된 지역 변수이다.
function displayName() { // displayName() 은 내부 함수이며, 클로저다.
alert(name); // 부모 함수에서 선언된 변수를 사용한다.
}
displayName();
}
init();
init 을 실행하면 init은 지역 변수 name과 함수 displayName()을 생성한다. displayName()은 init 함수 내부에서만 사용할 수 있지만 init() 내에서 displayName() 기준의 외부 변수인 name은 displayName() 내부에서 사용할 수 있다.
즉 외부에서는 내부 변수에 접근할 수 없지만 내부에서는 외부 변수에 접근할 수 있다.
이를 통해 함수와 변수의 범위가 어떻게 지정되는지 알 수 있다.
if (Math.random() > 0.5) {
var x = 1;
} else {
var x = 2;
}
console.log(x);
여기서 var x = 1; 은 전역적으로 선언이 된다. if 중괄호 블록이 스코프를 생성하지 않기 때문에 헷갈리지만 위 코드는 정상적으로 동작하여 x를 출력한다.
if (Math.random() > 0.5) {
const x = 1;
} else {
const x = 2;
}
console.log(x); // ReferenceError: x is not defined
const로 지정하면 const가 if 블록스코프 내부에 생성되기 때문에 에러가 발생한다.
function makeAdder(x) {
var y = 1;
return function(z) {
y = 100;
return x + y + z;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
//클로저에 x와 y의 환경이 저장됨
console.log(add5(2)); // 107 (x:5 + y:100 + z:2)
console.log(add10(2)); // 112 (x:10 + y:100 + z:2)
//함수 실행 시 클로저에 저장된 x, y값에 접근하여 값을 계산
위 코드에서
var add5 = makeAdder(5);
var add10 = makeAdder(10);
return function(z) {
y = 100;
return x + y + z;
};
이 부분에서 makeAdder의 x값이 5, 10으로 각각 두 개의 클로저가 생성된다.
내부에서 x가 클로저에 있는 x를 기억했다가 y, z를 더해서 리턴해준다. 즉 클로저가 add5, add10 에 할당되어 리턴된 이 후에도 외부 함수의 변수에 접근이 가능하다는 것을 의미한다.
자바의 경우 private 선언자를 통해 불필요한 변수, 메소드가 다른 곳에서 쓰이는 것을 방지할 수 있다. 자바스크립트에는 이러한 방식이 없지만 클로저를 통해 제한적인 접근을 구현할 수 있다.
이를 모듈 패턴이라고 한다.
var counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
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
위처럼 increment, decrement, value 를 지정하여 리턴하고 각 함수들이 privateCounter 라는 외부변수에 접근할 수 있도록 한다.
이렇게 하면 아래 counter 변수를 통해서는 privateCounter 에 접근할 수 없고 values() 메소드를 통해서만 접근할 수 있게 된다.
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email" /></p>
<p>Name: <input type="text" id="name" name="name" /></p>
<p>Age: <input type="text" id="age" name="age" /></p>
<script>
function setupHelp() {
function showHelp(help) {
document.getElementById("help").innerHTML = help;
}
var helpText = [
{ id: "email", help: "Your e-mail address" },
{ id: "name", help: "Your full name" },
{ id: "age", help: "Your age (you must be over 16)" },
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function () {
showHelp(item.help);
};
}
}
setupHelp();
</script>
위 코드는 각 인풋에 focus 할 때마다 주의사항 문구가 변경된다. 실제로 실행해보면 email, name, age에 따라 주의사항이 바뀌어야 하지만 실제로는 age의 주의사항 문구만 띄우는 것을 볼 수 있다.
이유는
document.getElementById(item.id).onfocus = function () {
showHelp(item.help);
};
여기서 function() { 으로 클로저가 세 번 생성이 되는데, var 를 통해 선언한 helpText[i] 를 모든 클로저가 공유하기 때문이다. 즉 외부변수를 기억해두는 클로저의 특성 때문에 배열 마지막인 age만의 값을 띄우게 된다.
다시 설명하자면 onfocus 에 = 로 선언된 함수가 클로저이기 때문이다.
var add5 = makeAdder(5); 위에서 살펴봤던 이 코드를 보자 makeAdder(5) 의 파라미터 5가 add5의 변수의 클로저 안에 갇히게(?) 된다.
따라서 마지막에 선언하게 되는 배열 맨 마지막 값인 age의 값만 보이게 되는 것이다.
가장 간단한 해결 방법으로는 var로 선언한 item을 let으로 선언해주면 각 블록이 각 클로저에 바인딩되어 원하는 결과를 볼 수 있다. 아니면
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
}
setupHelp();
위와 같이 더 많은 클로저를 사용할 수 도 있다. 변수를 한 번 더 클로저로 감싸는 것이다. item.help 값이 makeHelpCallback 안에 클로저로 들어가서 showHelp 를 호출하므로 원하는 동작을 볼 수 있다.
<script>
function setupHelp() {
function showHelp(help) {
document.getElementById("help").innerHTML = help;
}
var helpText = [
{ id: "email", help: "Your e-mail address" },
{ id: "name", help: "Your full name" },
{ id: "age", help: "Your age (you must be over 16)" },
];
for (var i = 0; i < helpText.length; i++) {
(function() {var item = helpText[i];
document.getElementById(item.id).onfocus = function () {
showHelp(item.help);
};})()
}
}
setupHelp();
</script>
위 처럼 즉시실행함수로 선언해주어도 된다. var item = 으로 선언된 코드들이 각각 클로저에 바인딩되어 따로따로 실행된다.