kimjeongwonnabout

자바스크립트의 핵심 '실행 컨텍스트'

자바스크립트로 개발한다면 모르면 안되는 필수 개념 '실행 컨텍스트'

자바스크립트를 공부하면서 개인적으로 자바스크립트에서 가장 중요한 개념을 꼽자면 실행 컨텍스트를 꼽는다. 자바스크립트의 핵심 개념들을 나열 해 보라고 한다면 보통 프로토타입, this 바인딩, 호이스팅, 렉시컬 스코프, 클로저, 이벤트 루프등을 떠올릴 수 있고 그 중에서도 앞의 개념들을 관통하는 가장 핵심적인 개념이 실행 컨텍스트이다.

실행 환경

실행 컨텍스트(이하 EC; Execution Context)는 실행 컨텍스트 스택에서 구성된다. 그리고 모든 코드는 EC 안에서 작동한다. 코드가 평가되기 위해서 자바스크립트 엔진은 EC를 생성한 뒤 코드를 평가하는데 EC가 생성되는 상황은 크게 세가지가 있다.

  1. 전역 코드의 실행 → 전역 EC 생성
  2. 함수의 실행 → 함수 EC 생성
  3. eval코드의 실행 → eval EC 생성

실행 컨텍스트 스택과 콜 스택은 같은 의미이다.

EC가 생성되면 생성 단계와 실행 단계를 거쳐 코드를 실행하게 된다. 생성 단계에서는 코드를 전체적으로 평가하고 렉시컬 환경(Lexical Environment)을 생성한다. 그리고 그 렉시컬 환경 내에서는 스코프 체인을 위한 외부 렉시컬 환경 참조 바인딩(Outer Lexical Environment Reference), 식별자 탐색을 위한 객체 환경 레코드(Object Environment Recode)가 연결되고, 전역EC의 경우에는 let과 const로 선언된 변수/상수를 저장하는 선언 환경 레코드(Declarative Environment Record)가 환경 레코드에 연결된다. 마지막으로 해당 렉시컬 환경의 this를 결정하는 this 참조 바인딩이 처리된다.

추가로 ES6이후부터는 블록 스코프별로 렉시컬 환경이 새로 생성되고 현재 실행중인 EC의 렉시컬 환경을 교체하는 식으로 스코프를 처리한다. 이 때 외부 렉시컬 환경 참조를 블럭문이 정의되었던 렉시컬 환경으로 바인딩 하여, 블록 스코프 내에서 스코프 체인이 일어날 수 있도록 해준다. 이렇게 렉시컬 환경과 변수 환경이 생성하면서 식별자에 대한 호이스팅이 일어나게 되고 var변수에는 undefined를 let과 const에는 <uninitialized>로 초기화하여 미리 식별자를 할당하게 된다. 이후에 코드가 실행되면서 실제 변수 할당이 일어난다.

var로 선언된 변수를 저장하기 위한 변수 환경(Variable Environment)도 함께 EC에 연결된다. 변수 환경은 EC 별로 생성되기 때문에 EC내에서 렉시컬 환경이 변경되어도(블록문 실행 등) var변수에 접근할 수 있게 해준다.

전역 EC

브라우저의 렌더링 단계에서 <script>태그를 만나면 자바스크립트 엔진에서 EC 스택에 전역 EC를 생성한다. 전역 EC에는 객체 환경 레코드를 통해 globalThis객체에 참조할 수 있게끔 연결이 되며 globalThis또한 별도로 바인딩된다. 일반적으로 전역에서 this를 참조하게 되면 globalThis에 연결되지만 strict mode에서는 undefined로 연결이 된다.

전역 EC의 전역 렉시컬 환경은 별도의 선언 환경 레코드를 갖고 있는데, 이곳에는 let과 const로 선언 된 변수/상수를 별도로 관리한다. var 변수의 경우 globalThis의 프로퍼티로 등록 된 후 변수 환경에서 참조되는 방식으로 식별자를 갖는다면 let과 const는 별도의 공간에 저장되어 globalThis의 프로퍼티와 충돌을 방지한다.

이 후 코드 실행 단계에서 함수 객체가 생성되게 된다면 현재 실행 컨텍스트에 바인딩 된 렉시컬 환경을 함수 객체의 내부슬롯 [[Environment]]에 바인딩하여 이후 함수가 호출 되었을 때의 외부 렉시컬 환경 참조를 결정할 수 있도록 한다.

블록 렉시컬 환경

ES6의 let과 const는 블록레벨 스코프를 갖는다. 그렇기 때문에 블록문이 실행 될때 EC는 그대로 유지 된 채로 렉시컬 환경의 바인딩이 교체되는 방법으로 새로운 스코프를 갖게 된다. 이 때 블록 렉시컬 환경이 생성되는데 블록문 내에서의 식별자를 저장하고 외부 렉시컬 환경 참조는 원래 바인딩 되어있었던 렉시컬 환경을 가르켜 스코프 체인을 생성한다.

함수 EC

자바스크립트 엔진에서 함수가 호출되면 EC 스택에 함수 EC를 생성한다. 이 때의 EC 스택에는 전역 EC가 있을 수도 있고, 이벤트 루프를 통해 호출된다면 비어있을 수도 있다. 함수 EC가 생성되면 EC 생성 단계에 진입하여 함수 렉시컬 환경을 만들고 이 때 함수가 호출된 방법에 따라 this가 결정되고 외부 렉시컬 환경 참조에 호출된 함수 객체의 [[Environment]] 내부 슬롯에 연결된 렉시컬 환경을 바인딩하여 렉시컬 스코프를 생성한다.

이후 함수 환경 레코드를 생성하여 매개변수와 arguments변수를 초기화하고 함수 내부에서 선언된 식별자들을 초기화 한다. (호이스팅) 이 때 var와 let, const를 별도로 분리하지 않고 함수 환경 레코드 내에서 모두 초기화 한다. 다음 코드 실행 단계에서 초기화 된 식별자에 할당이 일어난다. 만약 이 때 함수 내부에서 함수가 선언된다면 해당 함수객체의 내부슬롯 [[Environment]]함수가 선언된 함수의 렉시컬 환경이 바인딩 되고 만약 선언된 함수가 반환되거나 외부에 할당되어 해당 함수 EC가 삭제된 이후에도 남아있다면 연결된 렉시컬 환경은 [[Environment]]의 참조로 남아 클로저를 생성한다.

eval EC의 경우도 비슷하게 작동하며 eval코드는 자바스크립트의 대표적인 안티패턴이므로 거의 사용되지 않기 때문에 생략한다.


개인적으로는 자바스크립트의 작동 원리는 실행 컨텍스트에 대한 치밀한 이해만 있다면 반 이상은 이해할 수 있다고 생각한다. 때문에 자바스크립트를 제대로 공부한다면 반드시 적확하게 이해할 필요가 있는 부분이기 때문에 한 번 더 정리를 했다. 이미 알고 있는 내용임에도 용어나 원리가 헷갈리기도 했다. 이 매커니즘을 머릿속에서 언제든지 그릴 수 있을 때 까지 계속 공부해야 할 필요가 있다고 생각한다.