실행 컨텍스트 (Execution Context)

Hardy·2021년 8월 19일
19
post-thumbnail

실행 컨텍스트 (Execution Context)??

자바스크립트의 엔진이 우리가 작성한 코드를 실행하기 위해선 코드에 대한 정보들이 필요합니다. 변수, 함수, 스코프, this등 필요한 정보들을 모아 객체로 관리하게 되는데 이를 실행 컨텍스트라고 부릅니다. 즉, 실행 컨텍스트는 자바스크립트 엔진이 코드를 읽고 실행할 때 필요한 정보들의 모음이라고 할 수 있습니다.

Execution Context의 세가지 타입

실행 가능한 코드를 만날 때 마다 엔진은 실행 컨텍스트를 구축하게 되는데 실행 가능한 코드에 따라 실행 컨텍스트의 타입도 달라지게 됩니다.

1. Global Execution Context
Global Execution Context는 코드가 실행하면 가장 먼저 생성되는 전역 컨텍스트입니다. 전역 컨텍스트는 window 전역 컨텍스트를 생성하고 this를 전역 객체로 설정합니다. 해당 컨텍스트는 프로그램에 단 하나만 존재합니다.

2. Functional Execution Context
Functional Execution Context는 함수가 호출 될 때 마다 완전히 새로운 실행 컨텍스트가 생성됩니다. 단 하나만 존재하는 전역 컨텍스트와는 다르게 다른 함수가 호출 될 때 마다 다른 컨텍스트가 생성되기 때문에 여러개의 함수 컨텍스트가 존재합니다.

3. Eval Execution Context
Eval Execution Context는 eval 함수로 실행한 코드의 컨텍스트입니다. 하지만 보안상의 이슈로 더 이상 권장되지 않고 사용하지 않습니다.

실행 스택 (Execution Stack)

생성된 실행 컨텍스트는 엔진이 관리하고 있는 실행 스택에 의해 관리됩니다. 실행 스택은 다른 말로 콜 스택 (Call Stack)이라고도 부릅니다. 실행 스택은 Last-In-First-Out(LIFO) 구조를 가지고 있어 실행 가능한 코드를 만나 생성된 컨텍스트는 순서에 맞게 실행 스택에 쌓이고(push) 모든 컨텍스트 구축이 끝나면 함수의 실행을 종료하면서 스택의 맨 위 부터 컨텍스트를 제거(pop)합니다.

위의 코드와 그림을 토대로 Execution Stack이 어떻게 관리되는지 알아보겠습니다.

  1. 엔진이 코드를 처음 읽음과 동시에 Global Execution Context가 생성되어 Exection Stack에 추가(push) 됩니다.
  2. 전역 코드에서 실행 된 first 함수에 해당하는 Functional Execution Context가 생성 되어 Exection Stack에 추가(push) 됩니다.
  3. first 함수 내에 실행된 second 함수에 해당하는 Functional Execution Context가 생성 되어 Exection Stack에 추가(push) 됩니다.

작성된 실행 가능한 코드에 대한 Execution Context가 모두 생성 됐으니 Execution Stack의 맨 위에서 부터 Execution Context를 실행 종료합니다.

  1. second 함수의 실행이 종료되어 해당하는 Functional Execution Context가 제거(pop)됩니다.
  2. first 함수의 실행이 종료되어 해당하는 Functional Execution Context가 제거(pop)됩니다.
  3. 모든 코드가 종료되거나 프로그램을 종료하면 Global Execution Context가 제거(pop)됩니다.

Execution Context의 구조

Execution Context의 구조는 LexicalEnvironment와 VariableEnvironment 두개의 Environment로 구성되어 있으며 각 Environment에 공통적으로 Environment Records, Outer, ThisBinding으로 구성되어 있습니다.

쉽게 이해를 돕기 위해 객체로 표현했지만 실제로 엔진이 객체를 만들어 관리하진 않습니다. 때문에 Execution Context를 조회하거나 조작하는 것은 불가능합니다.

LexicalEnvironment

ECMAScript에서 정의하는 LexicalEnvironment는 자바스크립트 코드의 특정 변수, 함수에 대한 식별자를 정의하는 데 사용되는 규격 유형입니다. 간단히 말해서 LexicalEnvironment는 식별자들에 대한 정보를 매핑한 모음이라고 할 수 있습니다. LexicalEnvironment는 FunctionDeclaration, BlockState 또는 Try/Catch 코드와 같은 특정 구문을 만날 때 마다 새로운 LexicalEnvironment를 생성합니다.

식별자들이 매핑된다는 것이 무엇인지 쉽게 이해하기 위해 간단한 코드를 하나 준비했습니다. 위의 코드에서 식별자는 변수인 a,b 함수인 fn() 이렇게 3개가 있습니다. 이러한 정보들은 LexicalEnvironment에 매핑되어 아래와 같은 모양이 만들어집니다.

위의 모양은 이해를 쉽게 돕기위해 간단하게 만들었지만 사실 LexicalEnvironment는 위에서 말했듯이 Environment Records, Outer, ThisBinding이라는 3가지의 내부 속성이 있습니다. 각 속성은 각자의 역할이 있습니다.

Environment Records

Environment Records는 식별자와 참조, 값들이 기록되는 공간입니다. 쉽게 말해 variable, constant, let, function declarations, class, module, import 등 다양한 코드의 정보를 기록하는 공간이라고 할 수 있습니다.

Outer

Outer는 Lexical Scope를 기준으로 외부의 Lexical Environment를 참조하여 외부의 Scope를 탐색할 수 있습니다. 여기서 우리는 Scope Chain이라는 개념을 알 수 있습니다. 변수를 탐색하는 과정에서 Scope에 해당하는 변수가 없다면 Outer에 참조된 외부의 Lexical Environment에 접근해 변수를 탐색합니다. 이 행동은 변수를 탐색하기 까지 계속 반복되며 Global Excution Context까지 탐색을 하고 없다면 에러를 출력하게 됩니다. 그리고 Global Excution Context은 더 이상 참조 할 Environment가 없기 때문에 기본적으로 Outer에 null이 할당됩니다.

ThisBinding

ThisBinding에서는 this 값이 결정됩니다. Global Execution Context라면 this는 전역 객체로 결정되고 Function Execution Context라면 함수가 호출되는 방식에 따라 달라집니다.

VariableEnvironment

ECMAScript에서 정의하는 VariableEnvironment는 VariableStatements에서 만든 바인딩을 EnvironmentRecord에 보관하는 LexicalEnvironment입니다. 기본적으로 VariableEnvironment는 LexicalEnvironment를 참조하고 있기 때문에 서로 같은 Environment라고 할 수 있습니다.

위에서 설명한 것처럼 VariableEnvironment도 LexicalEnvironment이기 때문에 LexicalEnvironment에서 설명한 모든 특성과 구성 요소를 동일하게 가지고 있습니다.

다만 ES6에서의 LexicalEnvironment와 VariableEnvironment에서 한 가지 차이점이 있습니다. LexicalEnvironment는 함수와 변수 let, const를 매핑하고 블록문 만큼의 범위를 가지고 있으며 새로운 블록문을 만나면 새로운 LexicalEnvironment가 생성됩니다. 반면 VariableEnvironment는 변수 var만을 매핑하고 함수 만큼의 범위를 가지고 있습니다.

Execution Context 생성 과정

Excution Context는 생성 단계 (Creation Phase)와 실행 단계 (Execution Phase) 두 단계를 통해 생성됩니다. 앞서 정리한 Execution Stack과 Execution Context 구조를 이해했다면 이젠 Context가 생성되는 단계별로 어떤 일이 일어나는지 알아봅시다.

생성 단계 (Creation Phase)

생성 단계에서는 실행 가능한 코드를 읽어 코드를 실행하는데 필요한 VariableEnvironment와 LexicalEnvironment를 정의합니다. 이 과정에서 Outer, ThisBinding이 결정되고 EnvironmentRecord에 식별자가 매핑됩니다.

위에서 한번 설명했지만 EnvironmentRecord에 매핑되는 식별자는 VariableEnvironment인지 LexicalEnvironment인지에 따라 달라집니다.

  • VariableEnvironment의 경우 var 키워드로 선언된 변수만 매핑되며 초기값으로 undefined가 할당됩니다.
  • LexicalEnvironment의 경우 let과 const 키워드로 선언된 변수를 매핑하며 초기값을 할당하지 않습니다. 또한 함수 전체가 메모리에 할당됩니다.

위의 코드로 Execution Context가 생성 단계에서 어떠한 형태로 만들어지는지 확인해 봅시다.

LexicalEnvironment
LexicalEnvironment의 EnvrionmentRecords는 앞서 설명했듯이 let 키워드로 선언된 변수와 함수를 매핑하고 있고 let과 const는 생성 단계에서 초기값을 정해주지 않아 값이 할당되지 않은 상태입니다. 그리고 지금의 Context는 GlobalExcutionContext이기 때문에 Outer는 null이 기본적으로 할당 되고 ThisBinding의 경우도 전역 객체를 참조하고 있습니다.

VariableEnvironment
VariableEnvironment의 EnvrionmentRecords는 앞서 설명했듯이 var 키워드로 선언된 변수를 매핑합니다. 또한 생성 단계에서 undefined를 초기값으로 할당시켜 줍니다.

실행 단계 (Execution Phase)

실행 단계에서는 다시 한번 코드를 위에서 부터 아래로 읽으며 각 EnvrionmentRecords에 저장된 식별자의 메모리에 값을 수정 혹은 할당 시켜주는 작업을 진행합니다.

엔진은 위의 코드를 다시 한번 읽으면서 변수 식별자인 globalVarglobalLet을 만난 엔진은 각 유형에 맞는 Environment의 EnvironmentRecords에서 해당하는 식별자를 탐색하고 할당을 진행합니다.

이렇게 생성과 실행 단계를 끝마친 GlobalExcutionContext는 위와 같은 정보를 담고 있습니다.

정리

위의 코드가 실행시 모든 과정을 정리하며 Execution Context를 정리해보겠습니다.

1. Global Execution Context 생성 단계

처음 코드를 실행하면 Global Execution Context가 생성 됩니다. 생성된 Context는 Execution Stack에 Push되고 코드에 대한 정보들을 수집해 EnvironmentRecords, Outer, ThisBinding에 정보들을 매핑합니다.

Variable Envrionment에는 var 키워드로 선언된 globalVar가 매핑되며 초기값으로 undefined를 할당 받습니다. Lexical Envrionment에는 outer 함수가 매핑되며 공통적으로 두 Environmet가 전역 컨텍스트이기 때문에 Outer에 null이 할당됩니다. 마찬가지 이유로 ThisBinding에도 전역 객체를 참조합니다.

2. Global Execution Context 실행 단계

실행 단계에서는 다시 한번 코드를 위에서 부터 아래로 읽어가며 식별자에 대한 값을 탐색합니다. Environment에 매핑되어 있는 식별자를 찾았다면 해당 식별자에 값을 할당시켜 줍니다. 때문에 Variable Environment에 매핑된 globalVar"Global!!"이 할당된 것을 알 수 있습니다.

3. Function Execution Context 생성 단계

outer 함수가 호출되면 새로운 Function Execution Context가 Execution Stack에 push됩니다. Global과 마찬가지로 정보를 수집해 Environment에 매핑하지만 이상하게 Variable Environment에 blockVar까지 매핑이 되어 있습니다.

Variable Environment는 함수 단위로 평가가 이뤄지기 때문에 함수 내의 var 키워드를 모두 수집하기때문에 블록문 안에 있는 blockVar까지 Environment에 매핑됩니다.

Lexical Environment는 let 키워드를 사용한 변수를 매핑했습니다. let은 var와는 다르게 생성 단계에서 초기화되지 않습니다.

outer 함수의 Execution Context는 주변에 참조 가능한 상위 스코프가 존재하므로 Outer에 전역 실행 컨텍스트가 할당됩니다.

4. Function Execution Context 실행 단계

blockVar는 아직 블록문안의 코드가 실행되지 않아 할당이 이뤄지지 않습니다. 그 외에는 Global의 실행 단계와 별 차이 없이 진행됩니다.

4-1. 새로운 Lexical Environment 생성 단계

outer 함수가 실행되면서 블록문(if문)을 만나 새로운 Lexical Environment가 생성됩니다. 위에 설명했듯이 Lexical Environment는 블록문을 만나면 새롭게 생성되는 특징이 있습니다.

4-2 새로운 Lexical Environment 실행 단계

실행 단계에서 blockVar를 만나 Variable Environment에 할당되지 않던 blockVar"BlockVar!!"를 할당합니다.

5. 끝

모든 코드의 실행이 종료되면 Excution Stack 맨 위에서 부터 Context가 하나하나 제거 됩니다.


이렇게 Excution Context가 생성되고 실행되는 것을 단계별로 확인하면서 코드가 실행되기까지 어떤 흐름으로 진행되는지 알아봤습니다. 따로 설명은 넣어두지 않았지만 이 글에서 hoisting, scope chain (lexical nesting structure), var와 let,const의 차이점 등 여러 개념들이 등장했습니다. 실행 컨텍스트 하나만으로도 중요한 자바스크립트의 개념들을 이해할 수 있을 만큼 실행 컨텍스트는 아주 중요한 부분이라고 생각합니다.

다음 글은 이 글에서 미처 설명하지 못한 자바스크립트의 중요한 개념들을 작성해보려 합니다. 긴 글 읽어주셔서 감사드리고 틀린 부분이 있다면 저의 성장을 위해 과감한 피드백 부탁드립니다.

profile
안녕하세요 주니어 프론트엔드 개발자입니다.

0개의 댓글