실행 컨텍스트 정리

2024년 3월 31일

자바스크립트의 실행 컨텍스트(Execution context)는 말 그대로 자바스크립트의 코드가 실행되는 환경이다.

여기서 환경이란 자바스크립트가 접근할수 있는 this나, 변수, 객체, 함수 같은 값들을 의미한다.

여기서 실행 컨텍스트는 총 3가지의 종류로 나뉘어진다.

전역 실행 컨텍스트, 함수 실행 컨텍스트, Eval 이다. (여기서 Eval은 제외한다.)

- 전역 실행 컨텍스트 (Global Execution Context)

첫번째로 전역 실행 컨텍스트(Global execution context)는 브라우저 내에서 파일이 첫번째로 로드 될때 JS 코드가 실행을 시작하는 기본 실행 컨텍스트이다.

함수나 객체 내부가 아닌 모든 전역 코드는 전역 실행 컨텍스트 내에서 실행 된다

이 전역 실행 컨텍스트는 JS 코드 실행을 위한 글로벌 환경이 단 하나이기에 둘 이상일수 없다.

또한 JS 엔진이 싱글 스레드로 작동하기 때문에 이와 같은 제한이 있다.

- 함수 실행 컨텍스트 (Functional Execution Context)

함수 실행 컨텍스트는, JS 엔진이 함수 호출을 찾을 때마다 생성되는 컨텍스트로 정의 된다.

각 함수는 자신만의 실행 컨텍스트를 가진다.

따라서 함수 실행 컨텍스트는 전역 실행 컨텍스트와 다르게 여러개일수 있다.

함수 실행 컨텍스트에서는 글로벌 실행 컨텍스트의 모든 코드에 접근할 수 있지만, 반대의 경우 즉, 전역 실행 컨텍스트에서 함수 실행 컨텍스트에는 접근이 불가능하다.

const aValue = 1;

function B() {
  let bValue = 2;
  console.log(aValue); // 1;
}

B();

console.log(bValue); // b is not defined 에러가 발생합니다.

위의 코드처럼, 전역 스코프내에서 B 함수 컨텍스트 안에 있는, bValue의 값을 호출하려고 할때, not defined 에러가 발생하는 것을 볼수 있다.

전역 실행 컨텍스트의 코드를 실행하는 도중에 JS 엔진이 새로운 함수 선언문을 호출 하게 된다면, 그 함수를 위한 새로운 함수 실행 컨텍스트를 생성한다.

- 실행 컨텍스트 스택 (Execution context stack)

실행 컨텍스트 스택은 (ECS)은 스크립트의 생명주기 동안에 생성되는 모든 실행 컨텍스트를 저장하기 위해 사용되는 스택 자료구조이다.

즉, 선입 후출의 구조를 가지고 있다.

전역 실행 컨텍스트는 기본적으로 스택의 가장 아래에 위치한다.

JS 엔진이 전역 실행 컨텍스트를 실행하는 동안, 함수의 호출을 찾게 된다면, 실행 컨텍스트 스택 위에 함수 실행 컨텍스트를 추가한다.

JS 엔진은 실행 컨텍스트 스택의 상단에 있는 함수 실행 컨텍스트를 가진 함수를 실행한다.

만약 함수가 실행이 끝난다면, JS 엔진은 함수의 실행 컨텍스트를 제거하고, 다음 아래에 존재하는 함수를 실행한다.

- 실행 컨텍스트 내부 예시

Execution Context = {
  ThisBinding: {},
  LexicalEnvironment: {
      EnvironmentRecord: {
          DeclarativeEnvironmentRecord: {},
          ObjectEnvironmentRecord :{},
      },
      OuterEnvironmentReference: {},
  },
  VariableEnvironment: {
      EnvironmentRecord: {
          DeclarativeEnvironmentRecord: {},
          ObjectEnvironmentRecord :{},
      },
      OuterEnvironmentReference: {},
  }
}

실행 컨텍스트의 내부를 살펴보면 대략적으로 이런식으로 구성 되어있다.

하지만 이는 JS 엔진 내부의 로직을 간편화 하기 위해서 이렇게 표현 한것이므로 완벽히 재현 되어있다고 하기는 어렵다. (간편화 한거니 양해 부탁드립니다 🙇)

자 그러면 지금부터 간략히 하나하나 살펴보자.

ThisBinding

이는 현재 실행 컨텍스트에서 this 키워드가 가리키는 값이다.

전역 컨텍스트 내에서의 this는 전역 객체를 가리킨다.

브라우저에서는 window, Node.js 환경 에서는 global 객체가 된다.

Lexical 환경

이는 실행 컨텍스트의 일부로, 식별자 key, 값은 value라는 구조를 이루어서 변수 이름과 그 변수의 값을 저장하는 데 사용된다.

렉시컬 환경은 환경 레코드와 외부 렉시컬 환경에 대한 참조(부모 스코프)로 구성된다.

여기서 환경 레코드는 또 선언적 환경 레코드객체 환경 레코드로 나뉘어진다.

여기서 선언적 환경 레코드는 우리가 흔히 사용하는 let과, const 그리고 우리가 선언한 함수들의 정보를 담고, 객체 환경 레코드는 특별히 with문이나 eval같은 특정 상황에서의 객체 바인딩을 위하여 사용 된다.

그리고 이 선언적 환경 레코드객체 환경 레코드는 실행 컨텍스트가 생성될때 동적으로 결정된다.

그리고 Lexical 환경 안에 있는 OuterEnvironmentReference는, 현재 컨텍스트가 참조하는 외부 렉시컬 환경, 즉 상위 스코프에 대한 참조이다.

전역 컨텍스트에서는 일반적으로 null이나 다른 외부 스코프가 없으므로 이 부분은 비어 있다.

세번째로는 VariableEnvironment 이다.

이 VariableEnvironment는 var로 선언된 변수들의 호이스팅을 처리하기 위해서 사용 된다.

이는 LexicalEnvironment와 비슷한 구조를 가지지만, 주로 var의 선언을 다루는데에 초점을 맞춘다.


위의 내용은 실행 컨텍스트에 대한 기본적인 종류와 구조이다.

이제 이 실행 컨텍스트가 어떻게 만들어지는지 알아보자.

이에 대한 설명 전에, JS가 코드를 어떻게 해석하는지에 대해서 알아두어야한다.

JS 엔진은 기본적으로 컴파일과 실행 단계를 거친다.

컴파일에서는 변수 선언, 함수 선언 등에 대한 정보들을 수집하고,

실행 단계에서 위에서부터 아래로 차근차근 코드들을 실행한다.

마찬가지로 실행 컨텍스트 또한 생성 단계(Creation Phase)실행 단계(Execution phase)로 나뉘어진다.

아래의 코드 예시를 살펴보자.

- 예시

var a = 1;

function test1() {
  const b = 2;

  function test2() {
    const c = 3;

    return b + c;
  }

  return a + test2();
}

const b = test1();

console.log(b);

- 전역 실행 컨텍스트 생성 단계

Execution Context = {
  ThisBinding: {}, // window 값을 가진다
  LexicalEnvironment: {
      EnvironmentRecord: {
          DeclarativeEnvironmentRecord: {
            test1 : //함수 선언은 메모리에 저장된 후 해당 함수를 가리키는 참조가 환경 레코드에 설정
            b : // 메모리 공간은 할당 받지만, 접근을 하지는 못하는 상황
          },
      },
      OuterEnvironmentReference: {},
  },
  VariableEnvironment: {
      EnvironmentRecord: {
          DeclarativeEnvironmentRecord: {
            a : undefined // 초기에 undefined로 초기화
          },
      },
      OuterEnvironmentReference: {},
  }
}

첫번째로 JS 엔진 내에서 전역 실행 컨텍스트를 생성한다.

이 생성하는 과정에서 var 와 같은 전역변수를 variableEnvironmentundefined로 초기화한다.

test1 이라는 함수 선언식을 만나게 되는데, 이 함수는 직접적으로 레코드 안에 할당되지 않는다.

해당 함수는 JS엔진 내에서의 힙 메모리안에 해당 함수를 저장하고, 이 저장된 주소를 가리키는 포인터가 DeclarativeEnvironmentRecord선언적 환경 레코드안에 할당 된다.

그리고 const b 라는 선언을 만나게 된다.

이는 var 키워드와 다르게 생성 단계에서 undefined로 선언되지 않고, Temporal Dead Zone 이라는, 즉 일시적 사각지대인 TDZ 에 들어가게 된다.

이는 변수가 선언은 되지만, 해당 코드 라인에 도달하기 전까지 접근할수 없음을 의미 한다.

즉 실행 컨텍스트 생성 단계에서 메모리 공간을 할당은 받지만, 값에 대한 접근은 아직 허용이 되지 않은 상태다. 이는 실행 컨텍스트 실행 단계에서 직접적으로 할당 과정이 진행 된다.

- 실행 단계

생성 단계를 거친 후에 실행 단계를 거치게 된다.

var a는 원래 undefined 였지만, 전역 컨텍스트를 실행하게 되면서 해당 값인 1 이 할당 받게 된다.

또한 const b에 test1 이라는 함수가 호출이 되어서 할당이 되는데, 여기서 test1 함수를 호출하게 되면서 새로운 함수 실행 컨텍스트가 생성이 된다.


- test1의 함수 실행 컨텍스트 생성

여기서 전역 컨텍스트 위에 test1의 함수 실행 컨텍스트가 새롭게 생성 된다.

여기서 전역컨텍스트와 마찬가지로 생성과 실행 단계를 거치게 된다.

- test1의 실행 컨텍스트

test1 Execution Context = {
  ThisBinding: { global},
  LexicalEnvironment: {
      EnvironmentRecord: {
          DeclarativeEnvironmentRecord: {
            b : // TDZ
            test2 : // test2 함수에 대한 참조값 주소
          },
      },
      OuterEnvironmentReference: {
        // 전역 환경 참조
      },
  },
  VariableEnvironment: {
      EnvironmentRecord: {
          DeclarativeEnvironmentRecord: {
            a : 1,
          },
      },
      OuterEnvironmentReference: {
        // 전역 환경 참조
      },
  }
}

1. this 바인딩

여기서 test1() 함수는 전역에서 호출이 되었으므로 전역 객체가 바인딩 된다.

2. LexicalEnvironment 생성

여기서는 b가 const로 선언이 되었기 때문에, 변수에 대한 메모리를 할당을 받고, TDZ 로 들어가게 된다.

그리고 test2에 대한 함수 선언식을 만나게되어서, 또 test2에 대한 참조 값이 할당 된다.

그리고 이는 전역 컨텍스트를 참조하고 있기때문에, OuterEnvironment에서는 전역 환경을 참조하게 되고, 이로써 전역 컨텍스트에서 선언한 변수나, 함수에 접근할수 있게 된다.

VariableEnvironment 또한 외부에서 var 키워드로 선언한 키워드들을 참조할수 있다.

여기서는 전역 컨텍스트에서 a라는 변수에 1을 할당했으므로, 이 값을 참조할수 있다.

그 후에 test1 실행 컨텍스트가 실행이 되면서 b에는 2가 할당이 되고 test2()함수를 호출하게 된다.

여기서 또 마찬가지로 test2에 대한 실행 컨텍스트가 생성이 된다.


- test2에 대한 실행 컨텍스트 생성

test2 Execution Context = {
  ThisBinding: { global },
  LexicalEnvironment: {
      EnvironmentRecord: {
          DeclarativeEnvironmentRecord: {
            c : // TDZ
          },
      },
      OuterEnvironmentReference: {
        // test1과, 전역 환경 참조
      },
  },
  VariableEnvironment: {
      EnvironmentRecord: {
          DeclarativeEnvironmentRecord: {
            a : 1,
          },
      },
      OuterEnvironmentReference: {
        // test1과 전역 환경 참조
      },
  }
}

test1과 마찬가지로 test2는 일반 함수로써 호출 됐으므로, this 값은 전역 객체를 가리킨다.

또 const 키워드로 선언된 c는 TDZ 구간에 진입을 하게 되고

여기서 Outer 환경에서 test1과 전역환경의 변수와 함수 값들을 참조할수 있게 된다.

이제 test2의 실행 컨텍스트가 실행이 된다.

이 실행 단계에서 test1 함수나, 전역 컨텍스트를 실행하며 선언했던 변수들을 사용할수 있게 된다.

여기서 test1에서 선언했던 const b = 2값을 사용하게 되면서 5를 리턴하게 된다.

이렇게 되면 test2의 실행 컨텍스트는 종료가 된다.

마찬가지로 test1함수의 실행컨텍스트에서, test2의 리턴값이었던 5를 받게되고, 전역에서 선언했던 a인 1과 5를 더하게 되어서 최종적으로 6이라는 값을 const b에 할당하게 된다.

그 후에 전역 컨텍스트에 있는 console.log(b)가 실행되면서 6을 출력하게 된다.