Closure

>> 예시 출력란



이 책을 통해 함수를 인자로 받아들이는 많은 jQuery 메서드를 봤다. 예제에서 계속해서 함수를 생성하고, 호출하고, 전달했다. 이 정도는 보통 내부(inner) 자바스크립트 기술을 대충만 알아도 어렵지 않게 할 수 있는 작업이다. 하지만 가끔 부딪히는 부작용(오작동)을 살필 때, 스크립트 언어의 특징에 대한 지식이 없으면 상당히 낮설게 보일 수 있다. 여기서는 클로저(closure)라고 불리는 자주 쓰지만, 자세히 알려지지는 않은 비법을 알아보겠다.

텍스트를 화면에 출력하는 작은 예제로 이 내용을 확일할 것이다. 출력을 위해 특정 브라우저의 기능(console.log()와 같은)이나 alert()을 사용하지 않고 다음과 같은 작은 플러그인 메서드를 사용할 것이다.

							$.print= function (message) {
								$(document).ready(function(){
									$('<div class="result"></div>')
										.text(String(message))
										.appendTo('#result')
								});
							}							
						

이 메서드로 'hello' 텍스트를 <div id="results"> 내부에 삽입하려면 $.print('hello')와 같이 사용한다.



내부 함수

자바스크립트가 내부 함수(inner function) 선언을 지원하는 프로그래밍 언어에 속하는 것은 행운이라고 할 수 있다. C 언어와 같은 많은 전통적인 프로그래밍 언어는 하나의 최상위 영역에서 모든 함수를 선언한다. 이와 다르게 내부 함수를 사용할 수 있는 언어는 네임스페이스 오염을 피할 수 있게 필요한 곳에만 작은 함수를 선언해 사용할 수 있다.

내부 함수란 단순히 다른 함수 내부에 정의되는 함수를 말한다. 예를 들면 다음과 같다.

							function outerFn() {
								function innerFn() {
								}
							}					
						

innerFn()는 outerFn() 영역에 포함된 내부 함수다. 이는 innerFn()을 호울할 수 있는 영역은 outerFn() 안에서만 한정되며 그 밖에서는 호출할 수 없다는 뜻이다. 다음 자바스크립트 코드에서는 에러가 발생한다.

							function OuterFn () {
								$.print('Outer function');
								function innerFn () {
									$.print('Inner Function');
								}
							}
							$.print('innerFn() :');
							innerFn();		
							
							// 결과: Uncaught ReferenceError: innerFn is not defined 
						

다음과 같이 outerFn() 함수에서는 innerFn() 함수를 호출할 수 있다.

							function outerFn () {
								$.print('Outer function');
								function innerFn () {
									$.print('Inner function');
								}
								innerFn();  // innerFn() 호출
							}
							$.print('outerFn() :');
							outerFn();
						

수행 결과는 다음과 같다.

							outerFn() : 
							Outer function 
							Inner function
						

특히 이 기술은 작으면서 한 번만 사용되는 함수를 위해 사용된다. 예를 들어, 내부적으로는 재귀(recursive)되는 알고리즘이지만 외부적으로는 재귀형태가 아닌 단순한 API 형태에서 내부 함수를 이용하면 가장 잘 표현할 수 있다.



영역 벗어나기

함수에 대한 참조(function reference)까지 가미되면 상황은 훨씬 복잡해진다. 파스칼 같은 언어는 코드 일부를 숨기는 목적으로만 내부 함수를 사용한다. 그 함수들은 그 부모 함수에 묻혀서 영원히 밖으로 나올 수가 없다. 이와 다르게 자바스크립트에서는 함수를 데이터처럼 다른 곳으로 전달할 수 있다. 이는 내부 함수가 부모 함수와 같은 구속자로부터 탈출할 수 있음을 의미한다.

부모 함수에서 벗어나는 방법은 여러가지다. 예를 들어 함수를 다음과 같이 전역 변수에 할당했다고 가정하자.

							var globalVar;

							function outerFn () {
								$.print('Outer function');
								function innerFn () {
									$.print('Inner function');
								}
								globalVar = innerFn;
							}

							$.print('outerFn() :');
							outerFn();
							$.print('globalVar(); ');
							globalVar();

							// 결과
							// outerFn() :
							// Outer function 
							// globalVar() :
							// Inner function
						

함수를 정의한 후에 outerFn()을 호출하면 전역 변수 globalVar를 변경하게 된다. globalVar는 이제 innerFn()을 참조하므로 나중에 globalVar()을 호출하면 innerFn()을 호출했을 때와 똑같이 동작해 print로 출력할 수 있다.

물론 여전히 outerFn()의 밖에서 innerFn()을 직접 호출할 수는 없다. 함수를 전역 변수에 저장하는 방법으로 외부에서 참조할 수 있게 했지만, 함수의 이름 자체는 여전히 outerFn()의 영역에 묶여있다.

다음과 같이 함수에 대한 참조를 반환하면 역시 부모 함수 밖에서도 내부 함수를 참조할 수 있다.

							function outerFn () {
								$.print('Outer function');
								function innerFn () {
									$.print('Inner function');
								}
								return innerFn;
							}

							$.print('var fnRef = outerFn(): ');
							var fnRef = outerFn();
							$.print('fnRef() :');
							fnRef();

							// 결과
							// var fnRef = outerFn():
							// Outer function
							// fnRef() :
							// Inner function
						

여기서는 outerFn() 함수에서 전역 변수를 수정하는 일은 없지만 대신 outerFn() 함수가 innerFn() 함수의 참조를 반환한다. 따라서 outerFn()의 결과 값을 저장해 호출하면서 앞의 예제와 같은 결과를 나타낸다.

여기서는 outerFn() 함수에서 전역 변수를 수정하는 일은 없지만 대신 outerFn() 함수가 innerFn() 함수의 참조를 반환한다. 따라서 outerFn()의 결과 값을 저장해 호출하면 앞의 예제와 같은 결과를 나타낸다.

							var fnRef = outerFn():
							Outer function
							fnRef() :
							Inner function							
						

내부 함수가 자신의 영역 범위(내부함수를 포함하는 함수)를 넘어서 참조를 통해 호출될 수 있다는 사실은 그 함수가 앞으로도 계속 호출될 가능성이 있는 한 자바스크립트가 그 함수를 계속 유지해야 함을 뜻한다. 함수를 가리키는 각 변수는 자바스크립트 런타임에 의해 관리된다. 함수를 가리키는 마지막 변수가 사라지면 자바스크립트 가비지 컬렉터(garbage collector)에 의해 함수는 메모리로부터 해제된다.




변수 범위

내부 함수들도 그 사용 범위가 함수 내부로 한정된 자신만의 변수를 가질 수 있다.

							function outerFn () {
								function innerFn () {
									var innerVar = 0;
									innerVar++;
									$.print('innerVar = '+ innerVar);
								}
								return innerFn;
							}

							var fnRef = outerFn();
							fnRef();
							fnRef();

							var fnRef2 = outerFn();				
							fnRef2();
							fnRef2();				
						

위와 같은 코드에서는 내부 함수가 참조로든 다른 방법으로든 매번 호출될 때마다 새로운 변수 innerVar가 생성되고 값이 증가하므로 다음과 같이 출력될 것이다.

							innerVar = 1
							innerVar = 1
							innerVar = 1
							innerVar = 1					
						

내부 함수도 다른 함수처럼 전역 변수를 참조할 수도 있다.

							function outerFn () {
								function innerFn () {
									var innerVar = 0;
									innerVar++;
									$.print('innerVar = '+ innerVar);
								}
								return innerFn;
							}

							var fnRef = outerFn();
							fnRef();
							fnRef();

							var fnRef2 = outerFn();				
							fnRef2();
							fnRef2();				
						

전역 변수를 사용하면 함수가 호출될 때마다 변수의 값이 계속 증가한다.

							globalVar = 1
							globalVar = 2
							globalVar = 3
							globalVar = 4			
						

하지만 변수가 부모 함수의 지역 변수라면 어떻게 될까? 내부 함수는 그 부모의 범위를 상속하고 있으므로 부모 함수의 변수를 참조할 수 있다.

							function outerFn () {
								var outerVar = 0;
								function innerFn () {
									outerVar++;
									$.print('outerVar = '+ outerVar);
								}
								return innerFn;
							}

							var fnRef = outerFn();
							fnRef();
							fnRef();

							var fnRef2 = outerFn();				
							fnRef2();
							fnRef2();		
						

이제 함수는 좀 더 흥미로운 결과를 보여준다.

							outerVar = 1
							outerVar = 2
							outerVar = 1
							outerVar = 2
						

앞서 살펴본 두가지 효과가 섞여서 나타난다. 서로 다른 참조 변수를 사용하는 innerFn() 함수의 호출에서는 outerVar 변수가 서로 독립적으로 증가하는 것을 알 수 있다. outerFn()을 두번째 부를 때는 기존의 outerVar가 초기화되는 게 아니라 두번째 함수영역에 새로운 인스턴스(instance)를 생성한다. 그 결과 fnRef()를 다시 한번 호출하면 3을 출력할 것이다. 이와 마찬가지로 fnRef2()를 계속 호출하면 역시 3을 출력할 것이다. 이 두개의 카운터는 서로 완전히 독립해서 증가한다.

내부 함수에 대한 참조가 정의된 범위 밖으로 나가면 그 내부 함수의 클로저가 만들어진다. 내부함수의 인자도 지역변수도 아닌 변수를 자유 변수(free variable)라고 하며 바깥 함수의 상태가 이 변수를 '가두는'(closes) 역할을 한다. 바깥 함수의 지역변수는 내부 함수에 의해 참조되므로 기본적으로 실행 중인 상태로 유지된다. 그러므로 바깥 함수 수행이 끝나더라도 클로저에 의해 사용되므로 메모리는 해지되지 않는다.