2010. 8. 16. 22:52

[번역] Explaining JavaScript Scope And Closures

이 글은 아래 주소를 간략하게 번역한 것입니다. 


범위 (Scope)

범위는 변수와 함수가 접근가능한 곳이 어디인지 그리고 실행되고 있는 컨텍스트가 무엇인지를 뜻합니다. 기본적으로, 변수 또는 함수는 전역 또는 지역 범위로 정의될 수 잇습니다. 변수는 보통 함수 범위를 가지며 함수는 변수와 동일한 범위를 가집니다. 

전역 범위 (global Scope)

전역이라고 하면 코드 어디에서든 접근이 가능하다는 것을 의미합니다. 아래 예를 보세요.

var monkey = "Gorilla";

function greetVisitor () {
	return alert("Hello dear blog reader!");
}
이 코드가 만약 웹 브라우저에 실행되고 있었따면, 함수 범위는 윈도우가 될 것입니다. 그리하여 그 브라우저 내에서 실행되고 있는 모든 곳에서 접근이 가능합니다.

지역 범위 (local scope)

전역 범위와는 반대로, 지역 범위는 정의된 특정 범위 내에서만 접근이 가능합니다. 예륻 들면,

function talkDirty () {
	var saying = "Oh, you little VB lover, you";
	return alert(saying);
}
alert(saying); // Throws an error
위 코드를 살펴보면, saying 이라는 변수는 talkDirty 함수 내에서만 접근이 가능합니다. 외부에는 정의가 되어 있지 않습니다. 주의: sayingvar 키워드 없이 선언한다면, saying 은 자동적으로 전역 변수가 됩니다.

중첩된 함수를 가지고 있다면, 내부 함수는 외부 함수의 변수와 함수에 접근할 수 있습니다.

function saveName (firstName) {
	function capitalizeName () {
		return firstName.toUpperCase();
	}
	var capitalized = capitalizeName();
	return capitalized;
}
alert(saveName("Robert")); // Returns "ROBERT"
위에서 본 것처럼, 내부 함수 capitalizeName 은 어떤 파라미터도 가지고 있지 않지만, 외부의 saveName 함수에 있는 firstName 파라미터에 대한 완벽한 접근을 가지고 있습니다. 좀더 명확하게 하기 위해, 또 다른 샘플을 살펴봅시다.

function siblings () {
	var siblings = ["John", "Liza", "Peter"];
	function siblingCount () {
		var siblingsLength = siblings.length;
		return siblingsLength;
	}
	function joinSiblingNames () {
		return "I have " + siblingCount() + " siblings:\n\n" + siblings.join("\n");
	}
	return joinSiblingNames();
}
alert(siblings()); // Outputs "I have 3 siblings: John Liza Peter"
위처럼, 두 내부 함수는 외부 함수에 있는 sibling 배열에 접근할 수 있으며, 각각의 내부 함수는 동일한 레벨에 있는 다른 내부 함수에 대해서 접근할 수 있습니다 (이 경우에, joinSiblingNames siblingCount 에 대해 접근할 수 있습니다). 하지만, siblingCount 내에 있는 siblingsLength 변수는 그 함수 내에서만 이용가능 합니다.

Closures

이제 범위에 대한 어느 정도 감을 잡았다면, closure 에 대해 살펴봅시다. Closure 는 표현식(expression)입니다, 대개 함수로 작성되며, 특정 컨텍스트 내의 변수 집합과 동작할 수 있습니다. 외부 함수에 있는 지역 변수를 참조하는 내부 함수는 closure 를 생성합니다. 예를 들면:

function add (x) {
	return function (y) {
		return x + y;
	};
}
var add5 = add(5);
var no8 = add5(3);
alert(no8); // Returns 8
자자~ 방금 어떤 일이 있어 났는지 파헤쳐 봅시다.

1. add 함수가 호출될 때, 하나의 함수를 반환합니다. 
2. 그 함수는 컨텍스트를 닫으면서 그 당시에 파라미터 x 가 어떤 값이었는지를 기억합니다. (여기서는 5가 됩니다.)
3. add 함수 호출의 결과가 변수 add5 에 할당 될 때,  초기에 생성될 당시 x 가 무엇인지를 항상 알게 됩니다.
4. add5 변수는 항상 값 5에다가 전달되는 값을 더하는 하나의 함수를 참조하게 됩니다.
5. 그것은 add5 가 3 을 전달하여 호출될 때, 5 와 3을 합한 8을 반환한다는 것을 의미합니다.

그래서, JavaScript 세계에서 add5 함수는 실제로는 아래와 같습니다.

function add5 (y) {
	return 5 + y;
}
악명 높은 반복 문제

변수 i 를 어떤 엘리먼트에 할당하는 반복문을 만들고 그 반복문의 값이 단지 i 가 가지고 있던 가장 최근 값을 반환했다는 것을 얼마나 많이 발견하게 되었습니까? 

틀린 참조

아래의 잘못된 코드를 살펴봅시다. 아래 코드는 5개의 엘리먼트를 생성하고, 그 다음 각 엘리먼트의 텍스트로 i 의 값을 추가하고, 다음에는 그 link 에 연결된 i 값을 출력하기를 기대하는, - 즉, 엘리먼트의 텍스트와 동일한 값을 출력하는 - onclick 핸들러를 등록합니다. 그러고 나서 그 엘리먼트들을 다큐먼트 body 에 삽입합니다.

function addLinks () {
	for (var i=0, link; i<5; i++) {
		link = document.createElement("a");
		link.innerHTML = "Link " + i;
		link.onclick = function () {
			alert(i);
		};
		document.body.appendChild(link);
	}
}
window.onload = addLinks;
각 엘리먼트는 올바른 텍스트를 가집니다, 예를 들어, "Link 0", "Link 1" 등등. 하지만 링크를 클릭할 때마다, 숫자 5 를 경고 메시지로 보여주게 됩니다. 저런, 이유가 무엇일까요? 이에 대한 원인은 변수 i 는 반복문이 수행될 때 마다 1 식 증가하는 값을 가지게 되고 onclick 이벤트가 아직 실행되고 있지 않기 때문에, i 는 계속해서 증가하기 때문입니다.

그리하여, i 가 5가 될 때까지 반복문이 수행되고, addLinkes 함수가 종료되기 전에 i 의 마지막 값은 5가 됩니다. 그러고 나서, onclick 이벤트가 실제로 트리거 될 때, 그 이벤트는 i 의 최종 값을 가지게 됩니다.

Working Reference (올바르게 동작하는 참조)

당신이 원하는 것을 하려면 위 코드 대신 closure 를 생성합니다. 그렇게 하면 i 값을 엘리먼트의 onclick 이벤트에 할당할 때, 그 당시의 정확한 값을 가지게 됩니다. 아래처럼요:

function addLinks () {
	for (var i=0, link; i<5; i++) {
		link = document.createElement("a");
		link.innerHTML = "Link " + i;
		link.onclick = function (num) {
			return function () {
				alert(num);
			};
		}(i);
		document.body.appendChild(link);
	}
}
window.onload = addLinks;
이 코드에서는, 첫번째 엘리먼트를 클릭하면, "0" 을 출력하고, 그 다음은 "1"을 출력합니다. 이게 바로 위에서 보여준 첫번째 코드가 하려고 했던것이지요. 여기서의 해결책은 onclick 이벤트에 적용된 내부 함수가 파라미터 num 을 참조하는 곳에서 (변수 i  그당시 값을 가지는) closure 를 생성하는 것입니다, 

그러고 나서 그 함수는 그 값을 안전하게 밀어넣으며(할당하며) 종료합니다, 그리고 onclick 이벤트가 호출될 때 올바른 숫자를 반환할 수 있게 되는 것이죠.

Self-Invoking Functions (자체-호출 함수?)

Self-invoking 함수는 즉시 실행하면서 자신의 closure 를 생성하는 함수입니다. 아래를 살펴봅시다.

(function () {
	var dog = "German Shepherd";
	alert(dog);
})();
alert(dog); // Returns undefined
OK, dog 변수는 해당 컨텍스트 내에서만 이용가능합니다. 아주 재미있는 것이 여기에 있습니다. 이 함수는 위의 반복문이 가지는 문제를 해결합니다, 그리고 이 함수는 또한 Yahoo JavaScript Module Pattern 의 기초이기도 입니다.

Yahoo JavaScript Module Pattern 

이 패턴의 요지는 closure 를 생성하기 위한 self-invoking 함수를 사용하는 것이고, 이런 이유로 private public 속성과 메소드를 만드는 것을 가능하게 합니다. 간단한 예는 다음과 같습니다.

var person = function () {
	// Private
	var name = "Robert";
	return {
		getName : function () {
			return name;
		},
		setName : function (newName) {
			name = newName;
		}
	};
}();
alert(person.name); // Undefined
alert(person.getName()); // "Robert"
person.setName("Robert Nyman");
alert(person.getName()); // "Robert Nyman"
이것의 멋진 점은 이제 객체의 외부에 노출할 public 한 부분, (그리고 덮어써질 수 있는 부분), 그리고 아무도 접근할 수 없는 private 한 부분을 결정할 수 있다는 것입니다. 위의 name 변수는 함수 컨텍스트의 외부에서는 숨겨집니다, 하지만 getName setName 을 통해서 접근될 수 있습니다, 왜냐하면 이 함수들은 name 변수에 대한 참조를 가지는 closure 를 생성하기 때문입니다.

결론

나(원문 필자)의 진정한 바램은 초보자 또는 숙련된 프로그래머가 이글을 읽고 난 후, 범위와 closure JavaScript 에서 실제로 동작하는 방법에 대한 명확한 개념을 가지게 되는 것입니다. 질문과 피드백은 언제나 환영이며, 충분히 중요하다고 여겨지면, 그 내용을 추가하도록 하겠습니다.