-
[JavaScript] 2. 실행 컨텍스트개발자 이야기/JavaScript 2021. 7. 10. 16:04
실행 컨텍스트란?
실행 컨텍스트란 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로, 자바스크립트 엔진이 사용할 목적으로 생성한다.
자바스크립트는 어떤 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 호이스팅하고, 외부 환경 정보를 구성하고, this 값을 설정한다.
하나의 실행 컨텍스트를 구성할 수 있는 방법 : 전역공간, eval() 함수, 함수 등(ES6에서는 블록{}에 의해서도 실행 컨텍스트가 생성된다.)
자동으로 생성되는 전역공간과 eval 함수를 제외하면 흔히 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것뿐이다.
실행 컨텍스트의 코드 실행 과정
// (1) var a = 1; function outer() { function inner() { console.log(a); // undefined var a = 3; } inner(); // (2) console.log(a); // 1 } outer(); // (3) console.log(a); // 1
- 자바스크립트 코드를 실행하는 순간 전역 컨텍스트가 콜 스택에 담긴다.
- 전역 컨텍스트는 코드 내부에서 별도의 실행 명령이 없어도 브라우저에서 자동으로 실행되기 때문에, 자바스크립트 파일이 열리는 순간 전역 컨텍스트가 활성화된다고 이해하면 된다. - (3)에서 outer() 함수를 호출하면 자바스크립트 엔진은 outer에 대한 환경 정보를 수집해서 outer 실행 컨텍스트를 생성한 후 콜 스택에 담는다. 콜 스택 맨 위에 outer 실행 컨텍스트가 놓이면 전역 컨텍스트와 관련된 코드 실행을 일시중단하고, 대신 outer 실행 컨텍스트과 관련된 코드, 즉 outer 함수 내부의 코드들을 순차로 실행한다.
- (2)에서 inner() 함수를 호출하면 inner() 함수의 실행 컨텍스트가 콜 스택의 가장 맨 위에 놓이게 되고, 즉시 outer 실행 컨텍스트와 관련된 코드 실행을 중단한다. 그리고 inner 함수 내부의 코드를 순차로 실행한다.
- inner 함수의 실행이 종료되면 inner 실행 컨텍스트가 콜 스택에서 제거된다. 그리고 중단했던 outer 실행 컨텍스트를 (2) 다음 줄부터 이어 실행한다.
- outer 함수 실행이 종료되면 outer 실행 컨텍스트가 콜 스택에서 제거된다. 그런 다음, 중단했던 (3) 다음 줄부터 이어 실행한다.
- 전역 공간에 더은 실행할 코드가 남아 있지 않게 되면 전역 컨텍스트도 콜 스택에서 제거되고, 콜 스택에는 아무것도 남지 않은 상태로 종료된다.
중요 내용
콜 스택의 맨 위에 실행 컨텍스트가 놓이게 되는 순간 곧바로 해당 실행 컨텍스트가 실행된다.
그리고 실행 컨텍스트가 활성화되면 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는 데 필요한 환경 정보들을 수집해서 실행 컨텍스트에 저장한다.
실행 컨텍스트에 저장되는 정보들
- VariableEnvironment : 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보. 선언 시점의 LexicalEnvironment의 스냅샷으로, 변경 사항은 반영되지 않는다.
- LexicalEnvironment : 처음에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영된다.
- ThisBinding : this 식별자가 바라봐야 할 대상 객체
VariableEnvironment와 LexicalEnvironment의 내부는 environmentRecord와 outerEnvironmentReference로 구성된다.
ThisBinding
실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장된다. 실행 컨텍스트 활성화 당시에 this가 지정되지 않은 경우 this에는 전역 객체가 저장된다. 그밖에는 함수를 호출하는 방법에 따라 this에 저장되는 대상이 다르다. this에 대해서는 다음 3.this에서 자세히 다룬다.
VariableEnvironment
VariableEnvironment에 담기는 내용은 LexicalEnvironment와 같다. 그러나 최초 실행 시의 스냅샷을 유지한다는 점이 LexicalEnvironment와 다르다.
실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LexicalEnvironment를 만든다. 이후 LexicalEnvironment를 주로 활용하게 된다.
LexicalEnvironment
LexicalEnvironment은 컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아놓은 것이다.
ex. 현재 컨텍스트 내부는 a, b와 같은 식별자들이 있고 그 외부 정보는 c를 참조하도록 구성되어 있다.
environmentRecord와 호이스팅
environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들(매개 변수 식별자, 함수 자체, 변수 식별자 등)이 저장된다.
컨텍스트 내부 전체를 처음부터 끝까지 순서대로 훑어나가며 수집(호이스팅)한다. 식별자 정보 수집은 실행 컨텍스트가 관여할 코드를 실행하기 전에 모두 마친다.
*전역 실행 컨텍스트는 변수 객체 생성 대신 자바스크립트 구동 환경이 별도로 제공하는 전역 객체를 활용한다. 전역 객체에는 브라우저의 window, Node.js의 global 객체 등이 있다.
호이스팅(hoisting)이란 변수 정보를 수집하는 과정을 이해하기 쉬운 방법으로 대체한 가상의 개념으로, '끌어올리다'라는 의미다.
자바스크립트 엔진이 실제로 식별자들을 최상단으로 끌어올리지는 않지만 편의상 끌어올린 것으로 간주하기 위함으로 만들어진 개념이다.
아래 코드는 매개변수와 변수에 대한 호이스팅 예제다.
function a (x) { // 수집 대상 1(매개변수) console.log(x); var x; // 수집 대상 2(변수 선언) console.log(x); var x = 2; // 수집 대상 3(변수 선언) console.log(x); } a(1);
인자는 함수 내부의 다른 코드보다 먼저 선언 및 할당이 이루어진다.
그러므로 LexicalEnvironment 입장에서 위 코드는 아래 코드와 완전히 같다.
function a () { var x = 1; // 수집 대상 1(매개변수 선언) console.log(x); var x; // 수집 대상 2(변수 선언) console.log(x); var x = 2; // 수집 대상 3(변수 선언) console.log(x); } a();
environmentRecord는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있다. 즉, 각 식별자에 어떤 값이 할당될 것인지에는 관심이 없다. 따라서 아래 코드와 같이 변수 호이스팅 시 변수명만 끌어올리고 할당 과정은 끌어올리지 않는다.
function a () { var x; // 수집 대상 1(변수 선언) var x; // 수집 대상 2(변수 선언) var x; // 수집 대상 3(변수 선언) x = 1; // 수집 대상 1(변수 할당) console.log(x); console.log(x); x = 2; // 수집 대상 3(변수 할당) console.log(x); } a();
다음은 함수 선언의 호이스팅 예제다
function a () { console.log(b); var b = 'bbb'; // 수집 대상 1(변수 선언) console.log(b); function b () {} // 수집 대상 2(함수 선언) console.log(b); } a();
a 함수를 실행하는 순간 a 함수의 실행 컨텍스트가 생성된다. 이때 변수명과 함수 선언 정보를 위로 끌어올린다(수집한다).
변수는 선언부와 할당부를 나누어 선언부만 끌어올리는 반면, 함수 선언은 함수 전체를 끌어올린다. 그래서 아래와 같은 형태로 변환된다.
function a () { var b; // 수집 대상 1(변수 선언) function b () {} // 수집 대상 2(함수 선언) console.log(b); b = 'bbb'; // 수집 대상 1(변수 할당) console.log(b); console.log(b); } a();
해석의 편의를 위해 한 가지 더 바꿔보자면, 호이스팅이 끝난 상태에서의 함수 선언문은 함수명으로 선언한 변수에 함수를 할당한 것처럼 여길 수 있다.
function a () { var b; // 수집 대상 1(변수 선언) var b = function b () {} // 수집 대상 2(함수 선언) console.log(b); b = 'bbb'; // 수집 대상 1(변수 할당) console.log(b); console.log(b); } a();
스코프
스코프(scope)
- 식별자에 대한 유효범위
- 변수를 사용할 수 있는 유효범위
- 변수에 접근할 수 있는 범위
어떤 경계 외부에서 선언한 변수는 그 외부뿐만 아니라 경계 내부에서도 접근이 가능한다. 그러나 경계 내부에서 선언한 변수는 오직 내부에서만 접근할 수 있다.
ES6에서는 함수 뿐만 아니라 블록에 의해서도 스코프 경계가 발생한다. 다만 블록의 경우 var로 선언한 변수에 대해서는 적용하지 않고 오직 새로 생긴 let과 const, class, strict mode에서의 함수 선언 등에 대해서만 범위로서의 역할을 수행한다. 둘을 구분하기 위해 함수 스코프(var), 블록 스코프(let, const, ...)라는 용어를 사용한다.
스코프 체인 & outerEnvironmentReference
스코프 체인(scope chain)이란 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것을 말한다. 그리고 이를 가능하게 하는 것이 LexicalEnvironment의 두 번째 수집 자료인 outerEnvironmentReference이다.
outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다.
예를 들어, A 함수 내부에 B 함수를 선언하고 다시 B 함수 내부에 C 함수를 선언한 경우, 함수 C의 outerEnvironmentReference는 함수 B의 LexicalEnvironment를 참조한다.
선언 시점의 LexicalEnvironment를 계속 찾아 올라가면 마지막엔 전역 컨텍스트의 LexicalEnvironment가 있을 것이다.
이러한 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우, 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근이 가능하게 된다.
var a = 1; var outer = function () { var inner = function () { console.log(a); // undefined 출력 var a = 3; }; inner(); console.log(a); // 1 출력 }; outer(); console.log(a); // 1 출력
스코프 체인 상에 있는 변수라도 무조건 접근 가능한 것은 아니다.
위 코드를 살펴보면, 식별자 a는 전역 공간과 inner 함수 내부 두 곳에서 선언되었음을 알 수 있다. 이 상황에서는 inner 스코프의 LexicalEnvironment부터 검색하게 되는데, 실제로 inner 스코프의 LexicalEnvironment 안에 a 식별자가 존재하기 때문에 스코프 체인 검색을 더 진행하지 않고 즉시 inner LexicalEnvironment 상의 a를 반환하게 된다.
즉, inner 함수 내부에서 a 변수를 선언했기 때문에 전역 공간에서 선언한 동일 변수인 a 변수에는 접근할 수 없게 된다. 이를 변수 은닉화하고 한다.
하지만 inner 함수 내에서 undefined가 출력된 이유는 a 변수의 할당 이전에 console.log를 실행했기 때문이다. 위에서 언급했듯이, 매개변수와 변수의 경우 선언부만 호이스팅되기 때문이다.
전역변수 : 전역 공간에서 선언한 변수 / 지역변수 : 함수 내부에서 선언한 변수
위 예제에서의 전역변수(global variable)는 전역 스코프에서 선언한 a와 outer 2개입니다. 지역변수(local variable)는 outer 함수 내부에서 선언한 inner와 inner 함수 내부에서 선언한 a 2개입니다.
코드 안정성을 위해서는 가급적 전역변수 사용을 최소화해야 한다.
요약
실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체다.
전역 공간에서 자동으로 생성되는 전역 컨텍스트 + eval 및 함수 실행에 의한 컨텍스트 등이 있다.
실행 컨텍스트 객체는 활성화되는 시점에 VariableEnvironment, LexicalEnvironment, ThisBinding의 세 가지 정보를 수집한다.
실행 컨텍스트를 생성할 때는 VariableEnvironment와 LexicalEnvironment가 동일한 내용으로 구성되지만, LexicalEnvironment는 함수 실행 도중에 변경되는 사항이 즉시 반영된다. 그러나 VariableEnvironment는 초기 상태를 유지한다.
VariableEnvironment와 LexicalEnvironment는 매개변수명, 변수의 식별자, 선언한 함수의 함수명 등을 수집하는 environmentRecord와 바로 직전 컨텍스트의 LexicalEnvironment 정보를 참조하는 outerEnvironmentReference로 구성된다.
호이스팅은 코드 해석을 쉽게 설명하기 위해 environmentRecord의 수집 과정을 추상화한 개념으로, 실행 컨텍스트가 관여하는 코드 집단의 최상단으로 '끌어올린다'고 해석하는 것이다. 매개변수와 변수의 경우 선언부만을 호이스팅하고, 함수 선언문의 경우에는 함수 전체를 호이스팅한다.
스코프는 변수의 유효범위를 말한다.
outerEnvironmentReference는 해당 함수가 선언된 위치의 LexicalEnvironment를 참조한다. 코드 상에서 어떤 변수에 접근하려고 하면, 먼저 현재 컨텍스트의 LexicalEnvironment를 탐색하고 발견하게 되면 그 값을 반환한다. 만약 발견하지 못한다면 outerEnvironmentReference에 담긴 LexicalEnvironment를 탐색하는 과정을 거친다. 최종적으로 전역 컨텍스트의 LexicalEnvironment까지 탐색해도 해당 변수를 찾지 못한다면 undefined를 반환한다.
전역 컨텍스트의 LexicalEnvironment에 담긴 변수를 전역변수라 한다. 그 밖의 함수에 의해 생성된 실행 컨텍스트의 변수들은 모두 지역변수라 한다. 코드 안정성을 위해서는 가급적 전역변수 사용을 최소화하는 것이 바람직하다.
참고자료
- 코어 자바스크립트(정재남)
'개발자 이야기 > JavaScript' 카테고리의 다른 글
[JavaScript] new 연산자 (0) 2021.07.19 [JavaScript] 4. 콜백 함수 (0) 2021.07.15 [JavaScript] 3. this (0) 2021.07.13 [JavaScript] 함수 선언식 vs 함수 표현식 (0) 2021.07.10 [JavaScript] 1. 데이터 타입 (0) 2021.07.09 - 자바스크립트 코드를 실행하는 순간 전역 컨텍스트가 콜 스택에 담긴다.