자바스크립트의 실행 컨텍스트 (execution context)

ggong·2021년 6월 1일
20

자바스크립트에서의 실행 컨텍스트를 좀더 디테일하게 파봅시당!
어디서 멈춰야할지 몰라 아주 아주 긴 글이 될 예정

참고 : 실행 컨텍스트에 대해 찾다보면 ES3와 최근 스펙이 조금 다른 것을 볼 수 있다.
(ES3의 예: Variable Object 등)
이 글에서는 ES5 이후를 기준으로 설명할 예정이다.


1. 실행 컨텍스트란

자바스크립트 엔진은 코드를 실행하기 위해서 실행에 필요한 여러가지 정보들을 알고 있어야 한다. 예를 들면 변수(전역변수, 매개변수 등)와 변수의 유효 범위, this와 같은 정보들이 필요하다.
어떤 실행 컨텍스트가 활성화될 때, 자바스크립트 엔진은 해당 컨텍스트의 코드를 실행하는데 필요한 환경 정보들을 수집해서 실행 컨텍스트에 저장한다. 다시 말해, 실행 컨텍스트란 코드가 실행되기 위해 필요한 정보들을 가진 범위를 추상화하기 위해 객체 형태로 나타낸 것을 말한다.

자바스크립트에서 실행 컨텍스트를 만들 수 있는 방법은 아래 케이스들이다.

  • 전역 코드 : 전역 영역에 존재하는 코드
  • Eval 코드 : eval 함수로 실행되는 코드
  • 함수 코드 : 함수 내에 존재하는 코드
  • (ES6부터는) 블록문

실행 컨텍스트는 논리적 스택 구조를 가진다. 실행되는 순서대로 콜 스택(call stack)에 쌓였다가, 가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 동일한 환경과 순서를 보장한다.

설명이 더 복잡할 수 있다. 책에서 나온 사례를 보자.

var x = 'xxx';

function foo () {
  var y = 'yyy';

  function bar () {
    var z = 'zzz';
    console.log(x + y + z);
  }
  
  bar();
}

foo();

위의 코드를 실행하면, 아래 그림과 같이 실행 컨텍스트가 쌓이고 소멸한다.

  1. 처음 자바스크립트 코드를 실행하는 순간 전역 컨텍스트가 콜 스택에 담긴다. 최상단의 공간은 코드 내부에서 별도의 실행 명령이 없어도 브라우저에서 자동으로 실행하므로 자바스크립트가 실행되는 순간 전역 컨텍스트는 활성화된다고 볼 수 있다. 전역 컨텍스트는 애플리케이션이 종료될 때(웹 페이지에서 나가거나 브라우저를 닫을 때)까지 유지된다.

  2. foo() 함수가 호출되면, 자바스크립트는 foo() 함수에 대한 환경 정보를 수집해서 새로운 실행 컨텍스트를 생성한 후, 전역 컨텍스트 위에 쌓는다.

  3. foo() 함수가 실행되다가 내부 함수 bar()를 만나면, 자바스크립트는 bar() 함수의 실행 컨텍스트를 생성한다. 이 실행 컨텍스트는 스택의 최상단에 쌓인다.

  4. 최상단에 쌓인 bar() 함수가 실행을 종료하면 bar() 함수에 의해 만들어진 실행 컨텍스트는 콜 스택에서 제거된다.

  5. foo() 함수가 실행을 종료하면 foo() 함수에 의해 만들어진 실행 컨텍스트는 콜 스택에서 제거된다.

여기까지가 우리가 알고 있는 실행 컨텍스트의 동작이다. 스택 구조를 생각해보면, 한 실행 컨텍스트가 콜 스택의 맨 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점임을 알 수 있다.

이제 실행 컨텍스트의 내부에 대해 알아보자.

2. 실행 컨텍스트의 구성

실행 컨텍스트 객체에는 아래 정보들이 담긴다.

  • VariableEnvironment : 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보. 선언 시점의 LexicalEnvironment의 스냅샷으로, 변경사항은 반영되지 않음
    ◦ environmentRecord
    ◦ outer-EnvironmentReference

  • LexicalEnvironment : 처음에는 VariableEnvironment와 같지만 변경사항이 실시간으로 반영됨
    ◦ environmentRecord
    ◦ outer-EnvironmentReference

  • ThisBinding : this 식별자가 바라보고 있는 대상 객체

VariableEnvironment(V.E)와 LexicalEnvironment(L.E)는 실행 컨텍스트에서 변수의 참조들을 기억하는 환경이다. 차이점이라면 V.E에서는 컨텍스트 최초 실행 시점의 스냅샷을 유지하는 반면, L.E에는 변경사항이 반영된다는 것이다. 일반적으로 함수의 LexicalEnvironment는 해당 함수가 가지는 자신의 로컬 스코프 범위를 말한다.

이 두 환경의 내부는 다시 EnvironmentRecordOuterEnvironmentReference 로 구성되어 있다.

  • EnvironmentRecord : 컨텍스트와 관련된 코드의 식별자 정보들이 저장됨
  • OuterEnvironmentReference : 호출된 함수가 선언될 당시의 Lexical Environment를 참조하는 포인터로, 스코프 체인을 가능하게 함

아래에서 더 자세히 보자.

3. EnvironmentRecord의 구성

EnvironmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다.

var a = 3; // 여기에서 식별자는 a를 말한다.

컨텍스트 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 식별자들을 수집한다. 여기에서 수집되는 식별자들은 매개변수 식별자, 선언된 함수, var로 선언된 변수의 식별자 등이 해당한다.

function a (x) {
  console.log(x);
  var a = 1;
  var b = 2;
  function foo() {
    console.log('work');
  };
}

a(1);

위의 코드에서, 실행 컨텍스트의 EnvironmentRecord에는 식별자 x, a, b와 함수 foo가 수집된다. EnvironmentRecord는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지만을 먼저 수집하기 때문에, 변수를 인식할 때 식별자만 끌어올리고 할당 과정은 원래 자리에 순서대로 남겨둔다.

"코드가 실행되기 전임에도 불구하고 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명들을 모두 알 수 있다" => 호이스팅(hoisting)의 개념이다.

function a (x) {
  var x; // 매개변수를 변수 선언/할당과 같다고 가정하고 변환하면...
  var a;
  var b;
  function foo() {
    console.log('work');
  };
  
  x = 1;
  console.log(x);
  a = 1;
  b = 2;
}

a(1);

위 코드처럼 식별자가 먼저 수집되고, 그 뒤에 순서대로 코드를 실행한다는 예시는 호이스팅을 설명할 때 자주 볼 수 있는 사례다.
(호이스팅에 관해 정리하고 싶은 주제도 있는데, 그건 다음에 별도로 써야지)

이렇게 식별자 정보를 수집하는 EnvironmentRecord는 구성 요소에 따라 다시 나눠질 수 있다.

1) Declarative Environment Record

  • 함수 선언, 변수 선언, catch절에서 사용되는 식별자 정보를 담고 있다.
  • 스코프 내에서 선언된 식별자들의 바인딩을 관리한다.
  • Environment Record를 상속한 서브클래스이다.
  • Function Environment Record
    • 함수의 최상위 스코프를 나타내는데 사용되는 선언적 환경 레코드이다.
    • 화살표 함수가 아니라면 this 바인딩을 제공한다.
    • 화살표 함수가 아니고 super를 참조하는 경우 super 메서드를 실행하는데 필요한 state를 제공한다.
  • Module Environment Record
    • Module의 외부 스코프를 나타낼 때 사용하는 선언적 환경 레코드이다.

2) Object Environment Record

  • with문과 같이 식별자를 어떤 특정 객체 A의 속성으로 취급할 때 사용한다.
  • 이 경우 A를 binding object라는 속성으로 정의한다.
  • Environment Record를 상속한 서브클래스이다.

3) Global Environment Record

  • 전역 컨텍스트의 경우, object Environment Recordbinding object는 전역 객체(window)를 가리킨다.
  • Object, Array, Function, parseInt 같은 모든 built-in global과, 전역 코드에서의 함수 선언, 제너레이터 선언, 변수 선언에 의해 생성된 모든 식별자 정보는 binding object, 즉 전역 객체에서 찾을 수 있다.
  • 다시 말해, 선언된 식별자들의 정보를 담고 있는 일반적인 Declarative Environment Record의 역할을 전역 컨텍스트에서는 object Environment Record가 담당한다.
  • Global Environment Recorddeclarative Environment Recordobject Environment Record에 포함되지 않은 식별자 정보만 보유한다.


4. OuterEnvironmentReference (혹은 outer)

OuterEnvironmentReference를 이해하기 위해서는 스코프(scope) 개념을 먼저 이해해야 한다. 스코프란 식별자에 대한 유효 범위를 말한다. 자바스크립트에는 전역 스코프(global scope)와 지역 스코프(local scope)가 있다.

var a = 1;
function scope() {  // 함수 스코프
  var b = 2;
  console.log(a);  // 1
  console.log(b);  // 2
}
console.log(a);  // 1
console.log(b);  // b is not defined

scope 함수 외부에서 선언한 변수 a는 함수 scope 안에서도 접근이 가능하다. 그러나 함수 안에서 선언한 변수 b는 오직 함수 안에서만 접근할 수 있다. 변수 a는 전역 스코프에, 변수 b는 지역 스코프에 있기 때문이다. 자바스크립트는 변수의 유효 범위를 검색할 때 안에서부터 바깥으로 찾아나가는데, 이것을 스코프 체인(scope chain)이라고 한다.
따라서 전역 스코프에 선언된 변수들은 어느 곳에서도 접근이 가능하다. 반면 지역 스코프는 선언된 함수의 내부에서만 접근이 가능하다.

OuterEnvironmentReference는 현재 호출된 함수가 선언되는 시점에서의 Lexical Environment를 참조하는 포인터이다. OuterEnvironmentReference는 연결리스트 형태를 띄며 ‘선언 시점의 LexicalEnvironment’를 계속 찾아 올라간다. '선언 시점의 LexicalEnvironment'라는건 결국 해당 함수가 속한 상위 스코프의 범위이다.
각 함수의 OuterEnvironmentReference는 오직 자신이 선언된 시점의 LexicalEnvironment만 참조하고 있기 때문에 가장 가까운 요소부터 위로 차례대로만 접근할 수 있다.

var outerWrap = function() {
  var a = 1;
  
  var outer = function() {
    var b = 2;
    
    var inner = function() {
      console.log(a, b); // inner의 LexicalEnvironment에는 a와 b가 없지만 스코프 체인을 통해 값에 접근한다.
      console.dir(inner);
    }
    
    inner();
    console.log(a); // outer의 LexicalEnvironment에는 a가 없지만 스코프 체인을 통해 값에 접근한다.
  }
  
  outer();
}

outerWrap();

중첩 구조가 많지만 각 함수들이 자신이 선언된 시점의 상위 LexicalEnvironment를 참조하고 있다는 것만 알면 어렵지 않다. 위의 코드에서 console.dir(inner)inner 함수의 계층 정보를 열어보면 [[scopes]] 프로퍼티에서 상위 스코프 정보를 확인할 수 있다.

0번째 스코프에는 함수 outer의 LexicalEnvironment에서 선언된 변수 b와 함수 inner가 있다. 한 단계 올라간 1번째 스코프에는 함수 outerWrap의 LexicalEnvironment에서 선언된 변수 a가 있다. 그 다음 단계로 올라가면 전역 컨텍스트가 노출된다. 이렇게 체인을 통해서 상위 스코프로 접근할 수 있음을 알 수 있다.

참고로, 전역 컨텍스트에서의 OuterEnvironmentReference는 null이다.

5. ThisBinding

아까 위에서 실행 컨텍스트 객체에는 VariableEnvironment, LexicalEnvironment, 그리고 ThisBinding 정보가 담겨 있다고 했다. 이 중 ThisBinding에 대해서는 추후 다른 글에서 다시 심도 있게 파볼 예정이다. 왜냐하면 자바스크립트에서 this는 너무 방대하니깐요

6. 정리

자바스크립트에서 실행 컨텍스트란, 코드를 실행하기 위해 필요한 정보들을 가진 범위를 객체 형태로 나타낸 것이다. 실행 컨텍스트를 구성하는 LexicalEnvironment는 현재의 실행 컨텍스트가 실행되기 위한 여러 정보를 담고 있다. LexicalEnvironment는 식별자들에 대한 정보를 담은 EnvironmentRecord와, 상위 LexicalEnvironment를 참조해 스코프 체인을 가능하게 하는 OuterEnvironmentReference 정보로 구성되어 있다. EnvironmentRecord는 식별자 바인딩을 관리하고, binding object라 불리는 특정 객체의 속성으로 선언된 식별자들을 관리한다. OuterEnvironmentReference는 현재의 실행 컨텍스트를 구성한 함수가 선언되는 시점에서의 상위 LexicalEnvironment를 참조하기 때문에 식별자의 유효 범위를 상위로 거슬러 올라가 찾게 되는 스코프 체인이 가능하다.



참고 :
[Javascript] Execution Context와 Lexical Environment
(https://iamsjy17.github.io/javascript/2019/06/10/js33_execution_context.html)
자바스크립트 함수(3) - Lexical Environment
(https://meetup.toast.com/posts/129)
JavaScript 식별자 찾기 대모험
(https://homoefficio.github.io/2016/01/16/JavaScript-%EC%8B%9D%EB%B3%84%EC%9E%90-%EC%B0%BE%EA%B8%B0-%EB%8C%80%EB%AA%A8%ED%97%98/)
[javascript] execution context, scope (실행 컨텍스트, 스코프)
(https://phrygia.github.io/2020-12-23/scope)

profile
파닥파닥 FE 개발자의 기록용 블로그

0개의 댓글