자바스크립트 실행 컨텍스트(Execution Context) 3 - 실행 컨텍스트의 생성과 실행

실행 컨텍스트 관련 첫번째 포스트인 자바스크립트 실행 컨텍스트(Execution Context) 에서는 실행 컨텍스트란 무엇인지 그리고 유형은 어떠한 것들이 있으며 Javascript 엔진이 실행 컨텍스트를 어떻게 관리하는지 알아보았습니다.

실행 컨텍스트 관련 두번째 포스트인 자바스크립트 실행 컨텍스트(Execution Context) 2 - 정의와 구조 에서는 실행 컨텍스트의 정의와 ES5 이후의 Scope Chain의 변화 그리고 실행 컨텍스트의 세부구조에 대해 다뤄봤습니다.

그리고 마지막 포스트로는 실행 컨텍스트의 생성과 실행 과정에 대해 참고중이던 영문 포스트의 일부분을 번역하면서 구조와 각 세부 프로퍼티에 대해 정리해보고, Creation PhaseExecution Phase 에 대해서 알아보며 끝마치도록 하겠습니다.

참고 영문 포스트: Understanding Execution Context and Execution Stack in Javascript

번역 포스트의 “How is the Execution Context created?” 부분만 발췌하여 번역하였음을 알려드립니다.


Javascript 실행 컨텍스트와 실행 스택 이해하기

Javascript 프로그램이 내부적으로 실행되는 방법 학습하기

How is the Execution Context created?

지금까지, 우리는 Javascript 엔진이 실행 컨텍스트를 어떻게 관리하는지 살펴봤습니다. 그럼 지금부터는 Javascript 엔진이 실행 컨텍스트를 생성하는 방법에 대해 이해해보록 하겠습니다.

실행 컨텍스트는 두 단계로 나뉘어 생성됩니다:

  1. 생성 단계 (Creation Phase)
  2. 실행 단계 (Execution Phase)

1. 생성 단계

실행 컨텍스트는 생성 단계 동안 아래의 컨포넌트가 발생합니다:

  1. LexicalEnvironment 컴포넌트가 생성된다.
  2. VariableEnvironment 컴포넌트가 생성된다.

그럼 실행 컨텍스트는 개념적으론 아래와 같이 나타낼 수 있다.

1
2
3
4
ExecutionContext = {
LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
VariableEnvironment = <ref. to VariableEnvironment in memory>,
}

Lexical Environment

The official ES6 문서에 정의된 Lexical Environment 는

Lexical Environment는 ECMAScript 코드의 lexical 중첩 구조 상에서 특정 변수와 함수에 대한 인식자(Identifier)들을 정의하는데 사용되는 명세 유형입니다. Lexical Environment는 Environment Record와 outer Lexical Environment에 대한 가능한한 null 참조로 구성되어 있다.

간단하게 말해, lexical environment인식자(identifier)-변수(variable) 매핑(mapping) 을 유지하고 있는 구조입니다. (여기서 identifier 란 변수/함수들의 이름을 참고하는 것이고, variable 은 [함수 객체와 배열 객체를 포함하는] 실질적 객체 또는 기본값에 대한 참조값이다.)

예를 들어, 아래의 코드 조각을 생각해보자.

1
2
3
4
5
6
var a = 20;
var b = 40;

function foo() {
console.log('bar');
}

위 코드 조각의 lexical environment는 아래와 같이 나타낼 수 있다.

1
2
3
4
5
lexicalEnvironment = {
a: 20,
b: 40,
foo: <ref. to foo function>
}

각 Lexical Environment는 세가지 컴포넌트를 가지고 있다.

  1. Environment Record
  2. Reference to the outer environment
  3. This binding

Environment Record

Environment Record는 lexical environment 안에 저장된 변수와 함수의 선언이 있는 곳이다.

또한, environment record 는 두가지 타입이 있습니다.

  • Declarative environment record — 이름에서 알 수 있듯이 변수와 함수의 선언에 대한 것으로 추측할 수 있다. 함수 코드에 대한 lexical environment는 declarative environment record를 포함하고 있다.
  • Object environment record — 전역(global) 코드의 lexical environment는 objective environment record를 포함하고 있다. 변수와 함수에 대한 선언과 구분지어, object environment record는 또한 전역 바인딩 객체(global binding object) (browser의 경우 window 객체)를 저장한다. 그리고 바인딩된 객체에 대한 각각의 속성(브라우저의 경우, window 객체에 browser에 의해 제공되는 속성과 메서드들이 포함된다)에 대해 새로운 엔트리가 레코드 안에 생성된다.

Note — 함수 코드 를 위해서, Environment Record 는 또한 인덱스와 함수에 전달된 인자 사이의 관계에 대한 매핑과 함수로 전달된 인자들에 대한 길이(숫자, number) 에 대한 정보를 담고있는 인자(arguments) 객체를 포함한다.

1
2
3
4
5
6
function foo(a, b) {
var c = a + b;
}
foo(2, 3);
// argument object
Arguments: {0: 2, 1: 3, length: 2},

Reference to the Outer Environment

reference to the outer environment 는 현재 실행 컨텍스트의 외부(outer) lexical environment에 대해 접근하는 것을 의미합니다. 이것은 JavaScript engine이 현재 lexical environment에서 변수를 찾지 못한다면 외부 환경(outer environment)에서 찾을 수 있음을 의미합니다.

This Binding

이 컴포넌트에서는 this 의 값을 결정하거나 설정합니다.

전역(global) 실행 컨텍스트에서 this 는 전역 객체를 참조합니다. (브라우저에서 this 는 Window 객체를 참조한다.)

함수 실행 컨텍스트에서 this 는 함수가 호출되는 방식에 따라 달라진다. 만약 객체 참조 방식에 따라 함수가 호출될 경우, this 값은 해당 객체로 설정되고 그렇지 않으면 this 는 전역 객체 또는 (strict mode 에서는) undefined 로 설정된다. 예를 들어,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const person = {
name: 'peter',
birthYear: 1994,
calcAge: function() {
console.log(2018 - this.birthYear);
}
}

person.calcAge();
// 'calcAge'가 'person' 객체 참조로 호출됐기 때문에 'this'는 'person'를 참조한다.

const calculateAge = person.calcAge;
calculateAge();
// 어떠한 객체 참조도 주어지지 않았기 때문에 'this'는 전역 window 객체를 참조한다.

추상적으로, lexical environment는 아래와 같은 가상의 코드로 나타낼 수 있다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 인식자(Identifier) binding은 여기로 이동한다.
}
outer: <null>,
this: <global object>
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 인식자(Identifier) binding은 여기로 이동한다.
}
outer: <Global or outer function environment reference>,
this: <depends on how function is called>
}
}

Variable Environment:

또한 Environment Record는 실행 컨텍스트에서 VariableStatements 에 의해 생성된 binding을 보관하는 Lexical Environment 이다.

즉, Variable Environment 또한 Lexical Environment이며 위에서 정의한 것처럼 Lexical Environment의 모든 속성과 컴포넌트들을 가지고 있다.

ES6에서 LexicalEnvironment 컴포넌트 and the VariableEnvironment 컴포넌트의 한가지 차이점은 LexicalEnvironment 는 함수 선언과 변수 (letconst) 바인딩을 저장하는데 사용되고, VariableEnvironment 는 오직 변수 (var) 바인딩을 저장하는데 사용된다.

실행 단계

이 단계에서는, 모든 변수에 대한 할당이 끝났으며 마침내 코드가 실행된다.

Example

이상의 개념을 이해하기 위해 몇 가지 예를 보자.

1
2
3
4
5
6
7
8
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);

이 코드를 실행시키면, Javascript 엔진은 전역(global) 코드를 실행하기 위한 전역 실행 컨텍스트를 생성한다. 그럼 생성 단계에서 전역 실행 컨텍스트는 아래와 같은 가상의 코드처럼 될 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 인식자(Identifier) binding은 여기로 이동한다.
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>,
ThisBinding: <Global Object>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 인식자(Identifier) binding은 여기로 이동한다.
c: undefined,
}
outer: <null>,
ThisBinding: <Global Object>
}
}

실행 단계 동안에는, 변수 할당이 끝났으며 전역 실행 컨텍스트는 아래와 같은 가상의 코드처럼 될 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 인식자(Identifier) binding은 여기로 이동한다.
a: 20,
b: 30,
multiply: < func >
}
outer: <null>,
ThisBinding: <Global Object>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 인식자(Identifier) binding은 여기로 이동한다.
c: undefined,
}
outer: <null>,
ThisBinding: <Global Object>
}
}

multiply(20, 30) 함수에 대한 호출이 발생했을때, 새로운 함수(function) 실행 컨텍스트가 함수 코드를 실행하기 위해 생성된다. 그리고 함수 실행 컨텍스트는 생성 단계동안 아래와 같은 가상의 코드처럼 될 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 인식자(Identifier) binding은 여기로 이동한다.
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 인식자(Identifier) binding은 여기로 이동한다.
g: undefined
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}

이후, 실행 컨텍스트는 함수(function) 내에서 변수 할당이 끝났다는 의미로 실행 단계를 거치고 함수 실행 컨텍스트는 실행 단계동안 아래와 같은 가상의 코드처럼 될 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 인식자(Identifier) binding은 여기로 이동한다.
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 인식자(Identifier) binding은 여기로 이동한다.
g: 20
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}

함수가 완료된 후에, 반환된 값은 c 에 저장되고, 전역(global) lexical environment가 업데이트 됩니다. 그 후, 전역 코크는 완료되고, 프로그램은 끝난다.

Note — 여러분은 letconst 로 정의된 변수는 생성 단계 동안에 그 어떠한 값(value)도 연결되지 않으며 var 로 정의된 변수는 undefined 로 설정된다는 것을 주의해야 합니다.

이것은 생성 단계에서 코드에서 변수와 함수 선언에 대해 스캔하고, 함수 선언이 환경에 함수 전체적으로 저장되는 동안, 변수는 처음에 (var 의 경우) undefined 로 설정되거나 (letconst 의 경우) 비초기화(uninitialized) 상태로 유지되기 때문입니다.

이게 여러분이 letconst 변수가 선언되기 전에 접근하려고 할때 참조 에러(reference error)가 발생하는 것과 달리 var 로 선언된 변수는 (undefined 로) 선언되기 전에 접근할 수 있는 이유입니다.

이것이 바로, 우리가 호이스팅(hoisting) 이라고 부르는 것입니다.

Note — 실행단계 동안에, Javascript 엔진이 소스 코드(source code) 상에서 선언한 실제 위치에서 let 변수에 대한 값을 찾지 못한다면 이 변수에는 undefined 값이 할당될 것입니다.

Conclusion

우리는 Javascript 프로그램들이 내부적으로 어떻게 실행되는지 살펴봤습니다. 여려분이 멋진 Javascript 개발자가 되기 위해 이러한 모든 개념들을 배울 필요는 없지만, 위와 같은 개념에 대한 왠만한 이해력은 Hoisting, Scope, and Closure 와 같은 다른 개념들도 더 쉽고 더 깊게 이해할 수 있도록 도울 것입니다.