실행 컨텍스트는 scope
, hoisting
, this
, function
, closure
등의 동작원리를 담고 있는 자바스크립트의 핵심원리이다.
실행 컨텍스트
란 실행할 코드에 제공할 환경 정보들을 모아 놓은 객체이다.
실행 컨텍스트
는 동일한 환경에 있는 코드들을 실행할 때, 필요한 환경 정보들을 모아 객체를 구성하고, 이를 콜 스택에 쌓아올렸다가 가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다.
어떤 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 위로 끌어올리고(hoisting), 외부 환경 정보를 구성하고, this 값을 설정하는 등의 동작을 수행한다.
let x = 'xxx';
function foo () {
let y = 'yyy';
function bar () {
let z = 'zzz';
console.log(x + y + z);
}
bar();
}
foo();
함수를 실행하면, 처음 자바스크립트 코드를 실행하는 순간 전역 컨텍스트
가 콜스택에 담긴다.
전역 컨텍스트는 코드 내부에서 별도의 실행 명령이 없어도 브라우저에서 자동으로 실행하므로, 자바스크립트 파일이 열리는 순간 전역 컨텍스트가 활성화된다고 이해하면 된다.
참고로, 브라우저의 경우에는 window
객체가 전역 실행 컨텍스트가 된다.
이제 콜 스택에서 전역 컨텍스트와 관련된 코드들을 순차로 실행하다가, foo
함수를 호출하면 자바스크립트 엔진은 foo
에 대한 환경 정보를 수집해서 foo 실행컨텍스트
를 생성한 후 콜 스택에 담는다.
그러면 콜 스택의 맨 위에 foo 실행 컨텍스트
가 놓였으므로 전역 컨텍스트와 관련된 코드의 실행을 중지하고, foo 실행 컨텍스트와 관련된 코드, 즉 foo 함수
내부의 코드들을 순차로 실행한다.
그리고 bar 함수
의 실행 컨텍스트가 스택의 가장 위에 담기면 foo 실행 컨텍스트와 관련된 코드의 실행을 중단하고, bar 함수 내부의 코드를 순서대로 진행한다.
코드가 모두 진행되고 bar 함수의 실행이 종료되면, bar 실행 컨텍스트가 콜스택에서 제거되고, 그러면 foo 실행 컨텍스트가 콜 스택의 맨 위에 존재하게 되므로 중단했던 부분부터 이어서 실행하게 된다.
위의 스택 구조를 보면, 실행 컨텍스트가 콜 스택의 맨 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점이다.
이렇게 어떤 실행 컨텍스트가 활성화될 때, 자바스크립트 엔진은 해당 컨텍스트와 관련된 코드들을 실행하는 데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장한다.
ES5를 기점으로 실행 컨텍스트의 동작 방식과 구조가 많이 달라졌다.
실행 컨텍스트란 자바스클비트가 코드를 실행하기 위해서 특정한 코드들에 대한 정보를 저장하는 것이다.
그 특정한 코드를 실행하기 위해 코드를 일종의 블록으로 나누는데, 그 코드 블록에 들어가 있는 변수, 함수, this, arguments 등에 대한 정보를 담고 있는 하나의 환경을 Execution Context
일명 실행 컨텍스트라고 부른다.
한마디로 정리하면, 실행 가능한 자바스크립트의 코드 블록이다.
이 Execution Context
라는 코드 블록은 딱 3가지 경우에만 생성된다.
eval()
이 사용되었을 때이렇게 생성된 실행 컨텍스트는 위에서 작성했던 대로 자바스크립트 엔진의 call stack 이라는 곳에 차곡차곡 쌓이게 된다.
실행 컨텍스트가 생성되는 경우 중 eval()
는 XSS(Cross Site Script) 공격에 사용되는 등 악의적인 공격에 사용될 수 있다보니, 보안상의 문제로 사실상 안쓰는 케이스이다.
실행 컨텍스트가 위의 3가지 상황 발생시 콜 스택에 쌓인다는 내용은 ES5 이후에 실행 컨텍스트가 바뀌고도 동일하게 가져가는 개념이다.
우선, 생긴 실행 컨텍스트가 함수 호출로 인해 생겼다면, 먼저 변수 객체가 생긴 뒤 arguments
정보를 생성한다.
이후 arguments
는 params
등의 정보를 담는다.
이후에는 scope
정보가 생긴다.
이 스코프 정보를 통해 하위 실행 컨텍스트에서 전역 실행 컨텍스트에 있는 변수를 참조할 수 있다.
이렇게 변수 객체의 scope 정보를 타고 상위 실행 컨텍스트의 데이터에 접근하는 것을 Scope Chain
이라고 한다.
이 scope 정보는 단방향 링크드 리스트와 같은 형태로 이루어져 있기 때문에, 특정한 함수 실행 컨텍스트에서 전역 실행 컨텍스트 방향으로만 참조가 가능하다.
따라서 아래와 같은 예시처럼 코드가 동작하게 되는 것이다.
var a = 1;
function hi(){
console.log(a);
}
hi(); // 함수에서 변수 a 접근가능
function hi(){
var aa = 123;
}
console.log(aa) // 전역에서 aa변수 접근 불가능
스코프 정보에 대한 정보가 생긴 뒤에는 변수에 대한 정보가 생성된다.
이런 변수가 있다고만 먼저 등록해두고, 값은 undefined
로 초기화해둔다.
이후에 변수 초기화 코드를 만나면, 그제서야 undefined
를 초기화하고자 하는 값으로 초기화한다.
변수 정보 생성 이후에는 this 바인딩에 대한 정보를 추가해준다.
만약 this가 참조하는 객체가 없다면, 전역으로 바인딩된다.
여기까지가 ES3 기준 Execution Context 생성 과정이다.
ES3와 ES6의 실행 컨텍스트의 차이는 우선 구성 요소가 달라진다.
가장 큰 차이점으로, ES6의 실행 컨텍스트는 Lexical Environment
라는 것이 두 개가 들어간다.
이렇게 들어간 두 개의 Lexical Environment
는 각각 다른 이름으로 구별되어 불린다.
Lexical Environment는 2가지로 이루어져 있다.
Outer Reference Environment
는 상위 스코프 Lexical Environment
를 가리킨다.
이는 자바스크립트에서 흔히 말하는 Scope Chain
에 대한 내용이다.
이 Outer Reference Environment
를 통해 상위 스코프의 데이터들을 참조할 수 있다고 생각하면 된다.
단, 이 Outer Reference Environment
는 단방향 링크드 리스트로 이루어져 있기에, 하위 스코프에서 상위 스코프를 참조하는 것만 가능하다.
전역 Lexical Environment
의 경우 당연히 Outer Reference Environment
는 null
이다.
전역에서 함수 하나만 호출된 상태의 상황을 그림으로 나타내면 아래와 같을 것이다.
이렇게 Lexical Environment
와 Outer Reference Environment
가 상위 Lexical Environment
와 연결되어 있는 것을 ES5버전 이후의 실행 컨텍스트에서는 Lexical Nesting Structure
라고 한다.
코드 상에서 어떤 변수에 접근하려고 하면, 현재 컨텍스트의 LexicalEnvironment를 탐색하여 발견되면 그 값을 반환하고, 발견하지 못할 경우 다시 Outer Reference Environment에 담긴 LexicalEnvironment를 탐색하는 과정을 거친다.
전역 컨텍스트의 Lexical Environment까지 탐색해도 해당 변수를 찾지 못하면, undefined를 반환한다.
Environment Records
는 기본적으로 변수, 함수 이름과 관련된 값들을 추적한다.
이 Environment Records의 경우 다시 2가지로 나뉜다.
함수, 변수, this 등의 식별자 바인딩이 Declarative Environment Records
에 저장되는데, 대부분의 경우 Object Environment Records
보단 Declarative Environment Records
에 저장된다고 보면 된다.
Object Environment Records는 with
문 같은 코드를 쓰는 것이 아니라면, 대부분 그쪽으로 저장될 일이 없다고 보면 된다.
이제 Lexical Environment의 전체적인 그림을 그려보면 아래와 같은 구조일 것이다.
왜 함수의 실행 컨텍스트를 Lexical Environment와 Variable Environment로 구별지어 나눈걸까?
여기서 나오는 내용이 바로 변수 선언 방식인 let
, const
, var
의 차이다.
Scope 범위
let, const로 선언한 변수들의 경우 스코프 자체가 block scope
이다.
그에 비해 var로 선언한 변수의 경우 스코프가 function scope
이다.
이 차이점으로 인해 한 함수 안에 여러 블록이 생긴다면, 그 함수의 Execution Context에는 여러 Lexical Environment를 만들어야 하는 상황이 만들어지기에 Lexical Environment와 Variable Environment를 구분짓는 것이다.
아래와 같은 코드가 있다고 하자.
이 경우 아래 그림과 같이 Lexical Environment가 블록 기준으로 여러 개 생기게 될 것이다. (함수, if)
TDZ
영향 여부)선언 단계: 변수를 실행 컨텍스트의 Lexical Environment에 등록
초기화 단계: Lexical Environment에 등록되어 있는 변수를 위하여 메모리를 할당하는 단계. 여기서 변수는 undefined
로 초기화된다.
할당 단계: 변수에 실제로 값이 할당되는 단계(undefined
-> 특정한 값)
let, const와 var는 이 3가지 과정을 거치는 부분에서 또 다시 차이가 발생하는데, var의 경우 1번과 2번이 동시에 진행되지만 let, cosnt는 1번만 먼저 진행된다.
선언 단계와 초기화 단계 사이를 TDZ(Temporal Dead Zone, 일시적 사각지대)라고 부르는데, 변수 생성 과정 차이로 인한 TDZ의 영향을 받느냐가 두 번째 차이점이다.
이 때문에, 동작을 아예 다르게 해야 하는 것이고, 그렇기에 Lexical Environment와 Variable Environment를 구분 짓는다.
참고로 흔히들 말하는 "변수 선언부가 스코프의 최상단으로 끌어올려진다"라는 특성인 호이스팅 현상은 선언 단계 때문에 발생하게 된다.
let과 const 또한 선언 단계를 거치기에 호이스팅은 일어난다.
오류가 발생하는 이유는 TDZ에 영향을 받아 초깃값을 할당받지 못한 변수에 접근하려 했기 때문일 뿐이지, 호이스팅이 일어나지 않는 것은 아니다.
실제로 실행 컨텍스트의 생성 과정은 크게 두 페이즈로 나누어진다.
1. Creation Phase (Execution Context에 대한 정의 과정)
2. Execution Phase (코드를 실행하는 과정)
첫 번째 페이즈인 Creation Phase에서 Lexical Environment와 Variable Environment에 대한 정의가 이루어지게 된다.
Variable Environment에서는 var로 선언된 변수를 변수 선언 1, 2단계(Declaration Phase, Initialization Phase) 모두 진행하기에 메모리에 매핑되고 undefined
로 초기화까지 마치게 된다.
반면에 Lexical Environment의 경우는 let, const로 선언된 변수를 1단계(Declaration Phase)만 진행하기에 Variable Environment와는 동작 방식에 차이가 있다.
결론적으로 위 두 가지 차이 때문에 Lexical Environment와 Variable Environment로 차이를 두는 것이다.