실행 컨텍스트란 자바스크립트 코드가 실행되는 추상적인 환경 혹은 문맥(context) 이다. 여기서 환경 이라 함은 this의 값, 변수의 유효 범위, 접근할 수 있는 변수, 함수, 객체 등을 말한다.
실행 컨텍스트는 세 가지 종류로 구분된다.
전역 실행 컨텍스트(Global Execution Context): 가장 기본이 되는 실행 컨텍스트로, 하나의 프로그램에는 하나의 전역 실행 컨텍스트만이 존재할 수 있다. 자바스크립트 코드가 실행될 때 생성되며, 어떠한 함수 내부에도 존재하지 않는 전역 코드들이 이 컨텍스트 안에서 실행된다.
함수 실행 컨텍스트(Functional Execution Context): 함수가 호출될 시 생성되며, 각각의 함수는 각각의 함수 컨텍스트를 가진다.
Eval 실행 컨텍스트: eval 함수로 실행되는 코드는 별도의 실행 컨텍스트를 가진다. 여러 취약점 때문에 MDN에서는 eval을 사용하지 말 것을 권고하고 있다. 따라서 이번 글에서 eval 실행 컨텍스트에 대해서는 다루지 않고 넘어가겠다.
자바스크립트 엔진은 실행 컨텍스트라는 추상적인 개념을 물리적인 객체의 형태로 관리한다. 실행 컨텍스트는 아래와 같이 세 가지 프로퍼티를 가진다.
변수, 매개변수(parameter)와 인수 정보(arguments), 함수 선언(함수 표현식 제외)을 담는 객체이다.
변수 객체는 전역 컨텍스트일 경우와 함수 컨텍스트일 경우 서로 다른 객체를 가리킨다.
전역 컨텍스트
변수 객체는 모든 전역 변수 및 전역 함수 등을 포함하는 전역 객체(Global Object / GO)를 가리킨다. 전역 객체는 전역에 선언된 전역 변수와 전역 함수를 프로퍼티로 가진다.
함수 컨텍스트
변수 객체는 Activation Object(AO / 활성 객체)를 가리킨다. 변수 객체는 지역 변수, 내부 함수, 그리고 매개변수와 인수들의 정보를 배열의 형태로 담고 있는 객체인 arguments 객체를 프로퍼티로 가진다.
스코프 체인(Scope Chain)은 해당하는 실행 컨텍스트가 참조할 수 있는 변수, 함수 선언 등의 정보를 담고 있는 전역 객체(GO) 또는 활성 객체(AO)리스트를 가리킨다.
스코프 체인은 현재 실행 컨텍스트의 활성 객체(AO)에서 시작해 순차적으로 상위 컨텍스트의 활성 객체(AO)를 담고 있으며, 리스트의 마지막 항목은 전역 객체(GO)를 담고 있다.
자바스크립트 엔진은 렉시컬 스코프(Lexical Scope)를 파악할 때 스코프 체인을 사용한다. 함수 실행 중에 변수를 만나면, 자바스크립트 엔진은 먼저 현재 스코프, 즉 활성 객체(AO) 안에 해당 변수가 존재하는 지 확인한다. 만약 존재하지 않는다면, 스코프 체인을 따라 상위 스코프의 활성 객체(AO)안에 해당 변수가 존재하는지 순차적으로 확인해 나간다. 만약 최상위 스코프, 즉 전역 객체(GO) 까지 확인했는데도 해당 변수가 존재하지 않는다면, ReferenceError
를 발생시킨다.
아래 예제 코드를 보자.
function foo() {
let a = 1;
function bar() {
console.log(a);
}
bar();
}
foo();
bar()
은 변수 a
에 접근하려 한다. 자바스크립트 엔진은 먼저 bar()
의 활성 객체를 확인하지만, a
를 찾을 수가 없다. 다음으로 자바스크립트 엔진은 스코프 체인을 따라 상위 스코프인 foo()
의 스코프에서 a
를 찾는다. a = 1
라는 걸 확인하고, 최종적으로 1
을 출력한다.
함수 호출 방식에 따라 결정된 this 값이 이 프로퍼티에 저장된다.
실행 스택이란 코드 실행 중 만들어지는 모든 실행 컨텍스트를 스택 형태로 저장한 자료 구조이다.
코드가 맨 처음 실행되면 전역 실행 컨텍스트만 실행 스택 바닥에 존재한다.
함수를 호출하게 되면 변수 객체(arguments,variable), 스코프 체인, this를 가진 함수 실행 컨텍스트가 생성되어 실행 스택 맨 위에 차곡차곡 쌓인다.
함수 실행 컨텍스트가 생성된 이후, 함수가 실행된다. 함수 안에서 변수, 함수, this 등을 사용할 시 함수 컨텍스트 내에서 해당하는 값을 찾는다. 만약 함수 컨텍스트 내에 해당하는 값이 없다면, 스코프 체인을 따라 상위 실행 컨텍스트로 올라가며 해당하는 값을 찾아 나간다.
함수 실행이 끝나면 해당 함수의 실행 컨텍스트는 실행 스택에서 제거된다.
예를 들어, 아래 코드를 실행할 때 실행 스택은 아래 사진과 같은 형태로 변화한다.
var x = 'xxx';
function foo () {
var y = 'yyy';
function bar () {
var z = 'zzz';
console.log(x + y + z);
}
bar();
}
foo();
그렇다면 실행 컨텍스트는 어떤 과정을 거쳐 생성되는 것일까? 아래 예제 코드를 보면서 알아 보자.
var x = 'xxx';
function foo () {
var y = 'yyy';
function bar () {
var z = 'zzz';
console.log(x + y + z);
}
bar();
}
foo();
가장 먼저 전역 객체(Global Object)가 생성된다. 초기 상태의 전역 객체에는 빌트인 객체(Math, String, Array 등)와 BOM, DOM이 설정되어 있다.
전역 실행 컨텍스트가 생성되어 실행 스택에 쌓인다.
스코프 체인이 생성되고 초기화된다.
스코프 체인의 초기화가 종료되면 변수 객체화(Variable Instantiation)가 실행된다. 변수 객체화는 아래의 세 단계에 걸쳐 이루어진다.
undefined
가 값으로 설정된다.(변수 호이스팅)전역 컨텍스트의 경우 변수 객체는 전역 객체(Global Object)를 가리킨다.
선언된 함수명 foo가 Variable Object(전역 코드인 경우 Global Object)의 프로퍼티로, 생성된 함수 객체가 값으로 설정된다.
생성된 함수 객체는 [[Scopes]] 프로퍼티를 가지게 된다. [[Scopes]] 프로퍼티는 함수 객체만이 소유하는 내부 프로퍼티(Internal Property)로서 함수 객체가 실행되는 환경을 가리킨다. 따라서 현재 실행 컨텍스트의 스코프 체인이 참조하고 있는 객체를 값으로 설정한다. 내부 함수의 [[Scopes]] 프로퍼티는 자신의 실행 환경(Lexical Enviroment)과 자신을 포함하는 외부 함수의 실행 환경과 전역 객체를 가리키는데 이때 자신을 포함하는 외부 함수의 실행 컨텍스트가 소멸하여도 [[Scopes]] 프로퍼티가 가리키는 외부 함수의 실행 환경(Activation object)은 소멸하지 않고 참조할 수 있다. 이것이 클로저이다.
함수 선언식의 경우, 변수 객체(VO)에 함수명을 프로퍼티로 하여 함수 객체를 즉시 할당한다. 따라서 함수 선언식의 경우 선언문이 나타나기 이전에 함수를 호출할 수 있다. 이러한 현상을 함수 호이스팅(Function Hoisting)이라 한다.
반대로 함수 표현식의 경우, 일반 변수와 똑같이 undefined
가 할당되기 때문에 함수 호이스팅이 일어나지 않는다.
이전 포스팅에서 이미 다루었듯, 변수 선언은 아래와 같은 세 단계로 이루어진다.
undefined
로 초기화된다.var 키워드로 선언된 변수는 선언 단계와 초기화 단계가 한번에 이루어진다. 다시 말해 스코프 체인이 가리키는 변수 객체에 변수가 등록되는 동시에 변수는 undefined
로 초기화된다. 따라서 변수 선언문 이전에 변수에 접근하여도 변수 객체(Variable Object)에 변수가 존재하기 때문에 에러가 발생하는 대신 undefined
를 반환한다. 이러한 현상을 변수 호이스팅(Variable Hoisting)이라 한다.
변수 선언 처리가 끝나면 다음은 this value가 결정된다. this value가 결정되기 이전에 this는 전역 객체를 가리키고 있다가 함수 호출 패턴에 의해 this에 할당되는 값이 결정된다. 전역 코드의 경우, this는 전역 객체를 가리킨다.
지금까지가 코드 실행 환경을 갖추기 위한 사전 준비 과정이었다면, 이제부터는 코드의 실행이 시작된다.
var x = 'xxx';
function foo () {
var y = 'yyy';
function bar () {
var z = 'zzz';
console.log(x + y + z);
}
bar();
}
foo();
예제 코드에서는 가장 먼저 전역 변수 x에 문자열 ‘xxx’ 이 할당되고 있다.
전역 변수 x에 문자열 ‘xxx’를 할당할 때, 현재 실행 컨텍스트의 스코프 체인이 참조하고 있는 변수 객체(Variable Object)를 선두(0)부터 검색하여 변수명에 해당하는 프로퍼티가 발견되면 값(‘xxx’)을 할당한다.
다음으로 예제 코드는 함수 foo()
를 실행하고 있다. foo()
가 실행되기 시작하면 새로운 함수 실행 컨텍스트가 생성된다. 그 다음에는 전역 코드의 경우와 마찬가지로
이 순차적으로 실행된다.
함수 코드의 스코프 체인의 생성과 초기화는 우선 활성 객체(Activation Object)에 대한 레퍼런스를 스코프 체인의 선두에 설정하는 것으로 시작된다.
활성 객체(Activation Object)는 arguments 프로퍼티를 초기화한 후, 변수 객체화(Variable Instantiation)를 실행한다.
이후 Caller(전역 컨텍스트)의 Scope Chain이 참조하고 있는 객체가 스코프 체인에 push된다. 따라서, 이 경우 함수 foo를 실행한 직후 실행 컨텍스트의 스코프 체인은 Activation Object(함수 foo의 실행으로 만들어진 AO-1)과 전역 객체를 순차적으로 참조하게 된다.
Function Code의 경우, 스코프 체인의 생성과 초기화에서 생성된 활성 객체(Activation Object)를 변수 객체(Variable Object)로 하여 변수 객체화(Variable Instantiation)를 실행한다.
변수 y를 Variable Object(AO-1)에 설정한다 이때 프로퍼티는 y, 값은 undefined이다.
변수 선언 처리가 끝나면 다음은 this value가 결정된다. this에 할당되는 값은 함수 호출 패턴에 의해 결정된다.
내부 함수의 경우, this의 value는 전역 객체이다.
이제 함수 foo의 코드 블록 내 구문이 실행된다. 위 예제를 보면 변수 y에 문자열 ‘yyy’의 할당과 함수 bar가 실행된다.
지역 변수 y에 문자열 ‘yyy’를 할당할 때, 현재 실행 컨텍스트의 스코프 체인이 참조하고 있는 Variable Object를 선두(0)부터 검색하여 변수명에 해당하는 프로퍼티가 발견되면 값 ‘yyy’를 할당한다.
#3.3.2 함수 bar의 실행
함수 bar가 실행되기 시작하면 새로운 실행 컨텍스트이 생성된다.
이전 함수 foo의 실행 과정과 동일하게 1. 스코프 체인의 생성과 초기화, 2. Variable Instantiation 실행, 3. this value 결정이 순차적으로 실행된다.
이 단계에서 console.log(x + y + z); 구문의 실행 결과는 xxxyyyzzz가 된다.
x : AO-2에서 x 검색 실패 → AO-1에서 x 검색 실패 → GO에서 x 검색 성공 (값은 ‘xxx’)
y : AO-2에서 y 검색 실패 → AO-1에서 y 검색 성공 (값은 ‘yyy’)
z : AO-2에서 z 검색 성공 (값은 ‘zzz’)
참고: