Closure

>> 예시 출력란



jQuery에서의 클로저

jQuery 라이브러리를 사용하면 봐온 메서드는 종종 하나 이상의 함수를 인자로 가진다. 편의를 위해 이때는 필요한 곳에 바로 함수를 정의할 수 있는 익명 함수를 사용할 때가 잦았다. 그러므로 최상위 네임스페이즈에는 함수가 거의 존재하지 않게 된다. 대부부 함수는 내부 함수며 아주 쉽게 클로저를 만들게 된다.



$(document).ready()로 인자 전달하기

jQuery를 사용하는 그대로 코드는 $(document).ready()의 인자로 사용되는 함수에 있게 된다. 이런 방식은 코드가 실행되기 전에 DOM 로드가 완료됐다는 것을 보장받기 위해 사용된다. 이는 jQuery 코드를 실행하기 위한 필요조건이다. 함수가 만들어지고 .ready()로 전달될 때 그 함수에 대한 참조는 전역 jQuery 객체에 저장돼 나중에 DOM이 준비되면 호출된다.

보통 $(document).ready()는 코드의 구조상 가장 상위에 존재한다. 이 때문에 실제로 이 함수는 클로저에 포함되지 않는다. 하지만 보통 이 함수에 코드를 작성하므로 이 함수에 있는 함수는 모두 내부 함수다.

							$(document).ready(function(){
								var readyVar = 0;
								function innerFn () {
									readyVar ++;
									$.print('readyVar = '+readyVar);
								}
								innerFn();
								innerFn();
							});			
						

지금까지 대부분의 코드와 마찬가지로 이 코드 역시 외부 함수가 $(document).ready()로 전달되는 콜백 함수라는 것을 제외하면 앞에서 다른 전역변수 예제와 거의 같다(?? 외부함수 변수 예제와 같은거 아닌가?? - 오류 확인). innerFn()가 내부에 정의돼 있고, 콜백 함수 범위에 있는 readyVar를 참조하므로 innerFn()과 그 환경은 클로저를 생성하게 된다. 함수 호출 사이에 readyVar의 값이 유지된다는 점으로 이 사실을 확인할 수 있다.

							readyVar = 1				
							readyVar = 2				
						

대부분의 jQuery 코드를 함수의 내부에 구현하고 있다는 사실이 때로는 유용할 수 있다. 이런 방식은 다른 네임스페이스에 있는 코드와 이름이 출돌하는 것을 방지할 수 있기 때문이다. 이런 특징 덕분에 다른 라이브러리와의 충돌을 방지하기 위해 jQury.noConflict() 기능을 사용해 $에 대한 단축기능을 해지한 후에도 여전히 $(document).ready() 에서는 $에 대한 단축 기능을 내부적으로 선언해 사용할 수 있다.



이벤트 핸들러

보통 $(document).ready()는 이벤트 핸들러 등록을 포함해서 코드의 나머지 부분을 포함한다. 핸들러가 함수이므로 내부 함수라고 할 수 있으며 이런 내부 함수는 저장된 후 나중에 호출되게 된다. 그리고 이 함수로 클로저를 구성할 수 있다. 클랙 핸들러의 간단한 예는 다음과 같다.

							$(document).ready(function(){ // 외부함수 이자 전역 함수?
								var counter = 0;
								$("#button-1").click(function () {  // 내부함수
									counter++;
									$.print('counter = '+ counter);
									return false;
								});
							});			
						

변수 counter가 .ready() 핸들러에 선언됐으므로 이 변수는 오로지 이 블록 안의 jQuery 코드만 사용할 수 있으며 밖에서는 사용할 수 없다. 하지만 click 핸들러의 코드에서는 참조될 수 있다. 클릭 핸들러는 변수의 값을 증가시키고 그 값을 출력하고 있다. 클로저가 생성됐으므로 버튼을 클릭할 때마다 같은 counter 인스턴스가 매번 참조된다. 그러므로 매번 1이 출력되는 게 아니라 꾸준히 증가하는 값이 출력된다.

							counter = 1				
							counter = 2				
							counter = 3				
						

다른 함수처럼 이벤틑 핸들러 역시 이벤트 핸들러 사이에 변수 범위를 공유할 수 있다.

두 개의 함수 모두 같은 변수를 참조하므로 두 버튼의 증가와 감소 연산은 독립적이지 않으며 같은 변수의 값에 영향을 미게 된다.

							counter = 1				
							counter = 2				
							counter = 1				
							counter = 0				
						


반복문에서 핸들러 연결하기

클로저의 동작 방식으로 반복문 구조는 까다로운 문제를 야기할 수 있다. 다음과 같이 어떤 반복문에서 반복문의 인덱스에 기반을 둬 요소를 생성하고 그 요소에 동작을 연결하다고 가정해 보자.

						$(document).ready(function(){
							for (var i=0; i<5; i++) {
								$('<div>print '+i+'</div>')
								.click(function () {
									$.print(i);
								}).insertBefore('#result');
							}
						});			
						

변수 i는 0부터 4까지 증가하고 매번 새 <div> 요소가 생성되며 예상과 같이 각 요소는 서로 고유한 텍스트 라벨을 가진다.

							print 0
							print 1
							print 2
							print 3
							print 4		
						

하지만 이들 항목 중 하나를 클릭하면 기대했던 항목 라벨의 숫자가 아닌 5가 출력된다. 각 click 핸들러가 참조하는 변수 i는 모두 같기 때문이다. 핸들러가 등록될 때 i의 값은 다르지만 결국 i라는 같은 변수를 사용하므로 클릭했을 때는 i의 마지막 값(5)를 얻게 된다.

다양한 방법으로 이 문제를 해결 할 수 있다. 우선, for 반복문을 jQuery의 $.each() 함수로 대체할 수 있다.

							$(document).ready(function(){
								$.each([0,1,2,3,4], function (index, value) {
									$('<div class="print">Print '+value+'</div>')
									.click(function () {
										$.print(value)
									}).insertBefore('#result');
								});
							});		
						

함수의 인자는 함수에서 선언된 변수와 같다. value 변수는 사실 반복문이 진행되는 동안 매번 다른 변수다. 이 때문에 각 click 핸들러는 다른 value 변수를 가리키고 의도했던 것처럼 요소를 클릭하면 그에 대응하는 요소의 라벨 숫자가 출력된다.

또한, 이 문제를 해결하기 위해 $.each()를 호출하지 않고 위와 같은 함수 인자의 특성을 이용할 수 있다. 다음과 같이 i를 서로 구분되는 별개의 변수로 나눠 주는 새로운 함수를 for 반복문에 정의하고 실행하는 것이다.

							$(document).ready(function(){
								for (var i=0; i<5; i++) {
									(function (value) {
										$('<div class="print2">Print '+value+'</div>')
										.click(function () {
											$.print(value)
										}).insertBefore('#result');										
									})(i);
								}
							});	
						

우리는 이미 즉석 호출 함수 표현식(IIFE, immediately invoked function expression)이라는 이 구조를 본적이 있다. 이전에 $.noConflict()를 호출한 후 $를 jQuery에 대한 단축 구분으로 다시 지정할 때 사용했다. 여기서는 이 방식을 i를 value라는 이름의 인자로 넘겨 각 핸들러를 구분하는 데 사용한다.

마지막으로 이 문제를 조금 다른 방식으로 해결하기 위해 jQuery 이벤트 시스템의 한 기능을 이용할 수 있다. .bind() 메서드는 임의의 객체를 인자로 받을 수 있으며 이 객체는 event.data로 이벤트 핸들러에 전달된다.

							$(document).ready(function(){
								for (var i=0; i<5; i++) {
									$('<div class="print2">Print '+i+'</div>')
									.bind('click', {value: i}, function (event) {
										$.print(event.data.value);
									}).insertBefore('#result');
								}
							});	
						

이때 i는 .bind() 메서드에 데이터로 제공돼 핸들러 내부에서 event.data.value로 값을 얻을 수 있다. 다시 한번 언급하자면 event는 함수의 인자로써, 매번 핸들러가 실행될 때 마다 고유한 객체가 생성되므로 다른 핸들러와 공유되지 않는다.




기명함수와 익명함수

이 예제는 jQuery 코드에서 지금까지 해왔듯이 익명함수를 사용했다. 함수에 이름이 있든 없든 간에 클로저가 만들어지는 것은 같다. 예를 들어 다음과 같이 jQuery 객체에서 아이템의 인덱스를 알려주는 익명함수를 작성할 수 있다.

							$(document).ready(function(){
								$("input").each(function(index) {
									$(this).click(function() {
										$.print('index = '+ index);
										return false;
									});
								});
							});	
						

가장 안쪽의 함수가 .each() 콜백 함수 안에 정의돼 있으므로 이 코드는 실제로 리스트 아이템의 수만큼의 함수를 생성한다. 각 함수는 버튼에 click 핸들러를 추가하고 있으며, index는 .each() 콜백 함수로 전달되는 인자이므로 각 닫혀진 환경에 index를 가지고 있다. 이 함수는 기명 함수로 click 핸들러를 작성하는 방시과 같은 방식으로 동작한다.

							$(document).ready(function(){
								$("input").each(function(index) {
									function clickHandler () {
										$.print('index = '+index);
										return false;
									}

									$(this).click(clickHandler);
								});
							});
						

익명 함수를 이용한 버전이 조금 더 짧다. 기명 함수를 사용할 때는 그 위치가 중요한다.

							$(document).ready(function(){
								function clickHandler () {
									$.print('index = '+index);
									return false;
								}

								$("input").each(function(index) {
									$(this).click(clickHandler);
								});
							});

							// Uncaught ReferenceError: index is not defined
						

위 예제는 버튼을 클릭하면 자바스크립트 에러가 발생한다. index를 clickHandler()의 닫힌 환경 안에서 찾을 수 없기 때문이다. 이 환경에서 index는 정의되지 않는 변수가 된다.