IT_Programming/JavaScript

자바스크립트 완벽가이드 - 8.8 함수 유효 범위와 클로저

JJun ™ 2010. 7. 4. 22:40


4장에서 설명한 바와 같이 자바스크립트 함수의 몸체는 전역 유효 범위와는 다른 지역 유요 범위 상에서 실행된다. 

이 절에서는 함수의 유효 범위와 클로저 같은 관련 유효 범위 이슈들을 다룬다.[각주:1]


         1) 이 절에서는 이 책을 처음 읽을 때는 건너뛰어도 좋을 자바스크립트의 고급 주제를 다룬다.

            [본문으로]

 

 

 

1. 어휘적 유효 범위

 

자바스크립트의 함수들은 동적이라기보단 어휘적으로 유효 범위가 정해진다.

이것은 함수가 실행되는 유효 범위가 아니라 함수가 정의되어 있는 유효 범위 안에서 실행됨을 의미한다.

함수가 정의될 때는 현재의 유효 범위 체인이 저장되며 이것은 함수의 내부 상태 중 일부가 된다.

 

최상위 레벨에서 유효 범위 체인은 단순히 전역 객체들로 구성되며 어휘 범위는 특별히 의미를 지니지

않는다. 그러나 중첩된 함수를 정의하면 유효 범위는 함수를 포함한다.

 

즉 이는 중첩된 함수가 그 함수를 포함하는 함수의 모든 전달인자와 지역 변수들에 접근할 수 있음을

의미한다.


비록 함수가 정의될 당시의 유효 범위 체인은 고정되어 있을지라도 그 유효 범위 체인 안에 정의되어 있는

프로퍼티들은 변할 수 있음을 주의하라. 유효 범위 체인은 '살아있다'. 즉 함수는 함수가 호출되는 시점을

기준으로, 연결되어 있는 모든 것들에 접근할 수 있다.

 

 

 

2. 호출 객체

 

자바스크립트 인터프리터가 함수를 호출할 때엔 먼저 유효 범위를 함수가 정의될 당시의 효력을 지니는

유효 범위 체인으로 설정한다. 그 다음에는 호출 객체(ECMAScript 명세에서는 activation 객체라는 용어를

사용한다)로 알려진 새로운 객체를 생성하여 유효 범위 체인의 맨 앞에 추가한다.

 

이 호출 객체는 함수의 Arguments 객체를 가리키는 arguments 프로퍼티로 초기화된다.

그 다음에는 함수의 이름 붙은 매개변수들이 호출 객체에 추가되고, 함수 안에서 var 문장으로 선언된

모든 지역 변수가 역시 호출 객체 안에 정의된다. 이 호출 객체는 유효 범위 체인의 맨 앞에 있기 때문에

함수의 지역 변수, 매개변수들과 Arguments 객체는 모두 함수 내 유효 범위에 있게 된다.

 

이는 곧 유효 범위 체인의 뒤쪽에 위치한 같은 이름의 프로퍼티들은 모두 가려지게 됨을 의미한다.
arguments와 달리 this는 키워드임을 유의하라. 따라서 this는 호출 객체의 프로퍼티가 아니다.

 

 

 

3. 네임스페이스로서의 호출 객체

 

때로는 전역 네입스페이스를 어지럽히는 대신 임시 네임스페이스로 작동할 수 있는 호출 객체를 생성하여,

이 안에 원하는 변수를 정의하고 프로퍼티를 생성하기 위한 용도로 함수를 정의하는 것이 유용할 수 있다.

예를 들어 여러분이 가진 자바스크립트 코드를 다양한 자바스크립트 프로그램들(또는 수많은 웹 페이지들에서 작동하는 클라이언트 측 자바스크립트)과 함께 사용하려 한다고 가정하자. 대부분의 다른 코드들처럼

 이 자바스크립트 코드에는 연산의 중간 결과값을 저장하기 위한 변수들이 정의되어 있다.

 

이 경우에 다른 여러 프로그램에 의해 코드가 사용되기 때문에 한곳에서 생성된 변수들이 이 코드를 사용하는 다른 프로그램에서 생성된 변수들과 충돌을 일으키지는 않는지 알 수가 없다는 것이 문제된다.


이에 대한 해답은, 코드를 함수 안에 넣고 함수를 호출하는 것이다.

이 경우 변수들은 함수의 호출 객체 안에 정의된다.

function init() {
// 코드는 여기에 넣는다.
// 선언된 모든 변수는 전역 네임스페이스를 어지럽히는 대신
// 호출 객체의 프로퍼티가 된다.
}
init(); // 그러나 함수를 호출하는 것을 잊으면 안 된다!


이 코드는 함수를 가리키는 다 ㄴ한 개의 프로퍼티 init을 전역 네임스페이스에 추가한다.

만약 단 한 개의 프로퍼티를 추가하는 것도 많다고 생각된다면 익명(anonymous) 함수를 정의하고

호출하는 단일 표현식을 사용할 수 있다. 이를 위해 관용적으로 사용할 수 있는 자바스크립트 코드는

다음과 같다.

(function() {  // 이 함수는 이름이 없다.
// 코드느 여기에 넣는다.
// 선언된 모든 변수는 전역 네임스페이스를 어지럽히는 대신
// 호출 객체의 프로퍼티가 된다.
})();  // 함수 리터럴을 종결하고 이를 지금 호출한다.


자바스크립트 문법에 따라서 함수 리터럴을 에워싸는 괄호를 필요함을 유의하라.

 

 

 

 

4. 클로저로서의 중첩된 함수

 

자바스크립트가 중첩된 함수를 허용한다는 사실, 함수를 데이터로 사용할 수 있다는 사실, 그리고 어휘적

유요 범위를 사용한다는 사실은 함께 상호 작용하여 놀랍고도 매우 강력한 결과를 낸다. 이를 탐험하기에 앞서서 함수 f안에 정의된 함수 g를 가정하자. f가 호출될 때의 유효 범위 체인은 f호출을 위한 호출 객체와 전역 객체 순으로 이루어져 있다. g는 f안에 정의되기 때문에 이 유효 범위 체인은 함수 g정의의 일부로서 가정된다. g가 호출될 때의 유효 범위 체인은 함수 g의 호출 객체, 함수 f의 호출 객체, 그리고 전역 객체 순으로

이루어 진다.


중첩된 함수를 그 함수가 정의된 어휘적 유효 범위 안에서 호출하는 경우는 아주 쉽게 이해할 수 있다.

예를 들어 다음의 코드는 특별히 어떠한 놀라운 일도 벌이지 않는다.

var x = "global";
function f() {
var x = "local";
function g() { alert!(x); }
g();
}
f(); // 이 함수를 호출하면 "local"이 출력된다.


그러나 자바스크립트에서 함수는 다른 모든 값과 마찬가지로 데이터다. 따라서 함수는 다른 함수에 의해

반환될 수 있고 객체에 프로퍼티로 할당될 수도 있으며, 또한 배열 등에 저장될 수도 있다. 

 

이도 또한 중첩된 함수가 개입하지 않는 한 특별히 놀라운 일을 벌이지 않는다. 이에 관하여 중첩된 함수를

반환하는 함수가 정의된 다음의 코드에 대해서 알아보자. 이 함수가 호출될 때 반환되는 함수의 자바스크립트 코드는 항상 동일하다. 그러나 반환된 각 함수의 유효 범위는 조금씩 차이가 난다. 이는 바깥 쪽 함수에 대한 전달인자 값이 각 호출 때마다 달라지기 때문이다. (즉, 바깥 쪽 함수를 호출할 때마다 유효 범위 체인 안의 호출 객체가 달라진다.)

 

만약 여러분이 반환된 함수들을 배열에 저장하고 이들을 하나씩 호출하면 전부 다른 값을 반환하는 것을

볼 수가 있다. 모든 함수는 동일한 자바스크립트 코드로 이루어져있고 완전히 동일한 유효 범위 상에서

호출되었기 때문에, 서로 다른 반환값을 초래하는 요소는 함수가 정의될 당시의 유료 범위 뿐이다.

// 이 함수는 호출될 때마다 함수를 반환한다.
// 이 함수 안의 유효 범위는 함수가 호출될 때마다 달라진다.
function makefunc(x){
return function(){ return x; }
}

// makefunc()를 수차례 호출한다. 그리고 결과를 배열에 저장한다.
var a = [makefunc(0), makefunc(1), makefunc(2)];

// 이제 이 함수들을 호출하고 결과값을 출력한다.
// 비록 모든 함수의 몸체는 동일하지만 유효 범위가 다르다.
// 따라서 각 호출은 서로 다른 값을 반환한다.
alert!(a[0]()); // 0을 출력한다.
alert!(a[1]()); // 1을 출력한다.
alert!(a[2]()); // 2을 출력한다.


이 결과는 어휘적 유효 범위 규칙, 즉 함수는 함수가 정의될 당시의 유효 범위 상에서 실행된다는 규칙을 엄격하게 적용했을 때 예상할 수 있었던 것과 완전히 일치한다. 그럼에도 불구하고 이 결과가 놀라운 것은 중첩된 함수를 정의하는 함수가 종료되는 시점에서 지역 유효 범위의 존재 또한 종료될 것이라 예상했기 때문인데, 사실은 이렇게 되는 것이 일반적이다.

 

함수가 호출되면 이를 위한 호출 객체가 생성되고 이 호출 객체는 유효 범위 체인 위에 위치한다.

함수가 종료되면 호출 객체는 유효 범위 체인에서 제거된다. 중첩된 함수가 결부되지 않으면 유효 범위 체인만이 유일하게 호출 객체를 가리킨다. 호출 객체가 유효 범위 체인에서 제거되면 이게 대한 아무런 참조도

남지 않으며, 따라서 이 객체는 가비지 컬렉션에 의해 사라진다.


그러나 중첩된 함수는 이러한 상황을 변화시킨다. 중첩된 함수가 생성되면 이 함수의 정의는 호출 객체를

가리키는데, 이는 함수가 정의된 유효 범위의 맨 앞에 호출 객체가 존재하기 때문이다.

 

만약 중첩된 함수가 바깥 함수의 내부에서만 사용된다면 이 함수에 대한 유일한 참조는 호출 객체 내부에만 존재한다. 바깥 함수가 반환할 때, 중첩된 함수는 호출 객체를 가리키고 호출 객체는 중첩된 함수를 가리키지만 이 둘에 대한 다른 참조들은 존재하지 않는다. 따라서 이 두 객체는 역시 가비지 컬렉션에 의해 사라진다.
그러나 만약 여러분이 중첩된 함수에 대한 참조를 전역 유효 범위 안에 저장해 놓는다면 이야기가 달라진다. 이를 위해서는 바깥 함수의 반환값으로 중첩된 함수를 받아 사용하거나 중첩된 함수를 다른 어떤 객체의 프로퍼티로 저장해두는 수가 있다. 이 경우 중첩된 함수에 대한 외부 참조가 존재하며 이 중첩된 함수는 바깥 함수의 호출 객체를 가리키는 참조를 계속 간직한다. 결말은 다음과 같다. 한번 호출된 바깥 함수를 위하여 생성된 호출 객체는 계속하여 살아남고 함수 전달인자와 지역 변수의 이름과 값들은 이 호출 객체 안에서 계속 유지된다. 자바스크립트 코드는 호출 객체에 직접적으로 접근할 방법이 없다.

 

그러나 정의된 프로퍼티들은 중첩된 함수 호출을 위한 유효 범위 체인의 일부가 된다. (만약 바깥 함수가 두 개의 중첩된 함수들을 위해 전역 참조를 저장한다면, 이 두 중첩된 함수들은 같은 호출 객체를 공유한다. 따라서 한 중첩된 함수 호출에 의해 초래된 변화는 나머지 다른 중첩된 함수에 의해서도 노출된다.)


자바스크립트 함수는 실행될 코드와 이 함수가 실행될 유효 범위의 조합이다. 컴퓨터 과학 문헌에서 이러한 코드와 유효 범위의 조합은 클로저(closure)로 알려져 있다. 모든 자바스크립트 함수는 클로저다. 이 클로저가 유일하게 흥미로운 경우는 바로 위에서 설명했듯이 중첩된 함수가 그 함수가 정의된 유효 범위의 바깥으로 익스포트(export)될 때다.


클로저는 흥미롭고 또한 강력한 프로그램 작성 기법이다. 클로저는 비록 자바스크립트 프로그래밍에서

비일비재한 것은 아니지만 그 작동 원리를 이해하는 것은 여전히 의미 있는 일이다. 만약 여러분이 클로저를 이해한다면 여러분은 유효 범위 체인과 함수의 호출 객체를 이해한 것이며, 스스로를 고급 자바스크립트

프로그래머라 부를 수 있다.

 

 

[클로저의 예]

 

함수 호출의 경계를 넘어 그 값을 기억할 수 있는 함수를 작성하고 싶을 때가 종종 있다. 호출 객체는 함수 호출의 경계를 넘어 존재를 유지할 수 없기 때문에 이 값은 지역 변수 안에 저장할 수가 없다. 전역 변수를 사용할 수는 있지만 이는 곧 전역 네임 스페이스를 오염시키게 된다. 8.6.3항에서 소개한 uniqueInteger() 함수는 사라지지 않는 값을 저장하기 위한 용도로서 함수 자신의 프로퍼티를 사용했다. 클로저를 사용하면 여기서 한 발짝 더 나아가 사라지지 않으면서도 private 속성을 지니는 변수를 생성할 수 있다. 여기에 우선 클로저를 사용하지 않고 작성한 함수가 있다.

// 호출될 때마다 서로 다른 정수 값을 반환한다.
uniqueID = function() {
if (!arguments.callee.id) arguments.callee.id = 0;
return argumenets.callee.id++;
}


이 방식의 문제점은 누구든 uniqueID.id를 0으로 되돌림으로써 함수가 절대로 같은 값을 두 번 반복하지 않는다는 규칙을 위반하게 만들 수 있다는 점이다. 이를 방지하기 위해서 클로저를 사용하면 사라지지 않는 값을 저장하면서도 오직 함수만 이 값에 접근하게 할 수 있다.

uniqueId = (function() { // 이 함수의 호출 객체가 우리의 값을 저장한다.
var id = 0;        // 이것이 바로 지속성 있으면서도 private 속성을 지닌 값이다.
// 바깥 함수는 중첩된 함수를 반환하는데, 이 중첩된 함수는
// 위의 지속성 있는 값에 접근할 수 있다. 위의 uniqueID 변수에 저장되는
// 것은 이 중첩된 함수이다.
return function() { return id++; }; // 값을 증가시켜서 반환한다.
})(); // 바깥 함수를 정의하고 호출한다


예 8-6은 두 번째 클로저 예다. 이 예는 위에서 설명한 것과 같이 private 속성의 지속성 있는 변수들을 하나 이상의 함수 간에 공유할 수 있는 방법을 설명한다.

예 8-6 클로저를 사용한 private 프로퍼티
// 이 함수는 객체 o가 가진 name이란 이름의 프로퍼티에 접근하기 위한 메서드를 추가한다.
// 메서드의 이름은 get<name>과 set<name>이다. 만약 술어 함수가 함께 제공되면
// setter(값을 설정하는) 메서드는 값을 설정하기에 앞서서 값이 적법한지를 판단하기 위해
// 술어 함수를 사용한다. 만약 이 함수가 false를 반환하면 setter 메서드는 예외를 발생시킨다.
//
// 이 함수의 별난 특징은 getter(값을 읽어오는) 메서드와 setter(값을 저장하는) 메서드에 의해
// 조작되는 프로퍼티 값이 객체 o에 저장되어 있지 않다는 점이다. 대신에 이 값은 함수의
// 지역 변수로만 저장된다. getter와 setter 메서드들도 또한 함수에 지역적으로 정의되어
// 지역 변수에 접근할 수 있다. 따라서 이 값은 getter와 setter 메서드에 의해서만
// 접근할 수 있으며, 오직 setter 메서드에 의해서만 설정되거나 변경될 수 있다.
function makeProperty(o, name, predicae) {
var value;  // 이것이 프로퍼티 값이 된다.

// getter 메서드는 단순히 값을 반환하는 역할을 한다.
o["get" + name] = function() { return value; };

// setter 메서드는 값을 저장하는 역할을 하는데, 만약 predicate가
// 값을 거부하면 예외를 발생시킨다.
o["set" + name] = function(v) {
if (predicate && !predicate(v))
throw "set" + name + ": invalid value " + v;
else
value = v;
};
}

// 다음 코드는 makeProperty() 메서드를 시험한다.
var o = {};  // 여기에 빈 객체가 있다.

// getName과 setName이란 이름의 프로퍼티 접근 메서드들을 추가한다.
// 오직 문자열 값만 허용되도록 보증한다.
makeProperty(o, "Name", function(x) { return typeof x == "string"; });

o.setName("Frank"); // 프로퍼티 값을 설정한다.
print(o.getName());  // 프로퍼티 값을 읽어온다.
o.setName(0);         // 잘못된 데이터 타입의 값을 설정하려고 시도한다.


내가 아는 한 가장 유용하면서도 또 가장 자연스러운 클로저 사용 예는 스티브 옌(Steve Yen)의 중단점(breakpoint) 기능 구현이다. 이 기능 구현은 TrimPath 클라이언트 측 프레임워크 일부이면 http://trimpath.com에 공개되었다. 중단점이한 함수 내의 한 지점을 말하는데, 이 위치에서 함수는 실행을 멈추고 프로그래머에게 변수의 값을 조사하거나 표현식을 평가하고 함수를 호출하는 등의 작업을 수행할 수 있는 기회를 제공한다. 스티브의 중단점 기술은 클로저를 사용하여 함수 안의 현재 유효 범위(지역 변수들과 함수의 전달인자들을 포함하는)를 포착하고, 이를 전역 함수인 eval!()과 함께 사용함으로써, 포착한 유효 범위 안의 값을 조사할 수 있게 한다. eval!()은 자바스크립트 코드를 담은 문자열을 평가하고 그 값을 반환한다. (3부에서 eval!()의 자세한 내용을 찾을 수 있다.) 다음 코드의 중첩된 함수는 자기 조사(self-inspecting)가 가능한 클로저로 작동한다.

// 현재의 유효 범위를 포착하고 이를 eval!()을 사용하여 조사할 수 있게 한다.
var inspector = function($) { return eval!($); }


이 함수는 일반적으로 사용하지 않는 식별자 $를 전달인자 이름으로 사용함으로써 함수가 조사하려는 유효 범위와의 이름 출동 가능성을 최소화하려 했다.
함수 안에 중단점을 만들기 위해서는 이 클로저를 예8-7과 같은 함수에 전달하면 된다.

예8-7 클로저를 사용한 중단점 구현
// 이 함수는 중단점을 구현한다. 이 함수는 사용자에게 반복적으로 표현식의 입력을 질의하고
// 사용자가 입력한 표현식을 함수의 전달인자로 제공된
// 자기 조사가 가능한 클로저를 사용하여 평가한 후, 그 결과를 출력한다.
// 유효 범위에 접근하여 ㄱ밧을 조사하려면 클로저가 필요하다.
// 따라서 각 함수는 자신만의 클로저를 제공해야만 한다.

// 이 함수는 스티브 옌의 breakpoint() 함수에서 영감을 얻었다.
// http://trimpath.com/project/wiki/trimBreakpoint
function inspect(inspector, title){
var expression!, result;

// 중단점을 사용하여 그 이후에 나타나는 다른 중단점을 끌 수도 있다.
// 이를 위해선 함수 안에 "ignore"라는 이름의 프로퍼티를 만들면 된다.

if ("ignore" in arguments.callee) return;

while(true) {
// 사용자에게 질의할 때 사용할 메시지인 질의 문구를 지정한다.
var message = "";
// 만약 title이 주어졌다면 이를 먼저 출력한다.
if (title) message = title + "\n";
// 만약 이미 평가한 표현식이 있다면 이를 결과값과 함께 출력한다.
if (expression!) message += "\n" + expression! + " ==>" + result + "\n";
else expression! = "";
// 다음의 메시지는 질의를 할 때마다 기본적으로 출력한다.
message += "Enter an expression! to eval!ute:";

// 사용자의 입력을 받기 위한 창을 질의 문구와 함께 출력한다.
// 여기서는 마지막으로 입력한 표현식을 기본값으로 사용한다.
expression! = prompt(message, expression!);

// 만약 사용자가 아무것도 입력하지 않으면(또는 취소 버튼을 클릭하면)
// 사용자가 자신의 일을 끝낸 것으로 간주하여 중단점도 역시 끝낸다ㅏ.
if (!expression!) return;

// 그렇지 않다면 제공된 클로저를 사용하여,
// 조사하려는 유효 범위 안에서 표현식을 평가한다.
//결과값은 루프의 다음 반복에서 출력된다.
result = inspector(expression!);
}
}


예8-7의 inspect() 함수는 사용자에게 텍스트를 출력하고 또 사용자에게 문자열을 입력받기 위해서 Window.prompt() 메서드를 사용한다. (더 자세한 내용은 4부의 Window.prompt()를 참고하라.)
다음의 계승(factorial)게산 함수는 중단점 기법을 사용한다.

function factorial(n) {
// 이 함수를 위한 클로저를 생성한다.
var inspector = function($) { return eval!($); }
inspect(inspector, "Entering factorial()");

var result = 1;
while(n > 1) {
result = result * n;
n--;
inspect(inspector, "factorial() loop");
}

inspect(inspector, "Exiting factorial()");
return result;
}

 

 

 

 

[클로저와 인터넷 익스플로러 메모리 누수]

 

마이크로소프트(Microsoft)의 인터넷 익스플로러(Internet Explorer) 웹 브라우저가 사용하는 가비지 컬렉션 기법은 ActiveX 객체와 클라이언트 측 DOM 엘리먼트에 대해 취약하다. 이 클라이언트 측 객체들에 대한 참조의 개수를 세고 있다가 참조수가 0이 되는 순간 객체를 메모리상에서 제거하는 방식을 사용하는데, 이 방식은 순환 현태의 참조가 존재할 경우 실패한다. 예를 들어 코어 자바스크립트 객체가 document 엘리먼트를 가리키고, 이 document 엘리먼트에 다시 코어 자바스크립트 객체를 가리키는 프로퍼티(이벤트 핸들러 같은)가

있을 수 있다.


IE 상의 클라이언트 측 프로그래밍에서 클로저가 사용될 때에는 이런 종류의 순환 형태 참조가 자주 발생한다. 클로저를 사용할 때에는 함수의 모든 전달인자와 지역 변수를 포함하는 바깥 함수의 호출 객체가, 클로저가 존재하는 한 메모리상에서 사라지지 않음을 기억하라. 만약 이러한 함수 전달인자나 지역 변수가 클라이언트 측 객체를 참조한다면 이는 곧 메모리 누수 현상을 일으킨다.
이 메모리 누수 현상에 대한 완전한 논의는 이 책의 범위를 머어선다. 보다 자세한 내용을 위해서는 http://msdn2.microsoft.com/en-us/library/Bb250448.aspx를 참고하라.