클로저는 독립적인 (자유) 변수를 가리키는 함수이다. 또는, 클로저 안에 정의된 함수는 만들어진 환경을 '기억한다'.
어휘의 범위
다음 함수를 생각해보자.
함수 init()
은 지역 변수 name
과 함수 displayName()
을 정의한다. displayName()
은 함수 init()
안에 정의되어 그 함수(init()
) 안에서만 사용할 수 있는 내부 함수이다. 함수 displayName()
자신은 지역변수를 가지지 않지만 외부 함수에 정의된 변수에 접근하는 권한이 있어 부모 함수에 있는 변수 name
을 사용할 수 있다.
코드를 한번 실행해보라. 잘 동작할 것이다. 이 예제는 함수 스코핑(functional scoping) 을 보여주기 위해 소개했다. 자바스크립트에서 중첩된 함수는 그 함수 외부에서 정의된 변수를 사용할 수 있다.
클로저
이제 또 다음 예제를 보자.
function makeFunc() { var name = "모질라"; function displayName() { alert(name); } return displayName; } var myFunc = makeFunc(); myFunc();
이 코드를 실행하면 앞서 예로 들은 init()
함수와 동일한 결과를 보이는걸 알 수 있다(알람창에 "모질라" 문자열이 보일 것이다). 위 예제와 다른 점, 그리고 흥미로운 점은 외부함수의 리턴 값이 내부함수 displayName()
라는 것이다.
이 코드가 여전히 작동하는 것은 직관적이지 않다. 일반적으로 함수안에 정의된 지역변수는 함수가 실행하는 동안에만 존재한다. makeFunc()
함수가 종료될 때 이 함수 내부에 정의된 지역변수는 없어지는게 상식적이다. 이 코드가 문제없이 동작하는 걸 보면 다른 일이 일어나고 있는 것 같다!
이것은 myFunc
함수가 클로저이기 때문이다. 클로저는 두 개의 것(함수, 그 함수가 만들어진 환경)으로 이루어진 특별한 객체의 한 종류이다. 환경이라 함은 클로저가 생성될 때 그 범위 안에 있던 여러 지역 변수들로 이루어진다. 이 경우에 myFunc
는 displayName
함수와 "모질라
" 문자열을 포함하는 클로저이다.
조금 더 흥미로운 예제를 보자. makeAdder
라는 함수이다.
function makeAdder(x) { return function(y) { return x + y; }; } var add5 = makeAdder(5); var add10 = makeAdder(10); print(add5(2)); // 7 print(add10(2)); // 12
여기서 인자 x
를 받아 새 함수를 반환하는 makeAdder(x)
라고 하는 하나의 인자를 받는 함수를 만들었다. 반환되는 함수는 인자 y
를 받아서 x 값과 y 값의 합을 돌려주는 함수이다.
본질적으로 makeAdder
는 함수 공장(자신의 인자에 특정 값을 더하는 함수들을 만들어내는 것을 말한다)이다. 위의 예제에서 두개의 함수를 찍어냈다. 첫째는 인자에 5를 더하는 함수이고 둘째는 인자에 10을 더하는 함수이다.
add5
와 add10
은 둘다 클로져이다. 두 함수는 같은 정의를 가지지만 다른 환경을 저장한다. add5
의 환경에서 x는 5이지만 add10
의 환경에서 x는 10이다.
실용적인 클로저
여기까지는 이론이었다. 클로저는 실용적인가? 이제는 실용적인 사용 방법을 알아보자. 어떤 데이터(환경)와 함수를 연관시키는데 클로저를 사용할 수 있다. 이건 객체지향 프로그래밍과 유사하다. 객체지향 프로그래밍에서는 객체가 데이터(그 객체의 속성)와 하나 이상의 메소드를 연관시킨다.
결론적으로 오직 하나의 메소드를 가지고 있는 오브젝트를 사용하는 곳에 일반적으로 클로저를 사용할 수 있다.
웹 프로그래밍에서 이런 일이 많이 일어난다. 많은 자바스크립트 코드가 이벤트를 기반으로 짜여진다. (특정한 동작을 만들고 클릭이나 키보드 누르기에 이 동작을 연결시킨다) 이벤트에 반응하는 코드를 만든다고 할 수 있겠다. 이런 코드들을 콜백(callback)이라고 부른다.
여기에 실용적인 예제가 있다. 페이지의 글자 크기를 조정하는 몇 개의 버튼을 만든다고 생각해보자. body 엘리먼트에 px단위로 font-size를 설정하고 다른 엘리먼트에서는 상대적인 em 단위로 font-size를 설정하면 되겠다.
body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; } h1 { font-size: 1.5em; } h2 { font-size: 1.2em; }
이제 body 엘리먼트의 font-size만 바꾸면 font-size가 em단위로 설정된 다른 엘리먼트들의 글자 크기도 바뀔 것이다.
여기 자바스크립트 코드가 있다.
function makeSizer(size) { return function() { document.body.style.fontSize = size + 'px'; }; } var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16);
size12
, size14,
size16
은 body 엘리먼트의 글자 크기를 각각 12, 14, 16 픽셀로 바꾸는 함수이다. 이제 이 함수를 버튼과 연결시키자.
document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a> <a href="#" id="size-14">14</a> <a href="#" id="size-16">16</a>
클로저를 이용해서 private 함수 흉내내기
몇몇 언어(예를들어 자바)는 같은 클래스 내부의 메소드에서만 호출할 수 있는 private 메소드를 지원한다.
자바스크립트는 이를 지원하지 않지만 클로저를 이용해서 흉내낼 수 있다. private 함수는 코드에 제한적인 접근만을 허용한다는 점 뿐만 아니라 전역 네임스페이스를 깔끔하게 유지할 수 있다는 점에서 중요하다.
아래에 모듈 패턴이라고 알려진 클로저를 통해 몇 개의 public 함수가 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
이전 예제에서는 각 클로저가 자기만의 환경을 가졌지만 이 예제에서는 하나의 환경을 counter.increment
, counter.decrement,
counter.value
세 함수가 공유한다.
공유되는 환경은 정의되자마자 실행되는 익명 함수 안에서 만들어진다. 이 환경에는 두 개의 private 아이템이 존재한다. 하나는 privateCounter
라는 변수이고 나머지 하나는 changeBy
라는 함수이다. 이 두 아이템 모두 익명함수 외부에선 접근할 수 없다. 하지만 익명함수 안에 정의된 세개의 public 함수에서 사용되고 반환된다.
이 세개의 public 함수는 같은 환경을 공유하는 클로저이다. 자바스크립트 문법적 스코핑(lexical scoping) 덕분에 세 함수 모두 privateCounter
변수와 changeBy
함수에 접근할 수 있다.
익명 함수가 카운터를 정의하고 이것을 counter
변수에 할당한다는 걸 알아차렸을 것이다. 이 함수를 다른 변수에 저장하고 이 변수를 이용해 여러개의 카운터를 만들수도 있다.
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 */ alert(Counter2.value()); /* 0 */
두개의 카운터가 어떻게 독립적으로 존재하는지 주목하라. makeCounter()
함수를 호출하면서 생긴 환경은 호출할 때마다 다르다. 클로져 변수 privateCounter 는 다른 인스턴스를 가진다.
객체지향 프로그래밍을 사용할 때 얻는 이점인 정보 은닉과 캡슐화를 클로저를 사용함으로써 얻을 수 있다.
자주하는 실수: 반복문 안에서 클로저 만들기
자바스크립트 1.7의 let
키워드 가 도입되기 이전에는 반복문 안에서 클로저를 생성해서 문제가 되는 경우가 빈번했다. 다음 예제를 보자.
<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>
function showHelp(help) { document.getElementById('help').innerHTML = 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 = function() { showHelp(item.help); } } } setupHelp();
helpText
배열은 세개의 도움말을 정의한다. 각 도움말은 입력 필드의 ID와 연관된다. 이 세개의 정의를 반복하며 입력필드에 onfocus 이벤트가 발생했을 때 입력필드에 해당하는 도움말을 표시한다.
이 코드를 실행해보면 제대로 동작하지 않는다는 것을 알 수 있다. 어떤 필드에 포커스를 주더라도 나이에 관한 도움말이 표시된다.
이유는 onfocus 이벤트에 지정한 함수가 클로저라는 것이다. 이 클로져는 함수 본체와 setupHelp
함수의 스코프로 이루어져 있다. 세개의 클로져가 만들어졌지만 각 클로저는 하나의 환경을 공유한다. 반복문이 끝나고 onfocus 콜백이 실행될 때 콜백의 환경에서 item 변수는 (세개의 클로저가 공유한다) helpText
리스트의 마지막 요소를 가리키고 있을 것이다.
여러개의 클로저를 이용해서 문제를 해결할 수 있다. 위에서 언급한 함수 공장을 사용해보자.
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();
예상한대로 작동한다. 콜백이 하나의 환경을 공유하지 않고 makeHelpCallback
함수가 만든 새로운 환경을 가진다. 이 환경에는 helpText
배열로부터 해당하는 문자열이 help
변수에 담겨있다.
추가로 원문에는 없지만 makeHelpCallback 함수를 이용하지 않고 즉시 실행 함수를 이용하면 아래와 같다.
function showHelp(help) { document.getElementById('help').innerHTML = 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 = (function(help) { return function() { showHelp(help); } })(item.help); } } setupHelp();
성능과 관련해서
클로져가 필요하지 않은 작업인데도 함수안에 함수를 만드는 것은 스크립트 처리 속도와 메모리 사용량 모두에서 현명한 선택이 아니다.
예를들어 새로운 오브젝트나 클래스를 만들 때 오브젝트 생성자에 메쏘드를 정의하는 것 보다 오브젝트의 프로토타입에 정의하는것이 좋다. 오브젝트 생성자에 정의하게 되면 생성자가 불릴때마다 메쏘드가 새로 할당되기 때문이다.
비현실적이지만 설명을 위해 예제를 첨부했다.
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }
위의 코드는 일일히 메쏘드를 만들면서 클로져의 이점을 살리지 못하고 있다.
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype = { getName: function() { return this.name; }, getMessage: function() { return this.message; } };
또는 다음처럼 하자
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype.getName = function() { return this.name; }; MyObject.prototype.getMessage = function() { return this.message; };
위의 두 예제에서는 상속된 속성은 모든 오브젝트에서 사용될 수 있고 메쏘드 정의가 오브젝트가 생성될 때마다 일어나지 않는다. 오브젝트 모델에 대한 자세한 설명을 참고하라.