기타/WWW

Javascript의 핵심- ECMAScript

하늘이푸른오늘 2013. 9. 9. 18:03

제가 요즘 JavaScript 공부를 하고 있습니다. 프로그래밍에서 손을 놓은지 벌써 10년 이상된 시점에 무슨 개발자가 되겠다는 것도 아닙니다. 다만, 어쩌다 기회가 되어서 이 언어를 들여다 볼 기회가 생겼는데, -script- 라는 단어때문에 만만히 봤다가 아주 푹 빠져들고 있습니다.


제가 보는 관점에서 JavaScript는 자료유형도 마음대로 바꿔쓸 수 있는 등 아주 유연하다는 장점이 있습니다. 그래서 원하는 모든 것을 구현할 수 있을 것 같습니다. 그런데 그만큼 어렵습니다. 언어에 대해 충분히 이해하지 못하면 아주 나쁜 코드가 만들어질 수도 있습니다. 


처음에는 웹에 있는 글들을 몇개보다가 결국엔 별도의 책을 구입해서 공부하고 있습니다. 그러다가 이 글도 접하게 되었구요. 


이 글은 Dmitry Soshinikov 가 쓴 ECMA-262 라는 글을 Rhio Kim님이 번역하신 글을 복사한 후(글, 포맷, 그림까지) 제가 나름대로(마음대로) 수정한 것입니다. 약간 번역이 생경한 부분을 수정하였지만, 원래 이 글을 손댄 것은 제가 프로토타입 체인, 범위체인 등, 이 글에서 다루는 내용에 대해 좀 더 자세히 알기 위함이었습니다. 특히 클로저(Closure)에 대해서는 제가 이제껏 알던 언어에서는 전혀 나오지 않던 내용이라서 주의깊에 여러번 읽고 수정하였는데도 아직 내용을 완전히 이해하지 못했습니다.


그러다보니, 정리한다고 정리했지만 미진한 부분이 남아 있을 수 있고, 제가 오히려 번역을 망쳐놓은 부분이 있을 수도 있음을 이해해 주시면 감사하겠습니다. 


===========

JavaScript. The Core


이 글은 ECMA-262-3 in detail 시리즈를 요약 정리한 것입니다. 본문에는 이 ES3 시리즈에 대한 링크가 포함되어 있으므로, 자세한 내용은 그곳을 참조하십시오.


이 글을 경험있는 프로그래머 혹은 전문가를 대상으로 합니다.


그러면 ECMAScript 의 기본이 되는 객체의 개념부터 살펴보도록 하겠습니다.

객체(Object)


ECMAScript 는 상당히 추상화된 객체지향언어로서, 객체(object)를 다루고 있습니다. 이와 별도로 원시형(primitives: number, boolean 등)도 있지만, 이들도 필요할 때에는 객체로 변환됩니다. 


“객체”는 (여러개의)속성의 집합으로서, 하나의 프로토타입(prototype) 객체를 가집니다. 프로토타입은 또 다른 “객체”이거나 null 입니다.

우선 간단한 예제를 살펴보겠습니다. 객체의 prototype은 내부 [[Prototype]] 속성으로 참조됩니다. 하지만, 아래의 그림에서는 대괄호 두개를 사용하는 표현하는 대신,  __<internal-property>__ 형태의 표기법을 사용할 것입니다. 특히 프로토토타입 객체에 대해서는 __proto__ 라고 표기하겠습니다. (이것은 표준은 아니지만 SpiderMonkey 등 일부 ECMAScript 엔진에서 실제로 사용되고 있습니다.)

먼저 코드를 살펴보겠습니다.


  1. var foo = {
  2. x : 10,
  3. y : 20
  4. };

여기에서 “foo” 객체는 두 개의 명시적인 속성과, 하나의 암묵적인 속성 __proto__(“foo” 객체의 프로토타입에 대한 참조)를 가지게 됩니다.



이 프로토타입은 도대체 왜 존재하는가? 이 질문에 대한 답을 위해 프로토타입 체인(prototype chain)이라는 개념에 대해 알아보겠습니다.

프로토타입 체인(Prototype Chain)


프로토타입 객체는 단순한 객체로서, 이 객체도 자기자신에 대한 프로토타입 객체를 가지고 있습니다. 만약 어떤 프로토타입 객체가 null이 아닌 프로토타입을 참조하고, 이 객체는 또다른 프로토타입을 참조하는 식으로 계속 연결되어 있을 경우, 이것을 프로토타입 체인이라고 합니다.


“프로토타입 체인”은 객체간의 유한한(finite) 연결로서, 상속 및 공유 속성을 구현하는데 이용됩니다.

예를 들어 대부분은 같고 약간만 다른 두 개의 객체가 있다고 해봅시다. 물론 시스템을 잘 설계할 경우, 비슷한 기능을 각 객체에 중복해서 구현하지 않고 재사용(reuse)하도록 설계할 것입니다. 클래스를 기본으로 한 시스템에서는 이러한 코드 재사용 방법을 클래스기반 상속(class-based inheritance)이라고 합니다. 즉, 클래스 A에 중복되는 기능을 넣고, 클래스 B와 C 에서는 A의 기능을 상속받도록 한 후, 추가적인 변경사항만 구현하는 것입니다.


ECMAScript 에는 클래스라는 개념이 없습니다. 하지만 코드의 재사용 방법에는 큰 차이점은 없으며(오히려 어떤 면에서는 클래스 구조보다 훨씬 더 유연합니다.), 바로 프로토타입 체인을 통해 클래스 개념을 구현할 수 있습니다. 이런 유형의 상속 방법을 위임 기반의 상속(delegation based inheritance)라고 합니다.(좀더 ECMAScript 스럽게는 프로토타입 기반 상속(prototype based inheritance) 이라고 할 수 있습니다.)


클래스 기반 예제에서 “A”, “B”, “C” 클래스를 생성하는 것처럼, ECMAScript에서는 “a”, “b”, “c” 객체를 생성합니다. 즉, 객체 “a” 에 “b”, “c” 의 공통 부분을 가지고 “b”, “c” 에는 각각의 추가적인 속성과 메소드를 가지도록 구현합니다.


  1. var a = {
  2. x : 10,
  3. calculate : function(z){
  4. return this.x + this.y + z
  5. }
  6. };

  7. var b = {
  8. y : 20,
  9. __proto__ : a
  10. };

  11. var c = {
  12. y : 30,
  13. __proto__ : a
  14. };

  15. //상속된 메소드를 호출합니다.
  16. b.calculrate(30); //60
  17. c.calculrate(40); //80

무척 간단하죠? 여기에서 “b” 및 “c” 객체는 “a” 에 정의된 calculrate 메소드에 접근했습니다. 이것이 가능한 것은 프로토타입 체인 때문입니다.


규칙은 매우 간단합니다. 만약 객체에서 어떤 속성 혹은 메소드를 찾을 수 없다면(즉, 객체가 해당 속성을 소유하고 있지 않다면), 다음의 프로토타입 체인에서 이 속성/메소드를 찾으려고 시도합니다. 이 프로토타입에서도 해당 속성/메소드가 발견되지 않으면, 다음으로 프로토타입에 대한 프로토타입을 검색합니다. 이것을 프로토타입 체인 전체에 걸쳐 반복하게 됩니다. (클래스 기반의 상속에서 상속되는 메소드를 클래스 체인에서 찾아 가는 것과 동일합니다.) 이와 같은 과정에서 처음으로 발견된 이름의 속성 혹은 메소드가 사용됩니다. 이렇게 발견된 속성을 상속된 속성이라고 합니다. 프로토타입 체인 전체를 탐색해도 같은 이름의 속성이 발견되지 않는 경우에는 undefined 값이 반환됩니다.


참고로, 상속된 메소드에서 this 객체를 사용할 경우, 메소드가 발견된 (프로토타입) 객체가 아니라 “원래”의 객체로 설정됩니다. 즉, 위의 예제에서 this.x는 프로토타입 체인의 구조에 따라 “a” 객체의 것이 사용되는 반면 this.y 는 각각 “b”, “c” 객체의 것이 사용됩니다.


어떤 객체에 대해 명시적으로 프로토타입을 지정하지 않을 경우,  __proto__ 는 기본값인  Object.prototype 을 사용합니다. Object.prototype 객체도 __proto__ 속성이 있지만, 이것은 프로토타입 체인의 끝으로서, null 이 할당되어 있습니다.


다음 그림이 “a”, “b”, “c” 객체의 상속 구조입니다.



그런데 경우에 따라서는 상태의 구조는 동일 또는 유사하면서도(즉, 속성이 동일) 상태값은 다르게 하고 싶을 수 있습니다. 이러한 경우, 객체를 특정한 형태로 생성할 수 있는 생성자 함수(constructor function)를 사용하게 됩니다.

생성자(Constructor)


생성자 함수에는 지정한 형태로 객체를 생성하는 것 외에도 편리한 기능이 있습니다. 새로 생성된 객체에 자동으로 프로토타입 객체를 설정해 줄 수 있다는 것입니다. 이 프로토타입 객체는 ConstructorFunction.prototype 속성에 저장됩니다.


예를 들어 위의 “b”, “c” 객체를 생성자 함수를 사용하여 다시 작성해 보겠습니다. 이 경우 “a” 객체(프로토타입)의 역할은 Foo.prototype 에 해당합니다.


  1. //생성자 함수
  2. function Foo(y){
  3. //특정한 형태의 객체를 생성합니다.
  4. //생성된 객체는 자신의 "y" 속성을 갖게 됩니다.
  5. this.y = y;
  6. }
  7. // 동시에 "Foo.prototype" 에는 새로 생성되는 객체의 프로토타입에 대한 참조가 저장됩니다.
  8. // 따라서 이를 이용해 공유/상속 메소드를 정의할 수 있습니다.
  9. // 이렇게 하면 위의 예와 동일해 집니다.
  10. //상속되는 속성 "x"
  11. Foo.prototype.x = 10;
  12. //상속되는 메소드 "calculrate"
  13. Foo.prototype.calculrate = function(z){
  14. return this.x + this.y + z;
  15. }

  16. //이제 "Foo" 라는 "패턴"을 이용하여 "b", "c" 객체를 생성합니다.
  17. var b = new Foo(20);
  18. var c = new Foo(30);

  19. //상속된 메소드를 호출합니다.
  20. b.calculrate(30); //60
  21. c.calculrate(40); //80

  22. // 예상대로 속성이 참조되었는지 확인해 보겠습니다.
  23. console.log(
  24. b.__proto__ === Foo.prototype, //true
  25. c.__proto__ === Foo.prototype, //true
  26. //아울러 "Foo.prototype"는 특별한 프로퍼티 "constructor" 를 생성합니다.
  27. //(Foo.prototype.constructor)는 생성자 함수 자체에 대한 참조입니다.
  28. //인스턴스 "    b" 및 "c" 는 위임(프로토타입)을 통하여 생성자를 찾을 수 있고, 생성자를 체크하는데 사용합니다.
  29. b.constructor === Foo, //true
  30. c.constructor === Foo, //true
  31. Foo.prototype.constructor === Foo, //true
  32. b.calculrate === b.__proto__.calculrate, //true
  33. b.__proto__.calculrate === Foo.prototype.calculrate //true
  34. );

아래 그림은 위의 코드를 객체간의 관계도로 그려본 그림입니다.



이 그림에서 볼 수 있는 것처럼, 모든 객체에는 프로토타입 객체가 존재합니다. 생성자 함수 “Foo” 자신도__proto__ 속성을 갖습니다. (값은  Function.prototype 입니다.) 또한 Function.prototype 는  __proto__ 를 통해 Object.prototype 를 참조합니다. 따라서 다시 말하면, Foo.prototype 은 단지 함수 “Foo”의 명시적인 속성중 하나로, “b” 및 “c” 객체의 프로토타입을 참조하고 있습니다.


공식적으로는, “클래스화”라는 개념을 고려해보면 (방금 Foo 라는 별도의 새로운 것을 “클래스”화 했습니다.) 생성자 함수와 프로토타입 객체의 조합을 “클래스”라고 할 수도 있습니다. 실제로, 예를 들어 Python 의 일급(first class) “동적 클래스”는, 속성 및 메소드에 대해 절대적으로 동일하게 구현하고 있습니다. 이러한 관점에서 Python 의 클래스는 ECMAScript 에서 사용되고 있는 위임 기반의 상속 모델을 보다 쉽게 표현한 것이라고 볼 수 있습니다.


이 주제에 대한 좀더 완벽한 설명은 ES3 시리즈 7장에서 볼 수 있습니다. Chapter 7.1 OOP The general Theroy 에서는 여러가지 다양한 OOP 파라다임과 스타일을 설명하고, ECMAScript와 비교하고 있으며, Chapter 7.2 OOP ECMAScript implementation 에서는 전적으로 ECMAScript에서의 OOP 에 대해서 설명하고 있습니다.


지금까지 기본적인 객체에 대해 살펴봤는데, 다음으로 ECMAScript 에서 런타임 프로그램의 실행(runtime program execution)이 어떻게 구현되어 있는지 살펴 보겠습니다. 이것은 “실행 컨텍스트 스택(execution context stack)” 이라고 하는데, 이것의 모든 요소도 물론 (추상적으로) 객체로 표시할 수 있습니다. 네 맞습니다. ECMAScript 에서는 거의 모든 곳에서 객체라는 개념 하에 동작됩니다.

실행 컨텍스트 스택


ECMAScript 의 코드는 3개의 유형으로 나눌 수 있습니다. “글로벌 코드(global code)”, “함수 코드(function code)”, “eval 코드(eval code)” 입니다. 각각의 코드는 각각의 실행 컨텍스트 내에서 평가됩니다. 


ECMAScript 에서 글로벌 컨텍스트는 단 하나만 존재하며, 함수 또는 eval 실행 컨텍스트는 여러 인스턴스가 존재할 수 있습니다. 함수가 호출될 때마다 함수 실행 컨텍스트에 들어가 “함수코드 유형(function code type)”을 평가합니다. 마찬가지로 eval 함수가 실행될 때마다 eval 실행 컨텍스트에 들어가 그 코드를 평가합니다.


참고로, 하나의 함수가 무한한 컨텍스트를 생성할 수 있습니다. 왜냐하면 함수를 호출할 때마다(재귀호출의 경우에도) 새로운 컨텍스트 상태(context state)를 가진 새로운 컨텍스트를 생성하기 때문입니다.


  1. function foo(bar){
  2. // 동일한 함수를 호출하지만, 호출할 때마다 다른 컨텍스트 상태(여기서는 모두 다른 bar 매개변수)
  3. // 를 가진 3개의 다른 실행 컨텍스를 생성하고 있습니다.
  4. }
  5. foo(10);
  6. foo(20);
  7. foo(30);

실행 컨텍스트에서는 또다른 컨텍스트를 활성화시킬 수 있습니다. 예를 들어 함수는 또 다른 함수를 호출할 수 있으며 글로벌 컨텍스트는 전역 함수를 호출할 수 있습니다. 논리적으로 이 과정은 스택(stack)으로 구현되며, 이를 실행 컨텍스트 스택(execution context stack)이라고 합니다.


새로운 컨텍스트를 활성화시키는 컨텍스트를 호출자(caller), 이때 활성화되는 컨텍스트를 피호출자(callee)라고 합니다. 어떤 피호출자는 또다른 피호출자에 대한 호출자가 될 수도 있습니다.( 예를 들어 글로벌 컨텍스에서 호출된 함수가 내부 함수를 호출하는 경우)


어떤 호출자가 피호출자를 활성화시키면(호출하면), 호출자는 실행을 일시 중단하고 제어 흐름을 피호출자에게 전달합니다. 피호출자는 스택에 올려지고, 실행(활성) 컨텍스트가 됩니다. 피호출자의 컨텍스트가 종료되면 제어는 다시 호출자에게 돌아갑니다. 그리고 마지막에 도달할 때까지 호출자 컨텍스트의 평가를 진행하게 됩니다. (그 과정에서 또다른 컨텍스트를 활성화시킬 수도 있습니다.) 피호출자는 단순히 return 될 수도 있고, 예외 exception 로 종료할 수 있습니다. throw 되었으나 catch 되지 않은 예외가 발생하면 (스택에서 빠져나와) 여러개의 컨텍스트를 한꺼번에 빠져나올 수도 있습니다.


즉, ECMAScript의 모든 프로그램 실행(program runtime)은 실행 컨텍스트(Execution Context 이하 EC)의 스택으로 표현됩니다. 이중 최상위에 있는 스택이 활성 컨텍스트입니다.



프로그램이 시작되면, 글로벌 실행 컨텍스트(global execution context)로 진입합니다. 글로벌 실행컨텍스트는 스택중 가장 아래에 있는 최초의 요소입니다. 글로벌 코드가 실행되면서 여러가지 초기화가 이루어지고, 필요한 객체와 함수를 생성합니다. 글로벌 컨텍스트가 실행되는 동안, 또 다른 (이미 생성되어 있는) 함수를 활성화시킬 수 있습니다. 그러면 각각의 실행 컨텍스트로 들어가게 되면서 새로운 컨텍스트 요소가 스택에 올라가게 됩니다. 초기화가 끝나면, 런타임 시스템은 (사용자의 마우스 클릭등) 이벤트(event)의 발생을 기다리게 됩니다. 이벤트가 발생하면 즉시 함수가 시작되며 새로운 컨텍스트로 들어갑니다.


아래 그림은 “Global EC” 즉 글로벌 컨텍스트에서 “EC1″ 이라는 함수 컨텍스트에 들어가고 빠져 나갈때 스택의 변화를 나타낸 것입니다.



이것이 ECMAScript 의 런타임 시스템이 코드 실행을 관리하고 방식입니다.


ECMAScript 의 실행 컨텍스트에 대한 자세한 내용은 Chapter 1. Execution context 에서 볼 수 있습니다. 앞서 말했던것 처럼 스택상의 모든 실행 컨텍스트는 객체로 표현될 수 있습니다. 그러면 이 객체의 구조와 코드가 샐행되기 위해서 어떤 상태(속성)가 필요한지 살펴보기로 합니다.

실행 컨텍스트


실행 컨텍스트는 추상적으로 간단한 객체로 표현할 수 있습니다. 모든 실행 컨텍스트는 그 컨텍스트에 속하는 코드의 실행 상태를 추적하는데 필요한 속성(컨텍스트의 상태(context's state)라 함)을 가지고 있습니다. 다음의 그림이 실행컨텍스트의 구조입니다.



이 세가지 필수 속성(변수 객체(Variable Object), 범위체인(Scope Chain), this 값(this Value) 외에도 구현에 따라서 다른 추가적인 상태가 필요할 수도 있습니다.


여기에서는 중요한 3가지 속성에 대해 자세한 내용을 살펴보도록 하겠습니다.

변수 객체(Variable Object)


변수 객체는 실행 컨텍스트에 관련된 데이터의 범위(scope of data)입니다. 변수객체는 컨텍스트와 연계된 특별한 객체로서, 컨텍스트에서 정의되는 변수와 함수를 저장합니다.

참고로, 함수 정의(function declarations)가 아닌 함수 표현식(Function Expression)은 변수객체에 포함되지 않습니다. 


변수 객체는 추상적인 개념입니다. 컨텍스트의 유형에 따라 물리적으로 다른 객체가 변수 객체 역할을 합니다. 예를 들어 글로벌 컨텍스트에서 변수 객체는 글로벌 객체 그 자체입니다. 글로벌 오브젝트의 속성 이름을 통해 전역 변수에 접근할 수 있는 것은 바로 이 때문입니다.


글로벌 실행 컨텍스트에 대한 예제를 살펴보겠습니다.


  1. var foo = 10;
  2. function bar(){} //함수 정의(function declaration, FD)
  3. (function baz(){}); //함수 표현식(function expression, FE)
  4. console.log(
  5. this.foo == foo, //true
  6. window.bar == bar //true
  7. );
  8. console.log(baz); //ReferenceError "baz" is not defined

즉, 이 글로벌 컨텍스트의 변수 객체(VO)는 다음과 같은 속성을 가진다고 할 수 있습니다.



함수 “baz” 는 함수 표현식(function expression)이기 때문에 변수 객체에 포함되지 않습니다. 그래서 “baz” 함수를 함수 외부에서 접근하고자 할 때 ReferenceError 가 발생하는 것입니다.


주의해야 할 것은 C와 C++ 등 다른 언어와 달리 ECMAScript 에서는 함수만이 새로운 범위를 생성합니다. 함수의 범위 내에서 정의되는 변수와 내부 함수는 함수의 외부에서 직접 액세스할 수 없으며, 전역 변수 객체를 변경시킬 수도 없습니다.


eval 을 호출하면 새로운 eval 실행 컨텍스트에 들어가게 됩니다. 그러나 eval 실행 컨텍스트는 전역 변수 객체 또는 eval 을 호출한 함수(호출자(caller))의 변수 객체를 그대로 사용합니다.


그럼 함수 컨텍스트의 경우 변수 객체는 어떻게 될까요? 함수 컨텍스트에서는 변수 객체가 활성 객체(Activation Object)가 표현됩니다.

활성 객체(Activation Object)


함수가 호출자(caller) 의해 활성화될 때(호출 될 때), 활성 객체(activation object)라는 특별한 객체가 생성됩니다. 활성 객체에는 함수의 매개변수(formal parameter)와 특별한arguments 객체(매개변수를 사상시킨 것으로 인덱스 접근도 가능)로 채워집니다. 이후 이 활성 객체가 함수 컨텍스트에서 변수 객체(variable object)로 사용됩니다.


간단하게 말하면 함수에 대한 변수 객체는 단순 변수 객체와 같지만, 변수와 함수 선언 외에도 매개변수 및 arguments 객체를 저장하며, 활성 객체라고 합니다.


다음 예제를 보시기 바랍니다.


  1. function foo(x, y){
  2. var z = 30;
  3. function bar(){} //FD(함수의 정의)
  4. (function baz(){}); //FE(함수 표현식)
  5. }
  6. foo(10, 20);

이 경우는 다음 그림과 같이 “foo” 함수 컨텍스트에 대해 활성 객체(AO : activation object)가 생성됩니다.


다시 말하지만 함수 표현식 “baz” 는 변수 객체(이 경우 활성 객체)에 포함되지 않습니다.


기타 다양한 케이스(변수와 함수정의의 “hoisting” 등)에 대한 보다 완벽한 설명은 Chapter 2. Variable object 를 참조하시기 바랍니다.


다음 주제로 계속 가 보겠습니다. 아시다시피 ECMAScript 에서는 내부 함수(inner functions)를 정의할 수 있고, 이러한 내부함수에서는 부모(parent) 함수의 변수와 글로벌 컨텍스트의 변수에 접근할 수 있습니다. 변수 객체를 컨텍스트에 대한 범위객체(scope object)라고 하면, 위에서 설명한 프로토타입 체인과 비슷하게 스코프체인(scope chain)이라는 것이 존재합니다.

범위 체인(Scope Chain)


범위 체인이란, 컨텍스트의 코드에 있는 식별자를 검색하기 위한 객체들의 목록(a list of objects)입니다.

이 규칙도 간단하며, 프로토타입 체인과도 비슷합니다. 즉, 자신의 범위(자신의 변수객체 또는 활성객체)에서 변수를 발견하지 못할 경우, 부모 변수 객체를 검색하고, 이와 같은 과정이 반복됩니다.


컨텍스트에서 식별자로는 변수, 함수 선언, 매개 변수 등의 이름(names)이 있습니다. 함수가 코드에서 지역 변수(또는 지역 함수와 매개변수)가 아닌 식별자를 발견하게 되면, 이러한 변수를 자유 변수(free variable)라고 합니다. 이 자유 변수를 검색할 때 범위 체인이 사용됩니다.


좀더 일반적으로는, 범위 체인은 모든 “부모 변수 객체(parent variable objects)”의 목록과, 해당 함수 고유의 변수/활성객체(범위 체인 앞에 존재함)입니다. 그러나 범위 체인에는 다른 객체가 추가될 수도 있습니다. 예를 들어, 콘텍스트 실행중 범위체인에 동적으로 추가되는 객체(“with 객체” 또는 “catch” 구문의 특별한 객체 등)가 그러한 예입니다.


식별자를 찾아갈(상위 탐색) 때  범위 체인이 검색되는데, 먼저 활성 객체를 탐색하고 (식별자를 활성 객체에서 찾지 못한 경우) 범위 체인의 최상위까지 반복해서 검색하게 됩니다. 다시 한번 강조하지만, 프로토타입 체인과 동일한 방식입니다.


  1. var x = 10;
  2. (function foo() {
  3. var y = 20;
  4. (function bar(){
  5. var z = 30;
  6. //"x"와 "y"는 "자유변수"로서, bar 자신의 범위 체인의 다음(bar 활성객체 이후에) 객체에서 찾을 수 있습니다.
  7. console.log(x+y+z);
  8. }) ();
  9. }) ();

범위 체인 객체사이의 관계는 다음 체인의 객체를 참조하는 __parent__ 속성을 사용해 연결한다고 생각해 볼 수 있습니다. 이 방법은 실제 Rhino 용 코드에서 시도해 볼 수 있는데, 바로 이 기법이 ES5 의 어휘 환경(lexical environments, outer link 라는 이름으로)에서 사용되고 있습니다. 범위 체인을 다른 방법으로 표현하면 간단한 배열이라고 할 수 있습니다. __parent__ 개념을 이용하면, 위의 코드 예제는 아래 그림과 같이 나타낼 수 있습니다. (부모 변수 객체들은 bar 함수의 [[Scope]] 속성에 저장됩니다.)



코드 실행과정에서 with 문과 catch 절 객체를 사용하면 범위 체인이 확장될 수 있습니다. 그리고 이러한 객체도 간단한 객체이기 때문에, 프로토타입(그리고 프로토타입 체인)을 갖습니다. 이 사실로부터, 범위 체인 탐색은 두가지 차원이라고 할 수 있습니다. (1) 먼저 범위체인 링크를 고려하고, (2) 모든 범위체인에 대하여 (링크가 프로토타입을 가질 경우) 링크의 프로토타입 체인 끝까지 검색하는 것입니다. 


예를 들어보겠습니다.


  1. Object.prototype.x = 10;
  2. var w = 20;
  3. var y = 30;
  4. //SpiderMonkey 전역 객체(즉 글로벌 컨텍스트 변수 객체)는 "Object.prototype" 을 계승하고 있습니다.
  5. //따라서 전역 변수로 정의되지 않은 "x" 를 프로토타입 체인을 통해 찾을 수 있습니다.
  6. console.log(x); //10
  7. (function foo() {
  8. //"foo" 지역 변수
  9. var w = 40;
  10. var x = 100;
  11. //"x" 는 "Object.prototype" 객체에서 찾아 집니다.
  12. //{ z : 50 } 라는 객체가 "Object.prototype" 를 계승하고 있기 때문입니다.
  13. with({ z : 50 }) {
  14. console.log(w, x, y, z); //40, 10, 30, 50
  15. }
  16. //with 객체가 범위 체인에서 제거되었습니다.
  17. //그 결과 다시 "x"는 "foo" 컨텍스트의 활성객체에서 찾아집니다. "w" 변수도 로컬 변수입니다.
  18. console.log(x, w); //100, 40
  19. //브라우저 실행 환경에서는 보통 아래와 같은 방법으로 지역 변수에 의해 숨겨진 전역 변수에 접근할 수 있습니다.
  20. console.log(window.w); //20
  21. })();

이 상황을 그림으로 나타내면 아래와 같습니다. (즉, __parent__ 링크를 추적하기 전에  __proto__ 링크를 먼저 탐색합니다.)



참고로, 모든 Javascript 엔진에서 글로벌 객체가 Object.prototype 을 상속받는 것은 아닙니다. 이 그림에서 나타낸 동작(글로벌 컨텍스트에서 “정의되지 않은” 않는 변수 x를 참조)은 SpiderMonkey 에서 테스트해 볼 수 있습니다.


부모가 되는 모든 변수 객체가 존재하는 한, 내부 함수에서 부모의 데이터를 얻는 것은 특별할 게 없습니다. 즉, 단순히 필요한 변수에 대해 범위 체인을 탐색하기만 하면 됩니다. 그러나 위에서 언급한 것과 같이 컨텍스트가 종료된 후에는 모든 상태와 컨텍스트 자체가 파괴됩니다. 동시에 내부 함수는 부모 함수로 반환될 수 있습니다. 게다가 이 반환된 함수는 나중에 다른 컨텍스트에서 활성화될 수 있습니다. 자유 변수 컨텍스트가 이미 사라졌다면 이런 함수 호출은 도대체 어떻게 활성화 될까요? 일반적으로 이러한 문제의 해결점을 찾아주는 것을 ECMAScript 에서는 (어휘적) (클로져(lexical) closure) 라고 합니다,

클로저(Closure)


ECMAScript 에서 함수는 “일급 객체(first class object)” 입니다. 이 말은 함수를 다른 함수의 매개변수로 전달할 수 있다는 뜻입니다. (이 경우 매개변수로 전달되는 함수는 “functional arguments” 을 줄여서 “funargs” 라고 함), “funargs” 를 받은 함수는 고차 함수(higher-order functions) 또는 좀더 수학적으로 표현하면 연산자(operators)라고 합니다. 또한 동시에 함수가 다른 함수를 반환할 수도 있습니다. 다른 함수를 반환하는 함수는 “function valued” 함수(혹은 함수 값을 갖는 함수)라고 합니다.


“funarg” 및 “functional values” 에는 이론적인 문제가 두가지 있습니다. 이 두가지 문제를 일반화하여 “funarg 문제" 또는 “functional argument 문제"라고 합니다. 바로 “funarg 문제” 를 해결하기 위해 클로져라는 개념이 만들어졌습니다.


그럼 이 두가지 문제에 대해 자세히 살펴 보겠습니다. (ECMAScript 에서는 그림에서 언급된 함수의 [[Scope]] 내부 속성을 사용하여 이 두가지 문제를 해결하였음을 알아보겠습니다.)


“funarg 문제” 의 첫번째는 “상승 funarg 문제” 입니다. 이것은 함수가 다른 함수에서 위로(외부로) 반환될 때 위에서 이미 언급한 자유 변수(free variable)를 참조하는 경우 발생합니다. 부모의 컨텍스트가 종료 후에도 부모 컨텍스트의 변수를 참조할 수 있도록, 내부 함수가 생성되는 순간 그 함수의 [[Scope]] 속성에 부모의 범위 체인을 보존합니다. 그리고 함수가 호출되었을 때, 활성 객체와 (미리 저장해 놓은) [[Scope]] 속성를 조합하여 그 함수의 컨텍스트가 생성되는 것입니다.


범위 체인 = 활성 객체 + [[Scope]]

가장 중요한 점은 - 함수가 생성되는 시점에 - 함수는 그때의 부모 범위 체인을 (자신의 [[Scope]] 로) 저장한다는 것을 다시 확인할 수 있습니다. 이 저장된 범위 체인이 함수가 호출될 때 변수 검색 대상으로 사용되기 때문입니다.


  1. function foo() {
  2. var x = 10;
  3. return function bar(){
  4. console.log(x);
  5. };
  6. }
  7. //"foo" 함수는 함수를 반환하고 반환된 이 함수는 자유 변수 "x" 를 사용합니다.
  8. var returnedFunction = foo();
  9. //전역 변수 "x"를 설정합니다.
  10. var x = 20;
  11. //반환 함수를 실행합니다.
  12. returnedFunction(); //20이 아니라 10이 됩니다.

이런 스타일의 범위를 정적(혹은 어휘적) 범위(static (or lexical) scope)라고 합니다. 변수 “x” 는 반환된 “bar” 함수에 저장된 [[Scope]] 내에서 탐색됩니다. 일반적으로 위의 예제에서 “x” 가 10 이 아니라 20 이 되는 동적 범위(dynamic scope)라는 것도 존재합니다. 그러나 ECMAScript 에서는 동적 범위가 사용되지 않습니다.


“funarg 문제”의 두번째는 “하향 funarg 문제” 입니다. 이 경우 부모 컨텍스트는 존재해도 식별자 탐색은 모호해 질 수 있습니다. 문제는 어떤 부모의 범위에서 식별자 값을 탐색할 것인가 라는 것입니다. 함수 생성시 정적으로 저장된 범위를 사용하는가 또는 런타임에 동적으로 생성된 범위(호출자(caller)의 범위)를 사용하는가? 이러한 애매함을 회피하고 클로저를 형성하기 위해, 정적 범위가 사용되게 됩니다.


  1. //전역 "x"
  2. var x = 10;
  3. //전역 함수 "foo"
  4. function foo(){
  5. console.log(x);
  6. }
  7. (function(funArg) {
  8. //지역 "x"
  9. var x = 20;
  10. //전역 "x" 가 호출되므로 모호함이 없습니다.
  11. //왜냐하면 "foo" 함수의 [[Scope]] 에 정적으로 저장되기 떄문입니다.
  12. //하지만 "funarg"를 활성화한 caller 범위상의 "x" 는 그렇지 않습니다.
  13. funArg(); //20이 아니라 10
  14. }) (foo); //"foo"를 "funarg"로 하향 전달

우리는 ‘어떤 언어에서 정적 범위는 클로져를 가질 필수조건이다.’ 라고 결론 내릴 수도 있습니다. 그러나 일부 언어에서는 동적 및 정적 범위를 모두 함께 제공하고, 프로그래머가 자유롭게 선택할 수 있는 경우가 있습니다. ECMAScript 는 정적 범위에만 채용되고 있기 때문에 (“funarg 문제” 두가지가 해결됨), 결론적으로 ECMAScript 는 클로저를 완벽히 지원합니다(ECMAScript has complete support of closures). 기술적으로는 [[Scope]] 내부 속성을 사용하여 구현됩니다. 이제 클로저의 정확한 정의를 살펴봅시다.


클로저는 코드 블록(ECMAScript 에서는 함수)의 조합으로서, 정적 또는 어휘적으로 모든 부모 범위를 저장한다. 따라서 이렇게 저장된 범위를 통해 함수는 손쉽게 자유 변수들을 참조할 수 있다.

참고로, 모든 (일반) 함수는 생성되는 순간 [[Scope]] 내부 속성을 저장하므로, 이론적으로 ECMAScript 의 모든 함수는 클로져라고 할 수 있습니다.


또 하나의 중요한 것은, 여러개의 함수가 동일한 부모 범위를 가질 수 있는 것입니다. (예를 들어 두개의 내부/전역 함수가 있을 경우 지극히 정상적 상황입니다.) 이 경우 함수 각각의[[Scope]] 속성에 저장된 변수는, 동일한 부모 범위 체인을 가진 모든 함수가 공유하게 됩니다.


즉 하나의 클로저에 의해 만들어진 변수의 변화는 다른 클로저에서 이 변수를 참조할 때 반영됩니다.


  1. function baz() {
  2. var x = 1;
  3. return {
  4. foo : function foo() { return ++x; },
  5. bar : function bar() { return --x; }
  6. };
  7. };

var closures = baz();
console.log (
closures.foo(),    //2
closures.bur()    //1
);

이 코드를 그림으로 나타내면 아래의 그림과 같습니다.


바로 이로 인해 루프 속에서 여러개의 함수를 생성할 때 혼동을 일으키게 됩니다. 루프 내부에서 생성된 함수에서 루프 카운터를 사용하면, 때때로 모든 함수에서 카운터가 같은 값을 표시하는 의도하지 않는 결과가 나오는 경우가 있습니다. 이제 그 이유가 분명해졌습니다. 모든 함수가 동일한[[Scope]] 를 공유하므로, 마지막에 할당된 루프 카운터 값이 저장되어 있기 때문입니다.


  1. var data = [];
  2. for(var k = 0; k< 3; k++){
  3. data[k] = function(){
  4. alert(k);
  5. };
  6. }
  7. data[0](); //0이 아닌 3
  8. data[1](); //1이 아닌 3
  9. data[2](); //2가 아닌 3

이 문제를 해결하려면 몇 가지 방법이 있습니다. 그중 하나는 아래의 예제와 같이 추가적인 함수를 이용하여 범위 체인에 객체를 추가하는 방법입니다.


  1. var data = [];
  2. for(var k = 0; k < 3; k++) {
  3. data[k] = (function(x){
  4. return function(){
  5. alert(k);
  6. };
  7. })(k); //"k" 값을 전달
  8. }
  9. //이번에는 정답
  10. data[0](); //0
  11. data[1](); //1
  12. data[2](); //2

클로저 및 실제적인 응용에 대해 더 관심이 있는 분은 Chapter 6. Closure 를 참조하세요. 범위 체인은 그 이름대로 Chapter 4. Scope chain 라는 장을 참조하시면 됩니다.


그러면 다음 항목으로 이동합니다. 실행 컨텍스트의 마지막 속성인 this 라는 값에 대한 고찰입니다.

this 값


“this” 는 실행 컨텍스트와 관련된 특별한 객체입니다. 따라서 컨텍스트 객체(context object)라고 명명할 수도 있습니다. (즉, 실행 컨텍스트가 활성화 되는 컨텍스트의 객체라는 의미)

어떤 객체든지 컨텍스트의 “this” 가 될 수 있습니다. 나는 여기서 다시 ECMAScript 의 실행 컨텍스트에 대한 오해, 특히 “this” 에 대해 명확하게 하고 싶습니다. 종종 “this” 를 변수 객체에 속한 속성이라고 잘못 설명하는 경우가 있습니다. 최근 이 책 에서도 실수가 발견되었습니다.(물론 이 챕터 자체는 매우 좋은 내용을 담고 있습니다.) 그래서 다시 설명해 봅니다.


“this” 는 실행 컨텍스트의 속성(property of execution context)이며 변수 객체의 속성이 아닙니다.

이 특성은 매우 중요합니다. 왜냐하면 “this” 는 여타 변수와는 달리 결코 식별자 탐색 과정(identifier resolution process)에 참여하지 않기 때문입니다. 즉 코드에서 “this” 를 만나면 그 값은 실행 컨텍스트에서 직접 참조되고, 범위 체인 탐색은 행해지지 않습니다. “this” 값은 컨텍스트에 들어간 그 순간에 한 번만 결정되는 것입니다.


그런데 Python의 경우에는 ECMAScript와는 달리, 메소드에 “self” 라는 매개변수를 전달할 수 있고, 변수와 마찬가지로 식별자를 해결하고 또한 실행중에 다른 값으로 할당될 수도 있습니다. ECMAScript 에서는 “this” 에 새로운 값을 할당할 수 없습니다. 왜냐하면 “this” 는 변수가 아니며, 변수 객체에 저장되지 않기 때문입니다.


글로벌 컨텍스트내에서는 “this” 는 글로벌 객체 그 자체입니다.(즉, “this” 는 변수 객체와 동일합니다.)


  1. var x = 10;
  2. console.log(
  3. x, //10
  4. this.x, //10
  5. window.x //10
  6. );

함수 컨텍스트에서는, 모든 함수 호출마다 “this” 객체가 달라질 수 있습니다. 여기에서 this 값은 호출 표현식의 형태(즉, 함수가 활성화되는 방법)를 통해 호출자엑 의해 전해집니다. 예를 들어 아래 예에서 함수 “foo” 는 피호출자(callee)로서 호출자(caller) 인 글로벌 컨텍스트에게 호출됩니다. 아래의 코드는 함수 코드는 동일하지만, “this” 값이 함수 호출 방법(함수 활성화 방법)에 따라 어떻게 달리 제공되는지 살펴보겠습니다.


  1. //"foo" 함수의 코드는 전혀 변경되지 않습니다.
  2. //그러나 모든 활성화 방법에 따라 "this" 값은 다를 수 있습니다.
  3. function foo(){
  4. alert(this);
  5. }

  6. //caller가 "foo" (callee) 를 활성화하고 callee 에 "this" 를 제공합니다.
  7. foo(); //전역 객체
  8. foo.prototype.constructor(); // foo.prototype
  9. var bar = {
  10. baz : foo
  11. };
  12. bar.baz(); //bar
  13. (bar.baz)(); //이것도 bar
  14. (bar.baz = bar.baz)(); //그러나 여기에서는 전역 객체
  15. (bar.baz, baz.baz)(); //이것도 전역 객체
  16. (false || bar.baz)(); //이것도 전역 객체

  17. var otherFoo = "bar.baz";
  18. outerFoo(); //또한 이것도 전역 객체

각각의 함수 호출에서 왜(그리고 더욱 중요하게… 어떻게) “this” 값을 변화하는지에 대대 자세히 알고 싶다면 Chapter 3. This 를 읽어보시기 바랍니다. 위의 예제의 모든것을 자세히 설명하고 있습니다.

결론


지금까지는 간단한 개요입니다. 그다지 간단하지는 않았지만, 이 모든 사항을 자세히 설명하려면 별도의 책한권이 필요합니다. 우리는 ECMAScript에서 중요한 두가지 주제인 함수(함수 선언과 함수 표현식과 같은 함수 유형에 따른 차이)와 평가 전략(evaluation strategy)에 대해서는 다루지 않았습니다. 이 두가지 주제는 Chapter 5. Functions 와 Chapter 8. Evaluation Strategy를 참고하시기 바랍니다.


의견이나 질문 혹은 추가하고 싶은 내용이 있으시면 댓글로 남겨주세요. 같이 토론해보면 좋겠네요. 

Written by: Dmitry A. Soshnikov
Published on: 2010-09-02