자바스크립트 기초 개념에는 호이스팅, 스코프 등이 있다.
이 개념들의 동작 원리를 설명할 때 많이 등장하는 단어가 실행 컨텍스트(Execution Context)이다.
도대체 이 실행 컨텍스트라는건 뭘까?
본 게시글은 Sukhjinder Arora님 블로그의 글을 참고하여 작성하였습니다
실행 컨텍스트(Execution Context)
란 자바스크립트 코드가 평가되고 실행되는 환경에 대한 추상적인 개념이다.
자바스크립트에서 어떤 코드가 동작하든, 그것은 실행 컨텍스트
내에서 이뤄진다.
자바스크립트에는 3개의 실행 컨텍스트
타입이 있다.
전역 실행 컨텍스트(Global Execution Context)
전역 실행 컨텍스트
에 속한다.window
객체라고 불리는 전역 객체를 생성하기도 하며(브라우저의 경우),this
의 값을 전역 객체와 같게 세팅한다.전역 실행 컨텍스트
만이 존재한다.함수 실행 컨텍스트(Functional Execution Context)
함수 실행 컨텍스트
는 얼마든지(any number of) 존재할 수 있다.Eval Function Context
- 정확한 의미 파악이 힘들어 해석 생략eval
함수 내에서 실행되는 코드 또한 그들 본인의 실행 컨텍스트를 가진다.eval
은 보통 자바스크립트 개발자들이 사용하지 않기 때문에 이는 설명을 생략하고자한다.다른 프로그래밍 언어에서 Calling Stack
이라고도 불리는 실행 스택(Execution Stack)
은
코드가 실행되는 동안 생성되는 모든 실행 컨텍스트
를 저장하는데 사용되는
LIFO(Last In First Out)
구조의 스택이다.
자바스크립트 엔진이 당신의 스크립트를 처음 마주쳤을 때,
엔진은 전역 실행 컨텍스트
를 생성하고, 그것을 현재(current) 실행 스택
에 넣는다(pushes).
엔진이 함수 호출을 찾을 때마다, 그 함수를 위해 새로운 실행 컨텍스트
를 만들고,
그것을 스택의 가장 위에 넣는다.
엔진은 스택의 가장 위에 있는 실행 컨텍스트
를 가지는 함수를 실행한다.
이 함수가 완료되었을 때, 그것의 실행 스택
을 스택에서 빼내고(popped off),
엔진(control, 필자의 의역입니다)은 그 아래에 있던 컨텍스트에 접근한다.
이제 코드 예시를 살펴보자.
let a = "Hello World!";
function first() {
console.log("Inside first function");
second();
console.log("Again inside first function");
}
function second() {
console.log("Inside second function");
}
first();
console.log("Inside Global Execution Context");
이 코드의 실행 컨텍스트 스택(Execution Context Stack)
은 다음과 같다.
Sukhjinder Arora님 블로그에 있는 이미지입니다
위 코드가 브라우저에 불려왔을 때(loads),
자바스크립트 엔진은 전역 실행 컨텍스트
를 생성하고 그것을 현재 실행 스택
에 넣는다.
함수 first()
의 호출을 마주쳤을 때,
자바스크립트 엔진은 그 함수를 위한 새로운 실행 컨텍스트
를 생성하고
현재 실행 스택
의 최상단에 그것을 넣는다.
함수 second()
가 함수 first()
내에서 호출되었을 때,
자바스크립트 엔진은 그 함수(함수 second)를 위한 새로운 실행 컨텍스트
를 생성하고,
현재 실행 스택
의 최상단에 그 것을 넣는다.
함수 second()
가 완료되었을 때, 그 것의 실행 컨텍스틀
를 현재 스택에서 빼내고,
엔진이 그 아래에 있는 실행 컨텍스트
(함수 first의 컨텍스트)에 도달한다.
함수 first()
가 완료되었을 때,
실행 스택
에서 빼내지고 엔진은 전역 실행 컨텍스트
에 도달한다.
한번 모든 코드가 실행되고나면, 자바스크립트 엔진은 전역 실행 컨텍스트
를 현재의 스택에서 제거한다.
지금까지 자바스크립트 엔진이 실행 컨텍스트
를 어떻게 다루는지 살펴봤으니,
자바스크립트 엔진이 실행 컨텍스트
를 어떻게 생성하는지 알아보자.
실행 컨텍스트
는 2 단계를 거쳐서 생성된다.
생성 단계(Creation Phase)
실행 단계(Execution Phase)
실행 컨텍스트
는 생성 단계(Creation Phase)
중 생성된다.
그 때 일어나는 것은(happen) 아래와 같다.
Lexical Environment
요소(component)가 생성된다.Variable Environment
요소가 생성된다.따라서, 실행 컨텍스트
는 개념적으로는 아래와 같이 표현될 수 있다.
ExecutionContext = {
LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
VariableEnvironment = <ref. to VariableEnvironment in memory>,
}
공식 ES6 문서에서는 Lexical Environment
를 아래와 같이 정의한다.
Lexical Environment는 특정한 변수에 대한 Identifiers의 관계와 ECMAScript 코드의 lexical nesting structure에 기반한 함수를 정의하기 위해 사용되는 특정한 타입이다.
Lexical Environment는 Environment Record와 외부 Lexical Environment에 대해 가능한 null 참조(possibly null reference)로 구성된다.
Lexical Environment
는 identifier-variable mapping
을 담고 있는 구조라고 보면 된다.
여기서 identifier
는 변수, 함수의 이름을 가리키며,
variable
은 함수 객체, 배열 객체, 원시값을 포함하는 실제 객체(actual object)를 가리킨다.
아래 코드를 예시로 들어보자.
var a = 20;
var b = 40;
function foo() {
console.log('bar');
}
위 코드의 Lexical Environment
는 아래와 같다.
lexicalEnvironment = {
a: 20,
b: 40,
foo: <ref. to foo function>
}
각각의 Lexical Environment
는 3개의 요소(components)를 가진다.
불분명한 해석을 피하기위해 번역은 생략했습니다
Environment Record
Reference to the outer environment
This binding
Environment Record
는 Lexical Environment
내에 변수와 함수의 선언이 저장되는 장소이다.
이 때, 2가지 타입의 Environment Record
가 존재한다.
Declarative environment record
Lexical Environment
는 Declarative environment record
를 포함한다.Object environment record
Lexical Environment
는 Object environment record
를 포함한다.Object environment record
또한 전역 바인딩 객체(global binding object)을 저장한다(브라우저에서의 window 객체).entry
가 record
에 생성된다.참고로, 함수 코드(function code)의 경우, Environment record
는 함수를 지나는 indexes와 인자(arguments) 사이의 관계(mapping, 필자의 의역입니다)와 함수를 지나는 인자(arguments)의 길이(혹은 개수)를 담고있는 arguments
객체를 포함한다.
아, arguments
객체는 다음과 같다.
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
는 그것의 외부(outer) Lexical Environment
에 접근권(access)을 가지고 있다는 것을 의미한다.
이는 자바스크립트 엔진이 만약 현재(current) Lexical Environment
에서 변수를 찾지 못할 경우 외부 Environment
에서 변수를 찾을 수도 있다는 것을 의미한다.
이 요소(component)에서는 this
의 값을 결정하거나 세팅(set)한다.
전역 실행 컨텍스트(global execution context)
에서 this
의 값은 전역 객체(global object)를 가리킨다(브라우저에서 this
는 window 객체를 가리킨다.).
함수 실행 컨텍스트(function execution context)
에서 this
의 값은 함수가 어떻게 호출됐느냐에 따라 달라진다.
만약, 함수가 객체 참조(object reference)로 호출됐다면, this
의 값은 바로 그 객체로 세팅된다.
그렇지 않은 경우에는, this
의 값이 전역 객체(global object) 혹은 undefined
(엄격 모드(strict mode)에서)로 세팅된다.
아래 예시를 보자.
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
는 추상적으로 다음과 같을 것이다.
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier 바인딩은 여기로 간다.
}
outer: <null>,
this: <global object>
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier 바인딩은 여기로 간다.
}
outer: <Global or outer function environment reference>,
this: <depends on how function is called>
}
}
Variable Environment
또한 실행 컨텍스트
와 함께 VariableStatements
에 의해 생성되는 바인딩을 가지는 Environment Record
가 있는 Lexical Environment
이다.
방금 말했듯이, Variable Environment
또한 Lexical Environment
이다.
따라서, 위에서 언급된 Lexical Environment
의 모든 속성(properties)과 요소(components)를 가지게 된다.
ES6에서, Lexical Environment
와 Variable Environment
의 한 가지 차이는,
전자는 함수의 선언과, 변수(let, const) 바인딩 저장에 사용되는 반면,
후자는 변수(var) 바인딩 저장에만 사용된다는 것이다.
이 단계에서는 모든 변수의 할당이 끝났고, 코드가 마침내 실행된다.
예시를 살펴보자.
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
위 코드가 실행될 때,
자바스크립트 엔진은 전역 코드(global code)를 실행하기 위해 전역 실행 컨텍스트
를 생성한다.
따라서, 전역 실행 컨텍스트
는 생성 단계(Creation Phase)
에서 아래와 같이 보일 것이다.
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier 바인딩은 여기로 간다.
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>,
ThisBinding: <Global Object>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier 바인딩은 여기로 간다.
c: undefined,
}
outer: <null>,
ThisBinding: <Global Object>
}
}
혹시나, 각 변수들의 값이 왜 저런지 이해가 되지않는다면 필자의 호이스팅에 대한 게시글을 참고해주세요.
실행 단계(Execution Phase)
동안, 변수 할당이 완료된다.
따라서, 전역 실행 컨텍스트
는 실행 단계
에서 아래와 같이 보일 것이다.
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier 바인딩은 여기로 간다.
a: 20,
b: 30,
multiply: < func >
}
outer: <null>,
ThisBinding: <Global Object>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier 바인딩은 여기로 간다.
c: undefined,
}
outer: <null>,
ThisBinding: <Global Object>
}
}
함수 multiply(20, 30)
의 호출을 마주쳤을 때,
함수 코드를 실행하기 위해 새로운 함수 실행 컨텍스트
가 생성된다.
따라서, 생성 단계
에서의 함수 실행 컨텍스트
는 아래와 같이 보일 것이다.
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier 바인딩은 여기로 간다.
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier 바인딩은 여기로 간다.
g: undefined
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
이 이후에, 실행 컨텍스트
는 실행 단계(Execution Phase)
로 넘어가며,
이는 함수 내에 있는 변수의 할당이 완료되었음을 의미한다.
따라서, 함수 실행 컨텍스트
는 실행 단계
에서 아래와 같이 보일 것이다.
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier 바인딩은 여기로 간다.
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier 바인딩은 여기로 간다.
g: 20
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
함수가 완료된 이후에, 반환된 값은 변수 c
에 저장된다.
따라서 전역(global) Lexical Environment
가 업데이트된다.
이후에, 전역 코드가 완료되고, 프로그램이 종료된다.
참고로, 생성 단계
에서 let
, const
가 어떠한 값과도 연결되지않는 반면,
var
는 undefined
로 세팅되는 것을 볼 수 있다.
이는 생성 단계
에서, 변수와 함수 선언을 위한 코드가 스캔될 때,
함수 선언은 그것의 전체가 environment
에 저장되는 반면,
변수는 처음에 undefined
로 세팅되거나(var
의 경우),
uninitialized
상태로 남아있기 때문이다(let
, const
의 경우).
이게 바로 var
가 선언되기 전에 접근할 수 있는 이유이며(undefined
일지라도),
let
, const
가 선언되기 전에 접근하면 참조 에러(ReferenceError)가 발생하는 이유이다.
이 개념이 바로, 호이스팅(hoisting)
이라고 불리는 것이다.
참고로, 실행 단계
동안,
자바스크립트 엔진이 소스 코드 내 let
변수가 실제로 선언된 곳에서 그 값을 찾을 수 없다면,
엔진은 let
변수에 undefined
를 할당할 것이다.
위에서 살펴본 개념들이 자바스크립트 개발자에게 꼭 필요한 것은 아니지만,
호이스팅, 스코프, 클로저 등을 더 쉽고 깊게 이해할 수 있도록 도와준다.
실제로 필자도 호이스팅, 스코프를 공부하다가 여기까지 왔답니다...또로록...