JS 실행 컨텍스트(Execution-Context)

불꽃남자·2020년 9월 17일
1

고대하던 실행 컨텍스트에 대해 알아보는 시간이다.

실행 컨텍스트

실행 컨텍스트란 실행 가능한 코드가 평가되고 실행되는 환경을 제공하며 실행 결과값을 관리하는 부분이다.
실제 코드로는 확인할 수 없는 추상적인 개념이라 사람들마다 설명이 조금씩 다르고 두루뭉실하지만 내가 이해한 바를 최대한 설명하자면 이렇다.

JS에서 실행 가능한 코드는 크게 세 가지가 있다.

  1. 전역 코드
  2. 함수 코드
  3. eval 코드

위에 열거된 실행 가능한 코드들은 각각 다른 실행 컨텍스트 구조를 가진다.

eval 함수는 인자로 받은 문자열을 표현식으로 바꾸어주는 함수인데, eval함수가 가지는 취약점때문에 사용하지 않을 것을 권장하고 있으며, 굳이 사용할 이유도 없는 함수이다.
그래서 eval코드의 실행 컨텍스트에 대한 자료는 없다시피 했다. eval코드는 eval코드만의 실행 컨텍스트 구조를 갖는다는 정도만 알면 될 것이다.

또한 모든 실행 컨텍스트들은 코드가 실행되기 이전에 코드를 평가한다.

이제 각각의 실행 컨텍스트가 생성되어 코드를 평가하는 과정과 코드의 실행에 따른 실행 컨텍스트의 변화에 대해 알아보자.

전역 코드의 실행 컨텍스트(전역 실행 컨텍스트)

전역 객체의 생성

전역 실행 컨텍스트가 생성되어지기도 이전에, 전역 객체가 생성된다. 전역 객체는 브라우저 환경이라면 window, Node.js 환경이라면 global이 된다. 이 글에서는 브라우저 환경을 기준으로 삼겠다.

전역 실행 컨텍스트 생성

전역 객체가 생성되고 나면 전역 실행 컨텍스트가 생성하고 실행 스택에 push한다.
이후 렉시컬 환경 컴포넌트와 변수 환경 컴포넌트를 생성한다.

전역 컨텍스트의 구성 요소는 EcmaScript의 버전에 따라 다른데, 나는 가장 최신 버전인 ES6을 기준으로 알아 보았다.

전역 컨텍스트는 렉시컬 환경 컴포넌트(LexicalEnvironment), 변수 환경 컴포넌트(VariableEnvironment)로 구성된다.
이 둘의 차이점은 솔직히 모르겠다. 이 둘의 내부 구성이나 내부 값, 타입은 같다. 그럼에도 무언가 차이점이 있으니 나뉘어져 있을 것이다. 그러나 지금의 나로써는 알 수가 없다.

그래서 렉시컬 환경 컴포넌트에 대해서만 설명하겠다.

렉시컬 환경 컴포넌트는 환경 레코드(EnvironmentRecord), 외부 렉시컬 환경 참조(OuterLexicalEnvironmentReference), This Binding으로 구성된다.

전역 실행 컨텍스트의 환경 레코드는 객체 환경 레코드(Object Environment Record), 선언적 환경 레코드(Declarative Environment Record)로 구성된다.

이상이 전역 실행 컨텍스트의 전체적인 구성이다.

전역 실행 컨텍스트의 환경 레코드(전역 환경 레코드)

전역 환경 레코드는 객체 환경 레코드와 선언적 환경 레코드로 나누어져 있다.

사실 이 부분이 실행 컨텍스트를 알아 보며 가장 헤매게 된 부분이다.
웹 사이트마다 설명이 다 달랐다. 그리고 가장 공통된 설명은 내가 이해할 수가 없었다. 내가 아직 부족하다는 뜻이다.
그래서 내 코드 실험에 가장 근접한 설명을 하는 사이트를 참고하기로 했다.

객체 환경 레코드

var 식별자로 선언된 전역 변수, 함수 선언문으로 정의한 전역 함수는 객체 환경 레코드에 등록된다.
객체 환경 레코드는 bindingObject와 연결 되어 있고, 객체 환경 레코드에 등록된 식별자들은 bindingObject의 프로퍼티가 된다. 전역 객체 환경 레코드의 bindingObject는 전역 객체(window)이다. 그래서 var 식별자로 선언된 변수 및 함수, 객체는 window의 프로퍼티로써 접근 가능한 것이다.

호이스팅의 정체

var 식별자로 선언된 전역 변수는 객체 환경 레코드에 등록 된 뒤 bindingObject의 프로퍼티가 되고, 즉시 undefined로 값이 초기화된다. 이것이 변수 호이스팅의 정체이다.
함수선언식으로 정의된 전역 함수는 해당 함수명과 동일한 이름의 식별자를 객체 환경 레코드에 등록하고, 해당 함수를 값으로서 즉시 할당한다. 이것이 함수 호이스팅의 정체이다.

선언적 환경 레코드

let과 const 식별자로 선언된 전역 변수는 선언적 환경 레코드에 key(변수명)와 value(변수 값)으로써 등록되어 관리된다.
이들은 평가단계에서 선언과 초기화가 동시에 진행되는 var 전역 변수와 달리 선언만 된다. 그 후 전역 컨텍스트의 실행 단계에서 선언문을 만나게 되면 값이 초기화된다. 그래서 이들은 자신의 선언문이 실행되기 전까지 TDZ(Temporal Dead Zone)상태에 있고, TDZ 상태의 변수에 접근하려고 하면 참조 오류를 일으킨다.

let a의 선언문이 실행되기 이전에 접근하려하자 참조 오류를 일으키는 모습

또한 이들은 전역 객체의 프로퍼티로 등록되지 않고 선언적 환경 레코드에 등록되어 관리된다.

let a의 선언문 이후 전역 객체의 프로퍼티에 등록되어있지 않은 모습

전역 컨텍스트의 외부 환경 참조(Outer Lexical Envronment Reference)

외부 환경 참조는 말 그대로 외부의 렉시컬 환경 컴포넌트의 참조이다.
전역 컨텍스트는 코드의 가장 외부에 존재하므로 전역 컨텍스트의 외부 환경 참조의 값은 null이다. 그래서 전역 객체 위로 scope가 존재하지 않는 것이고 scope chain의 종점은 항상 전역 객체가 되는 것이다.

전역 컨텍스트의 This Binding

컨텍스트의 This Binding이 가지는 값은 곧 해당 컨텍스트에서 this가 가리키는 값이다.
전역 컨텍스트의 This Binding은 전역 객체이므로, 전역 컨텍스트의 this가 전역 객체를 가리키는 것이다.

전역 코드 평가 종료

이상이 JS 코드가 실행되기 전 일어나는 일들이다. 크게 정리해보자면 이 때 일어나는 일들은

  1. 전역 변수 등록 및 (var 변수라면)초기화
  2. 전역 함수 등록 및 (함수선언식이라면)함수 할당
  3. Scope Chain 생성
  4. this값 바인딩

이 된다.

전역 코드 실행

평가를 끝낸 뒤 전역 코드는 위에서부터 차례대로 실행된다.
이 단계에서 할당문을 만나 실행되면 해당 할당문의 식별자를 해당 컨텍스트의 환경 레코드에서 검색한다. 식별자에 따라 객체 환경 레코드 혹은 선언적 환경 레코드에서 변수명을 검색하고 해당 변수명에 값을 할당한다.

함수 코드의 실행 컨텍스트

함수 실행 컨텍스트 생성

전역 코드가 실행되다가 함수가 호출되면 함수 실행 컨텍스트가 생성되고, 실행 스택에 push된다.

함수 실행 컨텍스트도 전역 실행 컨텍스트의 구조와 거의 동일하다. 다른 점은 함수 실행 컨텍스트의 환경 레코드는 단일으로 존재한다.

함수 실행 컨텍스트의 환경 레코드(함수 환경 레코드)

함수의 매개변수 객체, 함수 내부에서 선언된 변수, 함수등은 모두 함수 환경 레코드에 등록되어지고 관리되어진다.

함수 실행 컨텍스트의 외부 환경 참조

함수가 평가되어서 함수 객체가 만들어 질 때에, 현재 실행 컨텍스트의 렉시컬 환경을 [[Environment]]라는 함수 내부 속성에 저장한다.

함수 실행 컨텍스트의 외부 환경 참조는 곧 해당 함수의 [[Environment]]에 저장된 렉시컬 환경과 같다.

함수 실행 컨텍스트의 This Binding

this는 그 호출 방식에 따라 가리키는 값이 달라진다.
this를 참조하게 되면 추상 연산인 ResolveThisBinding 메서드가 실행되고, this의 값이 결정된다... 라고 한다.

이전에 JS this에 대해 알아보는 시간을 가졌지만, 요약하자면 이렇다.

  1. 일반 함수로 호출 된 함수 내부의 this는 전역 객체를 가리킨다.
  2. 생성자 함수로 호출 된 함수 내부의 this는 생성할 인스턴스를 가리킨다.
  3. 메서드로 호출 된 함수 내부의 this는 메서드를 호출한 객체를 가리킨다.
  4. 위의 어떤 함수든 함수의 내부 함수의 this는 전역 객체를 가리킨다.

화살표 함수에 대해서 아직 제대로 알아보진 않았지만 화살표 함수의 This Binding을 알아보자.
화살표 함수는 화살표 함수 자신을 감싸고 있는 렉시컬 환경의 This Binding을 자신의 This Binding으로 삼는다.

함수 코드 실행

평가가 모두 종료되면 함수 코드가 실행된다. 코드 실행 중 식별자를 만나면 해당 렉시컬 환경 컴포넌트의 환경 레코드에서 해당 식별자를 탐색하고, 없다면 해당 렉시컬 환경 컴포넌트의 외부 환경 참조의 환경 레코드에서 탐색하고, 없다면 해당 렉시컬 환경 컴포넌트의.... 이것을 반복하다 해당 렉시컬 환경 컴포넌트의 외부 환경 참조가 null이 될 때까지 해당 식별자를 찾지 못 하면 참조 오류를 반환한다.
이것이 스코프 체인의 실체이다.

Block Scope와 실행 컨텍스트

아래의 코드를 보자.

{
     let b = 3;
}
console.log(b); //ReferenceError


let 선언문은 Block-level scope를 갖는다. 자신이 속한 블록 내부에서만 유효하다는 뜻이다.
이것이 의미하는 바는 저 블록이 렉시컬 환경을 갖고 있다는 의미이다.

블록은 자신의 실행 컨텍스트를 생성하진 않지만, 자신의 렉시컬 환경은 생성한다.
코드가 실행되다 블록 문을 만나면 블록 문을 평가한다. 그럼 해당 블록의 렉시컬 환경이 생성된다. 블록 렉시컬 환경은 외부 환경 참조로 해당 실행 컨텍스트를 가진다. 블록 렉시컬 환경은 환경 레코드 컴포넌트를 선언적 환경 레코드로 갖게 된다.


내가 이해한 블록 렉시컬 환경의 구성도. 이게 정확히 맞는지 확신할 수는 없다.

이렇게 블록 렉시컬 환경이 생성되면, 현재 실행중인 실행 컨텍스트의 렉시컬 환경을 oldEnv라는 변수에 저장해두고, 그 자리를 블록 렉시컬 환경으로 대체한다.

그리고 블록 문을 실행한다. 블록 문의 실행이 끝나면 oldEnv에 저장해두었던 현재 실행 컨텍스트의 렉시컬 환경을 제자리에 돌려놓는다.

block문의 var

아래 코드를 보자.

{
    var c = 3;
}
console.log(c);//3


위의 코드를 보면 블록 문 내부에서 선언한 let 변수나 const 변수는 블록 문 외부에서 접근 할 수 없었다.
하지만 블록 문 내부에서 선언한 var 변수는 블록 문 외부에서 접근이 가능하다.
이는 블록 렉시컬 환경의 환경 레코드는 선언적 환경 레코드만이 존재하기 때문이다.
블록 문 내부에서 선언된 let, const 변수는 블록 렉시컬 환경의 선언적 환경 레코드에 등록된다.
하지만 블록 문 내부에서 선언된 var 변수는 외부 환경 참조의 객체 환경 레코드에 저장된다. 만일 외부 환경 참조에도 객체 환경 레코드가 존재하지 않는다면 한 단계 위의 외부 환경 참조의 객체 환경 레코드에 저장 된다.

{
    {
        var d = 4;
    }
}
console.log(d);//4
console.log(window.d);//4


사실 렉시컬 환경이나 실행 컨텍스트라는 것이 눈으로 확인 할 수 없어서 나는 확신할 수가 없다.
하지만 코드의 실행결과를 보면 그렇다고 밖에 생각할 수 없다.

마치며

JS의 실행 컨텍스트는 온통 추상적이다. 코드로 그 구조를 직접 확인할 수가 없다. 그래서 그런지 웹 사이트를 돌아다니며 실행 컨텍스트에 대해 알아볼 때에도 난항을 겪었다. 이 사이트의 설명과 저 사이트의 설명이 다른 경우도 있었고, 다른 사이트의 내용이 그대로 복사되어져 있기도 했다.

그러다가 내가 실행한 코드의 결과와 일치하는 설명을 하는 사이트를 발견했고, 그 사이트는 실행 컨텍스트에 대해 이제껏 본 것 중 가장 잘 설명하고 있었다. 그래서 그 사이트를 참고해서 실행 컨텍스트에 대해 알아보는 시간을 가졌다.
해당 사이트에 댓글을 다는 기능이 없어 작성자님에게 여기에서 깊이 감사드린다.

참고한 사이트

수빈 님의 개발 블로그

profile
프론트엔드 꿈나무, 탐구자.

0개의 댓글