실행 컨텍스트

저번 시간에 이어 예제를 통해 실행 컨텍스트가 어떻게 생성되고 관리되는지 알아보자

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);// 42

1. 전역 객체 생성

전역 객체는 전역 코드가 평가되기 이전에 생성된다. 전역 객체에는 빌트인 전역 프로퍼티와 전역 함수, 그리고 표준 빌트인 객체가 추가되며 동작 환경에 따라 클라이언트 사이드 Web API(DOM, BOM, Canvas XMLHttpRequest, fetch...) 또는 특정 환경을 위한 호스트 객체를 포함한다.

2. 전역 코드 평가

소스코드가 로드되면 js엔진은 전역 코드를 평가한다. 세부적인 생성 과정을 살펴보자.

2-1. 전역 실행 컨텍스트 생성


먼저 전역 실행 컨텍스트를 생성하여 실행 컨텍스트 스택에 푸시한다.

2-2 전역 렉시컬 환경 생성

전역 렉시컬 환경을 생성하고 전역 실행 컨텍스트에 바인딩한다.
전역 렉시컬 환경은 전역 스코프 역할을 하는 객체 환경 레코드(Object Environment Record)와 선언적 환경 레코드(Declarative Environment Record)로 구성되어 있다.

  • 객체 환경 레코드: 기존(ES6이전)의 전역 객체가 관리하던 var 키워드로 선언한 전역 변수와 함수 선언문으로 정의한 전역 함수, 빌트인 전역 프로퍼티 및 함수, 객체 등을 관리한다.
  • 선언적 환경 레코드: let, const로 선언한 전역 변수를 관리한다.

    여기서 y는 const로 선언한 변수이므로 전역 객체의 프로퍼티가 되지 않기 때문에 window.y와 같이 참조할 수 없다. 또한 const 키워드로 선언한 변수는 "선언 단계"와 "초기화 단계"가 분리되어 진행된다. 따라서 런타임에 실행 흐름이 변수 선언문에 도달하기 전까지 일시적 사각지대(Temporal Dead Zone)에 빠지게 된다.

y에 바인딩 되어있는 uninitialized는 이런 의미이다. 실제로 uninitialized라는 값이 바인딩된 것이 아니다.

이후 [[GlobalThisValue]] 내부 슬롯에 this바인딩이 일어난다. 이후 외부 렉시컬 환경에 대한 참조가 결정된다.

현재 평가 중인 소스코드는 전역 코드로 외부 렉시컬 환경에 대한 참조는 null값이 할당된다.

3. 전역 코드 실행

이제 전역 코드가 순차적으로 실행되며 변수 할당문이 실행되어 변수에 값이 바인딩된다. 그리고 foo 함수가 호출된다.

4. foo 함수 코드 평가

foo 함수가 호출되면 전역 코드의 실행을 일시 중단하고 foo함수의 내부로 제어권이 이동한다. 그리고 함수의 코드가 평가된다.

이 과정을 거쳐 생성된 foo 함수 실행 컨텍스트와 렉시컬 환경은 다음과 같다.

생성 순서는 다음과 같다.

  • 처음 foo 함수의 렉시컬 환경이 생성되며 실행 컨텍스트에 바인딩된다.
  • 이후 foo 함수의 환경 레코드가 생성된다.
  • [[ThisValue]] 내부 슬롯에 this가 바인딩된다. 바인딩 되는 객체는 호출 여부에 따라 결정된다. 예제의 경우 일반 함수로 호출되었으므로 this는 전역 객체를 가리킨다.
  • 외부 렉시컬 환경에 대한 참조가 결정된다. foo 함수는 전역 코드에 정의된 함수이므로 외부 렉시컬 환경에 대한 참조는 전역 렉시컬 환경이 할당된다.

함수의 상위 스코프는 this와 달리 어디에 정의했는지에 따라 결정된다는 사실을 잘 기억하자.

foo함수는 전역에서 정의되었으므로 외부 렉시컬 환경 참조는 전역 렉시컬 환경을 가리킨다.

함수의 렉시컬 환경은 이런식으로 결정된다. 그리고 식별자를 검색할 경우 실행중인 실행 컨텍스트에서 식별자를 검색하며 만약 찾기 못할 경우 외부 렉시컬 환경에 대한 참조가 가리키는 렉시컬 환경으로 즉, 상위 스코프로 이동하며 식별자를 검색한다.

이것이 바로 스코프 체인의 원리이다.

함수의 상위 스코프를 결정하는 원리는 클로저의 원리와도 매우 밀접하므로 잘 기억해야 한다.

5. foo 함수 코드 실행

이제 런타임이 시작되어 foo 함수의 소스코드가 순차적으로 실행되기 시작한다.

6. bar 함수 코드 평가

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);// 42

코드는 이제 bar 함수가 호출되어 bar 함수 내부로 코드의 제어권이 이동한다. 이후 foo 함수와 동일하게 bar 함수의 실행 컨텍스트와 렉시컬 환경이 생성된다.

외부 렉시컬 환경 참조와 this 바인딩을 잘 이해 한다면 위의 그림을 무리 없이 이해할 수 있다.

7. bar 함수 코드 실행

8. bar 함수 내 console.log 실행

console.log(a + b + x + y + z);
코드가 실행되며 환경 기록과 외부 렉시컬 참조의 조합으로 스코프체인이 실행되며 변수의 값을 결정한다.

9. 순서대로 pop

해당 코드가 종료되면 bar -> foo -> global 순으로 실행 컨텍스트가 실행 컨텍스트 스택에서 팝되며 코드가 종료되게 된다.

  • 출처 - 모던 자바스크립트 Deep Dive / 이웅모
profile
웹 개발을 공부하고 있는 윤석주입니다.

0개의 댓글