<코어자바스크립트> 실행 컨텍스트
실행 컨텍스트란
실행 컨텍스트 (execution context) 는 실행할 코드에 제공할 환경 정보들을 모아 놓은 객체로, 자바스크립트의 동적 언어로서의 성격을 잘 파악할 수 있는 개념이다. 자바사스크립트는 어떤 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 위로 끌어올리고 (호이스팅), 외부 환경정보를 구성하고, this값을 설정하는 등의 동작을 수행하는데, 이로 인해 다른 언어에서는 발견할 수 없는 특이한 현상들이 발생한다.
사실 클로저를 지원하는 대부분의 언어에서 이와 유사하거나 동일한 개념이 적용되어 있다. (클로저 또한 추후 살펴볼 예정이다)
조금 더 자세히 들어가자면
동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 콜 스택 (call stack)에 쌓아 올렸다가, 가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다.
여기서
동일한 환경, 즉 하나의 실행 컨텍스트를 구성할 수 있는 방법으로 전역공간, eval, 함수 등이 있다 (ES6 에서는 블록에 의해서도 새로운 실행컨텍스트가 생성된다) 자동으로 생성되는 전역공간, 사용을 지양하라는 악마의 eval을 제외하면 우리가 흔히 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것 뿐이다.
실행 컨텍스트와 콜스택
// -------------------------------------- (1)
var a = 1;
function outer() {
function inner() {
console.log(a);
var a = 3;
}
inner(); // ---------------------------- (2)
console.log(a);
}
outer(); // ------------------------------ (3)
console.log(a);자바스크립트 코드를 실행하는 순간 (1) 전역 컨텍스트가 콜 스택에 담긴다. 전역 컨텍스트는 일반적인 실행 컨텍스트와 크게 다르지 않고, 최상단 공간은 별도의 실행 명령이 없어도 브라우저에서 자동으로 실행하므로 자바스크립트 파일이 열리는 순간 전역 컨텍스트가 활성화된다고 이해하면 된다
전역 컨텍스트 외의 다른 덩어리가 없으므로 전역컨텍스트를 순차로 진행하다가 (3)에서 outer 함수를 호출하면 자바스크립트 엔진은 outer에 대한 환경정보를 수집해서 outer실행 컨텍스트를 생성한 후 콜 스택에 담는다. outer가 콜스택의 맨 위에 놓인 상태가 됐으므로 전역컨텍스트와 관련된 코드의 실행을 일시중단하고 대신 outer 실행 컨텍스트와 과련된 코드, 즉 outer함수의 내부 코드들을 순차적으로 실행한다
그러고 다시 (2)에 도달해 inner함수의 실행 컨텍스트가 콜스택의 가장 위에 담기면 앞서 설명한 흐름대로 진행된다.

스택 구조를 잘 생각해보면 한 실행 컨텍스트가 콜 스택의 맨 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점임을 알 수 있다. 이렇게 어떤 실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장한다.
여기에 담기는 정보들은 다음과 같다
- VariableEnvironment\
- 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보
- 선언시점의 LexicalEnvironment의 스냅샷으로, 변경사항은 반영되지 않음
- LexicalEnvironment
- 처음에는 VariableEnvironment와 같지만 변경사항이 실시간으로 반영됨
- ThisBinding
- this 식별자가 바라봐야할 대상객체
- 따로 설정해주지 않으면 global 객체를 가르킴

VariableEnvironment
VariableEnvironment에 담기는 내용은 LexicalEnvironment와 같지만 최초실행 시의 스냅샷을 유지한다는 점이 다르다. 따라서 실행컨텍스트를 생성할 때 VariableEnvironment에 정보를 담은 후, 이를 그대로 복사하여 LexicalEnvironment를 만들고, 이후에는 LexicalEnvironment를 주로 활용하게 된다. (복사한다는 표현은 이해를 돕기위한 표현일 뿐, 너무 깊게 생각하지말자) (변수 값 바뀌고, 초기화되고 등등 변하는 것들은 전부 LexicalEnviroment에 변경된다는 뜻이다)
VariableEnvironment와 LexicalEnvironment의 내부는 enviromentRecord와 outerEnviromentReference로 구성돼 있다.
문득 그럼 VariableEnvironment는 왜 필요한걸까 하는 생각을 했다 (어차피 이후에는 LexicalEnvironment만 쓸건데..)
결론은 내 말처럼 예전에 let과 const 개념이 도입되지 않았을 때(var을 사용했을 때)는 따로 중간에 바뀌는 요소들을 관리해줄 수요가 많지 않아서 VariableEnvironment를 사용했는데, let과 const가 도입된 후로 LexicalEnvironment가 블록 스코프와 TDZ(Temporal Dead Zone) 상태를 관리하고 추적하는 등, VariableEnvironment와 차이가 보다 분명해졌다고 한다
VariableEnvironment와 LexicalEnvironment의 더욱 구체적인 차이점 [JavaScript] 실행 컨텍스트 ( Execution Context )
요약하자면 Variable은 var타입 관리, 같은 스코프에서 재선언가능, 함수 스코프 가짐, 호이스팅시 undefined으로 초기화 Lexical은 let과 const 관리, 같은 스코프에서 재선언 불가능, 블록 스코프가짐, 호이스팅시 초기화하지 않음 (TDZ 도입으로 초기화전 참조시 RefernceError 발생)
LexicalEnvironment
필자는 Lexical의 뜻이 이 용어의 경우 “사전적인” 이라는 말이 잘 어울리는 표현이라고 한다. 왜냐하면 “현재 컨텍스트의 내부에는 a, b, c와 같은 식별자들이 있고 그 외부 정보는 D를 참조하도록 구성돼 있다” 라는, 컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아 놓은 느낌이라고 한다.
EnvironmentRecord와 호이스팅
Environment에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 컨텍스트를 구성하는 함수에 지정된 매겨변수 식별자, 선언한 함수가 있을 경우 그 함수 자체, var로 선언된 변수의 식별자 등이 식별자에 해당된다.
컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집한다. 수집을 모두 마쳤더라도 아직 실행 컨텍스트가 관여할 코드들은 실행되기 전의 상태이다. 따라서 자바스크립트 엔진은 실행하기 전에 이미 해당환경에 속한 코드이 변수명들을 모두 알고 있게 되는 셈이다. 여기서 호이스팅이라는 개념이 등장한다.
이미 자바스크립트 엔진은 변수명들을 모두 알고 있기 때문에 “자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행한다” 라고 해도 코드를 해석하는데 문제가 생기지 않는다. 따라서 자바스크립트 엔진이 변수들을 실제로 끌어 올리지는 않지만 변수들을 수집하는 과정을 더욱 이해하기 쉽게 편의상 끌어올렸다고 간주하자는 것이 호이스팅 이다.
이런 의문이 생긴다 → 호이스팅 그냥 없애면 안되나?? 하지만 의문이 근본적으로 잘못되었다는 것을 알아야한다. 호이스팅을 없앤다는 뜻은 변수들을 미리 수집하지 않으면 안되나? 라는 것으로 동치가된다
그래서 식별자들을 미리 수집하는 이유를 좀 찾아봤다.
- 메모리 할당: 각 변수와 함수에 미리 메모리를 할당하여 실행 중 메모리를 효율적으로 관리할 수 있다
- 코드 실행 흐름 예측 가능성 향상: 코드 실행 흐름을 명확히 하고, 잠재적인 스코프 관련 오류를 줄이는데 도움을 준다
- 호이스팅 처리: 유연한 함수호출과 변수 선언 (너무 유연해서 문제가 되는것이다 (let const 도임))
- 코드의 초기화 단계, 실행 단계 분리: 코드 실행 중 불필요한 초기화나 선언 관련 비용을 줄인다.또한 스코프 체인을 명확히 설정하여 의도된 실행 흐름을 유지한다
- TDZ 관리: TDZ를 명확하게 관리할 수 있다. 선언 전에 변수를 참조하는 실수를 줄이고, 코드의 안정성을 높인다.
함수선언문과 함수표현식
둘 모두 함수를 새롭게 정의할 때 쓰이는 방식인데, 그중 함수 선언문은 function 정의부만 존재하고 따로 할당 명령이 없는 것을 의미하고, 반대로 함수 표현식은 정의한 function을 별도의 변수에 할당하는 것을 말한다.
함수선언문은 함수 전체를 호이스팅하고, 함수 표현식은 변수 선언문만 호이스팅한다. 딱 봐도 그냥 함수 표현식이 더 안전한 방식이다.
나중에 프로젝트 같은것을 진행하고 코드가 매우 길어진다고 했을때 함수선언문으로 함수를 작성하다 이전함수와 함수명이 같아지는경우 이후에 작성한 함수로 override를 하게 된다. 이 때 override한 함수가 다른곳에서 에러가 나지 않는다면, 디버깅하기 매우 까다로워진다.
function a() {...}; // 함수 선언문. 함수명 a가 곧 변수명
a(); // 실행 굳
var b = function () {...}; // 익명 함수 표현식. 변수명 b가 곧 함수명
b(); // 실행 굳
var c = function d() {...}; // 기명 함수 표현식. 변수명 c, 함수명 d
c(); // 실행 굳
d(); // 실행 에러
예전엔 기명함수표현식으로 작성해야 디버깅 시 함수명이 나와서 추적하는 측면에서 익명 함수 표현식보다 유리한 면이 있었는데, 이제는 모든 브라우저들이 익명 함수 표현식의 변수명을 함수의 이름 속성에 할당한다고 한다 (요약하면 이젠 그냥 익명 함수표현식도 디버깅시 함수 이름이 추적이 된다)
스코프, 스코프 체인, outerEnvironmentReference
스코프란 식별자에 대한 유효범위 이다. 자바스크립트는 ES5까지는 특이하게도 전역공간을 제외하면 오직 함수에 의해서만 스코프가 생성되었다.
현재는 블록에 의해서도 스코프가 생겨서 다른 언어와 훨씬 비슷한 형태의 스코프를 가지게 되었다. 함수 스코프, 블록 스코프라는 용어를 사용한다.
스코프 체인
outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다. 예를 들어 A 함수 내부의 B함수를 선언하고 다시 B 함수 내부에 C 함수를 선언한 경우, 함수 C의 outerEnvironmentReference는 함수 B의 LexicalEnvironment를 참조한다. 그리고 B는 A의 LexicalEnvironment를 참조하고 A는 전역컨텍스트의 LexicalEnvironment 를 참조하는 식으로 outerEnvironmentReference는 연결리스트의 형태를 띤다.
따라서 자신이 선언된 시점의 LexicalEnvironment를 참조하고 있으므로 가장 가까운 요소부터 차례대로 접근할 수 있고 다른 순서로 접근하는 것은 불가능하다. 이러한 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우 무조건 스코프 체인상에서 가장 먼저 발견된 식별자에만 접근 가능하게 된다
01 var a = 1;
02 var outer = function() {
03 var inner = function() {
04 console.log(a); // a가 호이스팅 되어 undefined가 출력된다.
05 var a = 3;
06 };
07 inner();
08 console.log(a);
09 };
10 outer();
11 console.log(a);다음 표에서 L.E는 LexicalEnvironment, e 는 environmentRecord, o 는 outerEnvironmentReference를 의미하며 [숫자] 표기는 코드의 줄 번호를 의미한다.

e에 존재하는 식별자들은 미리 수집한 식별자 들이고 호이스팅 되는 것들이다 식별자를 찾을 때 inner → outer → 전역공간 순으로 스코프 체인을 타고 접근을 할 수 있다. 접근한 것들 중에 가장 먼저 찾은 컨텍스트의 식별자에 접근하여 사용된다.
정리
실행 컨텍스트는 실행한 코드에 제공할 환경 정보들을 모아놓은 객체이다. 전역컨텍스트, eval, 함수 실행, 블록 등에 의해 실행 컨텍스트가 생성된다. 실행 컨텍스트 객체는 활성화되는 시점에 VariableEnvironment, LexicalEnvironment, ThisBinding 이렇게 세가지 정보를 수집한다.
VariableEnvironment는 초기상태를 유지하고 LexicalEnvironment는 함수 실행도중 변경되는 사항들이 실시간으로 반영된다. VariableEnvironment, LexicalEnvironment 모두 environmentRecord가 변수의 식별자, 선언한 함수의 함수명 등을 수집하고 바로 직전 컨텍스트의 LexicalEnvironment 정보를 참조하는 outerEnvironmentReference로 구성되어있다.
호이스팅은 environmentRecord가 식별자를 수집하는 과정을 추상화한 개념이고, 실행 컨텍스트가 관여하는 코드 집단을 최상단으로 끌어올린다고 해석한다
스코프는 변수의 유효범위를 말한다. 스코프 체인상 outerEnvironmentReference는 연결리스트의 형태를 띠는데, 순차적으로 접근, 탐색하여 식별자를 참조하고, 전역컨텍스트까지 탐색해도 해당 변수를 찾지 못하면 undefined를 반환한다.
전역 컨텍스트의 LexicalEnvironment에 담긴 변수를 전역변수라 하고, 그 밖의 함수에 의해 생성된 실행컨텍스트의 변수들을 모두 지역변수라 한다. 안전한 코드 구성을 위해 가급적이면 전역변수 사용은 최소화하는 것이 좋다.
this에는 실행 컨텍스트를 황성화 하는 당시 지정된 this가 저장된다. 함수를 호출하는 방법에 따라 그 값이 달라지는데, 지정되지 않은 경우 전역객체가 저장된다
전역변수를 사용하는 경우 접근 스코프가 전범위 이므로 같은 이름의 변수명이나 함수명이 있을 때 예기치 못한 에러가 발생할 수 있다. 항상 코드 실행이 너무 유연하다 싶은것은 어떠한 유형의 문제를 일으킬 수 있다는 것을 명심해야한다
this와 관련해서 다음장에 설명할 예정이다.
