자바스크립트 - Execution Context, Call Stack

kdeun1·2021년 6월 21일
3
post-thumbnail

실행 컨텍스트 (Execution Context)

실행 컨텍스트(Execution Context)는 코드를 실행되기 위한 환경이자 핵심 원리이다. 실행 가능한 코드를 형상화하고 구분하는 추상적인 개념이다. 한마디로 실행 가능한 소스 코드를 평가하고 실행하기 위해 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리하는 환경이다.
실행 컨텍스트 스택(Execution Context Stack)은 실행 컨텍스트(Execution Context)를 추적하는데 사용된다. 스택의 최상위 요소가 현재 실행 중인 실행 컨텍스트이다.
실행 컨텍스트는 순수하게 스펙적인 메커니즘이며 ECMAScript 구현은 아래에 작성된 실행 컨텍스트 로직과 동일하다는 뜻은 아니다. 자바스크립트 코드가 실행 컨텍스트에 직접 접근하거나 관찰하는 것은 불가능하다.
실행 컨텍스트는 자바스크립트에서 호이스팅, 클로저, 스코프 개념을 이해하는데 기본이 된다.
아래의 설명은 기본적으로 ES6+ 실행 컨텍스트에 대한 설명을 나열하였다. ES5의 내용은 중간에 참고 형태로 추가하였다.

실행 컨텍스트의 상태 컴포넌트


실행 컨텍스트는 총 4가지의 상태 컴포넌트로 구성된다 : code evaluation state, Function, Realm, ScriptOrModule

  • code evaluation state : 실행 컨텍스트의 코드를 실행, 일시중단, 재개하는데 필요한 모든 상태를 가지는 컴포넌트이다.
  • Function : 함수 실행 컨텍스트일 때 해당 함수 객체를 가지는 컴포넌트이다. 실행 컨텍스트가 함수 코드를 평가하는 경우, 컴포넌트의 값은 해당 함수의 객체가 된다. 실행 컨텍스트가 스크립트 또는 모듈 코드를 평가하는 경우 값은 null이다.
  • Realm : 활성 객체를 가진 컴포넌트이다. ECMAscript 리소스에 접근하는 코드에 대한 Realm Record이다.
  • ScriptOrModule : 해당 실행 컨텍스트의 스크립트 요약 정보와 연관된 모듈 정보를 가지는 컴포넌트이다.

간단한 실행 컨텍스트와 콜스택의 예제

function first() {
  console.log('first() 함수 컨텍스트 - second() 호출 전');
  second();
  console.log('first() 함수 컨텍스트 - second() 호출 후');
}

function second() {
  console.log('second() 함수 컨텍스트 - third() 호출 전');
  third();
  console.log('second() 함수 컨텍스트 - third() 호출 후');  
}

function third() {
  console.log('third() 함수 컨텍스트');
}

console.log('전역 실행 컨텍스트 - first() 호출 전');
first();
console.log('전역 실행 컨텍스트 - first() 호출 후');

> 전역 실행 컨텍스트 - first() 호출 전
> first() 함수 컨텍스트 - second() 호출 전
> second() 함수 컨텍스트 - third() 호출 전
> third() 함수 컨텍스트
> second() 함수 컨텍스트 - third() 호출 후
> first() 함수 컨텍스트 - second() 호출 후
> 전역 실행 컨텍스트 - first() 호출 후

위 코드에서 실행 컨텍스트는 함수를 호출한 경우에 생성된다. 단순하게 함수를 선언하는 경우에는 메모리 할당도 일어나지 않는다.
function 키워드로 선언된 함수를 함수명으로 호출하기 위해서 소괄호(())를 붙여서 함수를 동작시킬 수 있다.


참고로 함수 선언문으로 사용된 first(), second(), third()는 각 식별자에 전체 함수 선언 텍스트가 저장되며, 이를 실행하였을 때 함수 실행 컨텍스트가 생기는 것이다. 선언과 할당 그리고 실행에 대한 로직을 이해해야한다.


추가적인 2가지 상태 컴포넌트 (LexicalEnvironment Component, VariableEnvironment Component)


ECMAScript 2021 언어 스펙 12th 에디션에 있는 내용이다. 추가적인 2가지 상태 컴포넌트는 LexicalEnvironment component, VariableEnvironment component가 있다.

LexicalEnvironment Component (LE, LEC)


실행 컨텍스트 내의 코드에서 만든 식별자 참조를 해결(Identifier Resolution)하는데 사용되는 Environment Record를 식별한다.
LE는 식별자의 reference를 갖고 있어 값이 변하는 것을 알 수 있다. 때문에 변수에 값이 재할당되는 변경 즉시 환경 정보를 저장하는 것이다.
let, const declarations은 실행 중인 실행 컨텍스트의 LexicalEnvironment로 범위가 지정된 변수를 정의한다.
let, const 변수의 선언과 동시에 할당하는 경우 변수 생성할 때가 아닌 Lexical Binding이 평가될 때 초기 값이 할당된다.

VariableEnvironment Component (VE, VEC)


실행 컨텍스트 내에서 VariableStatements에 의해 생성된 바인딩을 보유하는 Environment Record를 식별한다.
초기에 LE와 동일한 내용으로 구성되지만, 초기 상태의 변수 정보만을 유지(저장)한다.
실행 컨텍스트 생성 시 변수 정보를 수집하여 Variable Environment를 만든다.
var statement은 실행 중인 실행 컨텍스트의 Variable Environment로 범위가 지정된 변수를 선언한다. var 변수는 Environment Record가 인스턴스화 될 때 생성되고, 생성 시 undefined로 초기화된다. 선언과 동시에 할당하는 경우는 변수가 생성할 때가 아닌 Variable Declaration이 실행될 때, 값이 할당된다.

ExecutionContextES6 = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}

ES6+의 실행 컨텍스트의 추상적인 구조는 다음과 같다. 실행 컨텍스트 내 Lexical Environment와 Variable Environment 컴포넌트는 항상 Environment Records가 존재한다. 대부분의 상황에서 Execution Context Stack의 맨 위의 실행 중인 Execution Context만이 해당 스펙 내의 알고리즘에 의해 직접 조작된다.

ES6+의 LexicalEnvironment Component (LEC) 상세 설명

렉시컬 환경(Lexical Enviromnemt, LE)은 함수와 변수의 식별자 해결을 위한 환경 설정이다. 자바스크립트 엔진이 자바스크립트 코드를 실행하기 위해 자원을 모아둔 것이며, 함수 또는 블록의 유효 범위 안에 있는 식별자와 값이 저장되는 곳이다.
Lexical Environment는 함수 호출, 실행 시점에서 생성된다. 자바스크립트 엔진은 함수 초기화 단계에서 유효 범위 안에 있는 해석한 식별자와 값(함수와 변수)을 key-value 쌍 형태({ name: value })로 저장한다.
참고로 new 키워드로 새로운 인스턴스를 생성하는 경우 생성자 함수도 새로 생성할 인스턴스가 this가 바인딩을 되는 점을 제외하면 일반적인 함수 실행과 동일하다.
LexicalEnvironment Component의 구조는 2가지로 구성된다.

환경 레코드 (Environment Record, ER)

Environment Record is a specification type used to define the association of Identifiers to specific variables and functions, based upon the lexical nesting structure of ECMAScript code. — ECMAscript

Environment Record는 ECMAScript 코드의 lexical 중첩 구조 기반으로 특정 변수, 함수에 대한 식별자 연결을 정의하는데 사용되는 특별한 타입이다. 일반적으로 Environment Record는 FunctionDeclaration, BlockStatement, Try ~ Catch절과 같은 ECMAScript 코드과 같은 몇가지 특정 syntactic(구문) 구조와 연결된다. 이러한 코드가 평가될 때마다 해당 코드에 의해 생성된 식별자 바인딩을 기록하기 위하여 새로운 Environment Record가 생성된다. 쉽게 말해 Environment Record는 현재 실행될 컨텍스트의 코드 내 어떠한 식별자가 있는지에만 관심이 있다는 뜻이다.

Environment Record에 스코프 내 선언부를 기록한다.(함수의 경우 함수 내부의 함수와 변수를 기록한다.) 참고로 함수의 경우 Environment Record에 arguments 객체가 포함된다.

Environment Record는 3개의 구체적인 하위 클래스가 있는 단순한 객체 지향 구조의 추상 클래스이다.
Declarative Environment Record, Object Environment Record, Glocal Environment Record가 하위에 존재한다.

  • Declarative Environment Record (선언적 환경 레코드)
    ECMAScript 언어로 표현되는 값들과 식별자를 직접적으로 연결해주는 FunctionDeclarations, VariableDeclarations 그리고 Catch절과 같은 문법 요소의 효과를 정의하는데 사용된다.
    DER은 variable, constant, let, class, module, import, function 선언을 포함하는 ECMAScript 프로그램 스코프와 연결되어있다. DER은 스코프 내에 포함된 선언에 의해 정의된 식별자 집합을 등록하고 바인딩한다.

  • Function Environment Record [extends Declarative Environment Record] (함수 환경 레코드)
    Function Environment Record는 ECMAScript의 function 객체의 호출에 관련있다. FER은 함수의 최상위 범위를 나타내는 데 사용되는 DER이며, 함수가 화살표 함수가 아닌 경우 this 바인딩을 제공한다. 또한, super 메소드 호출을 지원하는데 필요한 상태를 가지고 있다.

  • Module Environment Record [extends Declarative Environment Record] (모듈 환경 레코드)
    Module Environment Record에는 ECMAScript 모듈의 외부 범위를 나타내는데 사용되는 DER이며, 최상위 선언에 대한 바인딩이 포함되어있다. 여기에는 모듈에서 명시적으로 import되어있는 바인딩도 포함된다. [[OuterEnv]]는 gloval Environment Record이다.

  • Object Environment Record (객체 환경 레코드)
    식별자 바인딩을 어떤 특정 객체의 속성과 연결하는 WithStatement과 같은 ECMAScript 요소의 효과를 정의하는데 사용된다.
    OER은 binding object라 불리는 객체와 연결된다. 바인딩 객체의 프로퍼티명에 직접 해당하는 문자열 식별자명 집합과 바인딩된다.

  • Global Environment Record (전역 환경 레코드)
    스크립트 전역 선언에서 사용된다. outer environment가 없으므로 [[OuterEnv]]는 null이다.
    GER은 공통 realm에서 처리되는 모든 ECMAScript 스크립트 요소가 공유하는 가장 바깥쪽 스코프를 나타내는데 사용된다. GER은 스크립트 내 발생하는 built-in globals(내장 전역?), global object의 프로퍼티, 모든 최상위 선언의 바인딩을 제공한다.

This 바인딩

ES6+의 This Binding은 ES5와 다르게 LexicalEnvironment 내에 존재한다.
전역 환경 레코드의 [[GlobalThisValue]], 함수 환경 레코드의 [[ThisValue]] 내부 슬롯에 this가 바인딩된다.
Environment Record의 this 바인딩 값을 리턴하는 메소드인 getThisBinding()은 Function Environment Record, Global Environment Record, Module Environment Record에만 존재한다.
this에 대해서는 나중에 쓸 글에 자세하게 다룰 예정이다.

외부 렉시컬 환경 참조 (Outer Lexical Environment Reference, OLER)

Scope Chain은 ES3에서 사용된 정식 용어인데, ES6+에는 Scope Chain 이라는 용어가 존재하지 않는다.
ES6+의 스펙 문서에서는 Environment Record가 lexical structure of ECMAScript 코드의 기반으로 한다. 모든 Environment Record에는 null(Global Execution Context인 경우) 또는 외부의 Environment Record(GEC가 아닌 경우)에 대한 참조인 [[OuterEnv]]필드가 있다고 되어있다.
이는 Environment Record의 OLER이 Scope Chain과 상응하는 개념을 갖고 있으며, 중첩된 스코프 내에서 변수를 검색하는(체이닝하는) 메커니즘이라고 할 수 있다.
자바스크립트 엔진은 현재 Lexical Environment에서 해당 변수를 찾고 검색에 실패하면 다음 연결 리스트인 외부(상위) Lexical Environment를 참조하여 변수를 검색하는 탐색을 이어나간다. 이런 탐색은 OuterEnv이 null을 만나는 경우 즉, Global Execution Context일 경우 종료가 된다. 만약 변수를 찾지 못하는 경우 정의되지 않은 변수를 찾는 것으로 판단하여 Refenrence Error를 발생시킨다. (논리적 중첩 구조 탐색)

추상화된 구조

ExecutionContextES6 = {
  LexicalEnvironment = {
    EnvironmentRecord = { },
    ThisBinding = { },
    OuterLexicalEnvironmentReference = { },
  },
  VariableEnvironment = {
    EnvironmentRecord = { },
    ThisBinding = { },
    OuterLexicalEnvironmentReference = { },
  },
}

ES6+의 Lexical Environment Component의 추상화된 구조는 위와 같다. 개념적인 모양이지 실제로 저런 구조처럼 생기지 않았다.
실행 컨텍스트 생성, LexicalEnvironment 컴포넌트 생성, EnvironmentRecord 생성, this 바인딩, 외부 렉시컬 환경에 대한 참조 순으로 진행된다.


ES5 (ECMAScript 5.1 Edition)의 Execution Context의 구조

ES5의 Execution Context의 상태 컴포넌트는 위와 같이 3가지가 존재한다. 구글링한 결과 각 블로그마다 실행 컨텍스트에 구조를 기술하는게 달라서 엄청나게 헷갈렸다. 이는 ES3, ES5, ES6+ 버전에 따라 실행 컨텍스트 및 내부 구조가 달랐기 때문이라는 것을 ECMAScript의 상세 문서를 보고 알았으며, 원본을 해석하기에 너무 발번역이 될 것 같아 참고 표를 직접 캡쳐하였다.

LexicalEnviromnent Component

실행 컨텍스트 내의 코드에서 만든 식별자 참조를 해결하는데 사용되는 Lexical Environment를 식별한다.
Lexical Environment는 자바스크립트 엔진이 코드를 실행하기 위해 자원을 모아두는 곳이며, 함수 또는 블록의 유효 범위 안의 있는 식별자와 값이 저장되는 곳이다.
Lexical Environment Component 내부에 Enviromnent Record와 Outer Lexical Environment Reference가 존재한다.

  • 환경 레코드 (Environment Record)
    함수 내부의 함수과 변수를 기록한다.

  • 외부 렉시컬 환경 참조 (Outer Lexical Environment Reference)
    외부 렉시컬 환경 참조에 function 오브젝트의 [[Scope]]를 설정한다. 이로 인해 함수 밖의 함수, 변수에 접근할 수 있다.

LexicalEnvironment (LEC): {
  EnvironmentRecord (ER): { },
  OuterLexicalEnvironmentReference (OLER): { },
}

VariableEnvironment Component

실행 컨텍스트 내의 VariableStatements와 FunctionDeclarations에 의해 생성된 바인딩을 보유하는 Lexical Environment를 식별한다.
ES5의 Variable Environment Component는 실행 컨텍스트의 변수, 함수의 초기 저장소이다. 정확히 해당 Environment Record가 데이터 저장소로 사용되며, 컨텍스트 단계에 들어갈 때 채워진다.
자바스크립트 엔진이 실행 컨텍스트를 생성할 때 Variable Environment의 Environment Record에 값을 저장하고, 이를 Lexical Environment에 스냅샷 찍어놓는다. 그 후, 실행 컨텍스트 내 코드가 실행되면서 값이 할당되면서 Lexical Environment Component의 값들은 변경되지만, 그에 반해 Variable Environment Component의 값은 변하지 않는다.

ThisBinding

이 실행 컨텍스트와 연관된 ECMAScript 코드 내에서 this 키워드와 연관된 값이다.

ExecutionContextES5 = {
  ThisBinding: <this value>,
  VariableEnvironment: {
    EnvironmentRecord: { },
    OuterLexicalEnvironmentReference: { },
  },
  LexicalEnvironment: {
    EnvironmentRecord: { },
    OuterLexicalEnvironmentReference: { },
  },
}

ES5 실행컨텍스트와 ES6 실행컨텍스트의 차이점

ES5와 ES6의 실행 컨텍스트 내부에는 Lexical Environment 컴포넌트와 Variable Environment 컴포넌트가 동일하게 존재한다. 다만, 다른 점은 This Binding의 구조적인 위치이다. ES6+의 This Binding은 LexicalEnvironmentVariableEnvironment 각각 내부에 존재하지만, ES5의 This Binding은 실행 컨텍스트 바로 하위에 LexicalEnvironment와 같은 depth에 위치한다.
그 이유 때문에 var(ES5의 변수 선언 키워드)와 let, const(ES6의 변수 선언 키워드)의 SCOPE가 달라진다.
ES5에서는 var가 VE, LE 모두 사용되며 함수 스코프 내 동일하게 작용하고 있다. LE/VE/ThisBinding이 형제로서 구조화되어있다. 실행 컨텍스트가 생성될 때, Lexical Environment와 Variable Environment 컴포넌트는 처음에 동일한 값을 갖는다. Variable Environment 컴포넌트의 값은 절대 변경되지 않지만, Lexical Environment 컴포넌트의 값은 실행 컨텍스트 내에서 코드를 실행하는 동안 변경될 수 있다.

그에 비해 ES6의 키워드인 let, const는 블록 스코프를 사용하고 있다. 이는 ES5의 var의 함수 스코프의 한계점을 극복하기 위해 나온 개념이며, 그에 따라 ThisBinding의 위치도 변경되지 않았을까 하는 개인적인 생각을 가지고 있다.
ES5에서는 VE, LE가 var를 위해 모두 존재하지만, ES6의 LE는 let, const를 위해서 VE는 var를 위해서 존재한다. ES6 환경에서 var 키워드를 호환성의 문제로 버릴 수 없기 때문에 각각의 VE/LE에 스코프를 위한 ThisBinding이 존재한다.

이처럼 ES5와 ES6의 실행 컨텍스트는 ThisBinding의 구조적인 차이와 LE/VE의 역할이 다르다.


실행 컨텍스트 생성 과정

자바스크립트 엔진은 실행 컨텍스트를 2가지 단계로 생성한다.

1. 생성 단계 (Creation Phase(Stage)), 평가 단계 (Evaluate Phase(Stage))

변수, 함수 등의 선언문에 해당하는 모든 코드들이 평가된다. 이 단계에서 execution context를 생성하고 변수, 함수 등의 식별자는 Environment Record에 등록되어 관리된다.
쉽게 말해서 변수와 함수를 저장한다는 뜻이다.
이렇게 등록한 정보를 가지고 Execution Context는 코드 실행에 필요한 정보를 제공한다. 실행단계가 끝나면 해당 코드의 실행 결과는 Environment Record에 다시 등록된다.
execution context의 상태를 위한 두 컴포넌트 객체가 Execution Context 내부에 생성된다.
Lexical Environment 컴포넌트와 Variable Environment 컴포넌트를 생성한다. 두 컴포넌트의 정의가 이루어진다.
함수 실행 컨텍스트는 전역 실행 컨텍스트와 다르게 argumemts 객체가 생성되어 함수 내 스코프에 등록되고 this 바인딩도 결정된다.

2. 실행 단계 (Execution Phase(Stage))

평가 단계가 끝나면 런타임이 시작되어, 자바스크립트 엔진은 코드를 한 줄씩 실행한다. 선언된 변수에 실제 값을 할당되고 내부에 함수가 있다면 함수가 호출된다. 함수가 호출되면 실행 스레드는 실행을 멈추고 엔진컨트롤은 함수 내부로 진입하게 된다.
함수 실행 컨텍스트는 전역 실행 컨텍스트와 다르게 매개변수에도 값이 할당된다.


콜 스택 (Call Stack, Execution Stack)

콜 스택(Call Stack)은 코드가 실행되면서 생성되는 Execution Context를 저장하는 자료구조이다. LIFO (Last In, First out, 후입선출) 구조를 가지고 있다.

자바스크립트는 단일 실행 스레드이므로, 런타임에 하나의 Call Stack을 가지며 Context를 관리한다.
자바스크립트 엔진이 자바스크립트가 실행할 때, Glabal Execution Context를 생성하고 이를 Call Stack에 push한다. 이후 엔진이 함수를 호출할 때(함수명에서 소괄호를 만날 때)마다 함수의 Functional Execution Context를 생성하고 Call Stack에 push(추가)한다. 이 때, 전역에 있는 실행 쓰레드는 잠시 멈추고 함수 실행 컨텍스트 내부로 들어와 한 줄씩 코드를 실행하게 된다.
자바스크립트 엔진은 stack의 맨 위의 execution context에 있는 함수를 실행한다. 이 함수가 실행 완료되면(return 문을 만나게 되는 경우를 말한다. return이 없는 경우 암묵적으로 undefined를 return한다), stack에서 pop(제거)된다.

이처럼 자바스크립트는 call stack을 사용하여 execution thread가 실행되는 위치를 추적할 수 있고, 코드의 실행 순서를 관리한다.. 현재 실행 스레드와 로컬 메모리(Lexical Environment, Variable Environment)의 위치가 콜 스택의 맨 위의 execution context라는 것을 알 수 있다. 맨 위의 execution context는 running execution context라 불린다.


실행 컨텍스트의 생성과 삭제 과정

해당 글[JS]Execution Context와 Call Stack의 하단에 그림과 함께 설명되어있다.


참고

profile
프론트엔드 개발자입니다.

1개의 댓글

comment-user-thumbnail
2021년 6월 30일

마침 최근에 useEffect()를 사용하는 과정에서 실행 컨텍스트가 조금 꼬이는 오류가 생겼었는데 도움이 많이 됐습니다. 좋은 글 감사합니다!!

답글 달기