실행 컨텍스트란, 자바스크립트 코드 실행에서 아주 중요한 개념이며, 실행 컨텍스트를 이해하면 앞서 브라우저 동작 원리에서 알아보았던 스택과 큐를 좀 더 깊이 있게 이해할 수 있습니다.
ECMAScript 사양은 소스코드 타입을 다음과 같이 크게 4가지로 분류합니다.
전역 코드 : 전역에 존재하는 소스코드. (전역에 정의된 함수, 클래스 등의 내부 코드는 제외)
함수코드 : 함수 내부에 존재하는 소스코드. (함수 내부에 중첩된 함수, 클래스 등의 내부 코드는 제외)
eval 코드 : 빌트인 전역 함수인 eval 함수에 인수로 전달되어 실행되는 소스코드를 말한다
모듈 코드 : 모듈 내부에서 존재하는 소스코드. (모듈 내부의 함수, 클래스 등의 내부 코드는 제외)
자바스크립트 엔진은 소스코드를 평가하여 실행 컨텍스트를 다음과 같이 생성하게 됩니다.
실행 컨텍스트를 생성하는 이유는 무엇일까요? 소스코드별로 생성하는 스코프와 그에 대응하는 스코프 체인을 생성하고, 스코프 변수, 함수들을 스코프 객체와 바인딩 하기 위해서 실행 컨텍스트가 필요하기 때문입니다.
그리고 자바스크립트 엔진은 위의 표에 있는 4종류의 소스코드별로 각각의 실행 컨텍스트를 생성합니다.
정리하자면, 소스코드의 실행 과정은 2단계로 나뉩니다.
다음과 같은 코드의 실행 흐름을 순서대로 알아보도록 하죠.
const x = 1;
const y = 2;
function foo(a){
const x = 10;
const y = 20;
console.log(a + x+ y);
}
foo(100);
console.log(x + y);
이렇게 체계적으로 식별자를 검색하고, 호출 이전으로 돌아갈 위치를 인지하고 스코프의 계층 관계를 형성하는 등의 역할을 실행 컨텍스트없이는 하지 못합니다.
즉, 실행 컨텍스트는 소스코드를 실행하는 데 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리하는 영역입니다. 구체적으로 말하면 실행 컨텍스트는 식별자를 등록하고 관리하는 스코프와 코드 실행 순서 관리를 구현한 내부 매커니즘으로, 모든 자바스크립트 코드는 실행 컨텍스트를 통해 실행되고 관리됩니다.
이때, 식별자와 스코프는 렉시컬 환경으로 관리되고, 코드 실행 순서는 실행 컨텍스트 스택으로 관리합니다.
위의 예제처럼 함수가 호출되면 함수 내부로 들어가서 함수 실행 컨텍스트를 만들고 지역변수 선언등을 실행한 뒤, 함수 코드를 실행하고, 다 끝나면 다시 전역 코드 실행을 합니다. 이런 순서를 관리하는 것이 실행 컨텍스트 스택입니다. 먼저 전역 실행 컨텍스트를 스택에 push하고, 함수 호출문을 만나면 함수 실행 컨텍스트를 스택에 push하고, 함수 코드 실행이 끝나면 함수 실행 컨텍스트를 스택에서 pop하여 스택에 남아있는 전역 실행 컨텍스트를 재개하는 방식으로 코드 순서를 관리합니다.
실행 컨텍스트 스택의 최상위에 존재하는 실행 컨텍스트는 항상 현재 실행중인 코드의 실행 컨텍스트입니다. 이것을 실행중인 실행 컨텍스트(executing execution context)라고 부릅니다.
렉시컬 환경은 식별자와 식별자에 바인딩 된 값, 그리고 상위 스코프에 대한 참조를 기록하는 자료구조입니다. 렉시컬 환경은 실행 컨텍스트를 구성하는 컴포넌트이며, 다른 구성 컴포넌트로는 VariableEnvironment가 있습니다. 처음에 실행 컨텍스트를 구성하는 이 두 컴포넌트는 하나의 동일한 렉시컬 환경을 참조하지만 이후에 몇 가지 상황을 만나면 두 컴포넌트의 내용이 달라질 수 있습니다. 일단은 두 컴포넌트 모두 그냥 렉시컬 환경으로 통일해서 학습해보도록 하겠습니다.
렉시컬 환경은 다음과 같이 두 가지 컴포넌트로 구성됩니다.
다음과 같은 코드가 진행될 때 어떻게 실행 컨텍스트가 동작하는지 알아보겠습니다.
var x = 1;
const y = 2;
function foo(a){
var x = 3;
const y = 4;
function bar(b){
const z = 5;
console.log(a + b + x + y + z);
}
bar(10);
}
foo(20);
큰 그림에서 본 실행 순서는 다음과 같습니다.
✅ 전역 객체 생성→
전역 코드 평가→ 전역 코드 실행→
foo 함수 코드 평가→ foo 함수 코드 실행→
bar 함수 코드 평가→bar 함수 코드 실행→
bar 함수 코드 실행 종료→ foo 함수 코드 실행 종료
전역 코드 실행 종료
위에서 설명하였듯, 소스코드의 실행 컨텍스트는 소스코드 평가 단계의 결과로 생성됩니다. 따라서 위의 예제에서는 전역 실행 컨텍스트, foo 함수 실행 컨텍스트, bar 함수 실행 컨텍스트 이렇게 3가지 실행 컨텍스트가 생성될 것이고, 렉시컬 환경 또한 3가지가 생성될 것입니다.
이 3가지의 실행 컨텍스트와 렉시컬 환경, 그리고 각각의 관계를 그림으로 표현하면 다음과 같습니다.
다음과 같은 코드를 볼까요?
let x = 1;
if(true){
let x = 10;
console.log(x);
}
console.log(x)
let 키워드로 변수를 선언하면 블록 레벨 스코프를 지원합니다. 따라서 if 블록을 진입한다면 if 블록에 대한 렉시컬 환경을 새로 만들어주게 되죠. 처음에 if블록을 진입하기 전에는 글로벌 렉시컬 환경이 하나 존재하고, 전역 실행 컨텍스트의 렉시컬 환경 컴포넌트는 그걸 가리키고 있습니다.
그런데 if문 안으로 진입하게 되면 block 렉시컬 환경이 새로 만들어지게 됩니다. 그리고 전역 실행 컨텍스트는 이 새로 만든 환경을 가리키게 되고, 블록 렉시컬 환경의 외부 렉시컬환경 참조 파트는 글로벌 렉시컬 환경을 가리키게 되겠죠. 그림으로 나타내면 다음과 같습니다.
if 문을 빠져나오면, 전역 실행 컨텍스트는 다시 글로벌 렉시컬 환경을 가리킬 것입니다. 이처럼 블록 레벨의 코드는 새로운 실행 컨텍스트가 만들어지는 것이 아니고, 현재 활성화된 실행 컨텍스트에서 block 렉시컬 환경이 새로 만들어지는 식으로 동작합니다.
지금까지 자바스크립트 코드 실행의 모든것이라 할 수 있는 실행 컨텍스트에 대해 알아보았습니다. 이제 콜 스택이 결국 실행 컨텍스트 스택이란 것을 알게되었습니다. 단순히 코드를 한 줄 한 줄 실행하는 것이 아닌, 실행에 필요한 환경들이 어떠한 매커니즘에 의해 구성되는지, 스코프 체인은 어디에 존재했던건지, 블록 레벨 스코프와 함수레벨 스코프는 어떻게 구현되었는지 알게 된 것입니다. 축하드립니다! 실행 컨텍스트를 이해하면 다음에 배워볼 클로저 또한 이해하기 쉬울 것입니다!