Chapter 2. 실행 컨텍스트
2.1 실행 컨텍스트란?
실행 컨텍스트(execution content)는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로, 자바스크립트의 동적 언어로서의 성격을 잘 파악할 수 있는 개념이다. 자바스크리트는 어떤 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 위로 끌어올리고(호이스팅hoisting), 외부 환경 정보를 구성하고, this 값을 설정하는 등의 동작을 수행하는데, 이로 인해 다른 언어에서는 발견할 수 없는 특이한 현상들이 발생한다.
스택(stack)은 출입구가 하나뿐인 깊은 우물같은 구조다. 만약 순서대로 데이터 a, b, c, d를 저장했다면 꺼낼 때는 반대로 d, c, b, a의 순서로 꺼내게 된다.
큐(Queue)는 양쪽이 모두 열려있는 파이프다. 종류에 따라 양쪽 모두 입력과 출력이 가능한 큐도 있지만, 보통은 한쪽은 입력만, 한쪽은 출력만 한다. 순서대로 a, b, c, d를 저장했다면 꺼낼 때에도 a, b, c, d의 순서로 꺼내게 된다.
동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 콜 스택(call stack)에 쌓아올렸다가, 가장 위에 쌓여있는 컨텍스트와 관련된 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다.
여기서 '동일한 환경', 즉 하나의 실행 컨텍스트를 구성할 수 있는 방법으로 전역공간, eval() 함수 등이 있다. 그 외에 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것이다.
교재 p. 38에는 실행 컨텍스트와 콜 스택에 대한 설명과 순서가 잘 설명되어 있다. 여기서 눈여겨볼 것은 (1)의 전역 컨텍스트인데, 전역 컨텍스트는 일반적인 실행 컨텍스트와 특별히 다를 것이 없다. 단, 전역 컨텍스트가 관리하는 공간은 함수가 아닌 전역 공간이기 때문에 arguments가 없다. 전역 공간을 둘러싼 외부 스코프란 존재할 수 없기 때문에 스코프체인 상에는 전역 스코프 하나만 존재한다.
실행 컨텍스트 객체는 자바스크립트 엔진이 활용할 목적으로 생성할 뿐 개발자가 코드를 통해 확인할 수는 없다. 해당 객체에 담기는 정보는 아래와 같다.
- VariableEnviroment
현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보. 선언 시점의 LexicalEnvironment의 스냅샷(snapshot)으로, 변경 사항은 반영되지 않음 - LexicalEnvironment
처음에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영됨 - ThisBinding
식별자가 바라봐야 할 대상 객체
2.2 VariableEnvironment
VariableEnvironment에 담기는 내용은 LexicalEnvironment와 같지만 최초 실행 시의 스냅샷을 유지한다는 점이 다르다. 실행 컨텍스트를 실행할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 복사해서 LexicalEnvironment를 만들고, 이후에는 LexicalEnvironment를 주로 활용하게 된다.
VariableEnvironment와 LexicalEnvironment의 내부는 environmentRecord와 outer-EnvironmentReference로 구성돼 있다. 초기화 과정 중에는 사실상 완전히 동일하고 이후 코드 진행에 따라 서로 달라지게 된다.
2.3 LexicalEnvironment
lexical environment에 대해 '어휘적 환경', '정적 환경'이라는 단어가 많이 등장한다. 그러나 이보다는 '사전적 환경'이라는 표현이 적합하다. 쉽게 말하자면 "현재 컨텍스트의 내부에는 a, b, c와 같은 식별자들이 있고 그 외부 정보는 D를 참조하도록 구성돼있다"라는, 컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아둔 것이다.
2.3.1 environmentRecords와 호이스팅(hoisting)
environmentRecords에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자, 선언한 함수가 있을 경우 그 함수 자체, var로 선언된 변수의 식별자 등이 식별자에 해당한다. 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집한다.
★ 전역 실행 컨텍스트는 변수 객체를 생성하는 대신 자바스크립트 구동 환경이 별도로 제공하는 객체인 전역 객체(global object)를 활용한다. 전역 객체에는 브라우저의 window, Node.js의 global 객체 등이 있다. 이들은 자바스크립트 내장 객체(native object)가 아닌 호스트 객체(host object)로 분류된다.
변수 정보 수집을 모두 마쳤더라도 아직 실행 컨텍스트가 관여할 코드들은 실행되기 전의 상태다. 즉, 코드가 실행되지 않았음에도 불구하고 자바스크립트 엔진은 이미 해당 환경에 속한 코드들의 변수명들을 모두 알고 있다. 따라서 '자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행한다'라고 생각해도 된다. 여기서 등장하는 개념이 '호이스팅(hoisting)'이다.
environmentRecords에는 매개변수의 이름, 함수 선언, 변수명 등이 담긴다고 했는데, 아래의 코드를 살펴보자.
function a (x) { // 수집 대상 1(매개변수)
console.log(x); // (1)
var x; // 수집 대상 2 (변수 선언)
console.log(x); // (2)
var x = 2; // 수집 대상 3 (변수 선언)
console.log(x); // (3)
}
a(1);
호이스팅이 되지 않았을 때, (1)에서는 함수 호출 시 전달한 1이 출력되고, (2)는 선언된 변수 x에 할당한 값이 없으므로 undefined, (3)에는 2가 출력될 것 같다.
위의 코드에서 매개변수를 전달받는 것 대신, 함수 내부에서 해당 변수가 선언 및 할당되었다고 볼 수 있는데, 그렇다면 위 코드를 아래와 같이 바꿀 수 있다.
function a () {
var x = 1; // 수집 대상 1(매개변수 선언)
console.log(x); // (1)
var x; // 수집 대상 2 (변수 선언)
console.log(x); // (2)
var x = 2; // 수집 대상 3 (변수 선언)
console.log(x); // (3)
}
a();
이 상태에서 호이스팅을 처리하면 environmentRecords는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있고, 각 식별자에 어떤 값이 할당될 것인지는 관심이 없다. 따라서 변수를 호이스팅 할 때 변수명만 끌어올리고 할당 과정은 원래 그 자리에 남겨둔다. environmentRecords의 관심사에 맞춰 수집 대상 1, 2, 3을 순서대로 끌어올리고 나면 아래와 같은 형태로 코드가 바뀐다.
function a() {
var x; // 수집 대상 1의 변수 선언 부분
var x; // 수집 대상 2의 변수 선언 부분
var x; // 수집 대상 3의 변수 선언 부분
x = 1; // 수집 대상 1의 할당 부분
console.log(x); // (1)
console.log(x); // (2)
x = 2; // 수집 대상 3의 할당 부분
console.log(x); //(3)
}
a(1);
이제 호이스팅이 끝났으니 실제 코드를 실행할 차례다. 실제로 실행시켜 보면 3-4라인은 이미 앞서 선언된 변수가 있으니 무시하게 된다. 따라서 실제 출력해보면 예상과 다르게 (1)과 (2)는 1을, (3)은 2를 출력하게 된다.
아래는 함수 선언을 추가한 예제로, 예상에 따르면 (1)은 undefined, (2)는 'bbb' (3)은 함수 b가 출력될 것 같다.
function a () {
console.log(b); // (1)
var b = 'bbb'; // 수집 대상 1 (변수 선언)
console.log(b); // (2)
function b () { } // 수집 대상 2 (함수 선언)
console.log(b); // (3)
}
a();
위 코드에서 수집 대상들을 전부 위로 끌어올리는 호이스팅을 하면 코드는 아래와 같아진다. 아래 코드에서 눈여겨볼 부분은 변수는 선언부와 할당부를 나누어서 선언부만 끌어올리고, 함수 선언은 함수 전체를 끌어올리는 것이다.
실제로 아래 함수를 출력하면 (1)에서는 함수 b를, (2)와 (3)에서는 'bbb'를 출력한다.
function a () {
var b; // 수집 대상 1. 변수는 선언부만 끌어올린다.
function b () { } // 수집 대상 2. 함수 선언은 전체를 끌어올린다.
// 위 코드는 아래와 같이 바꿀 수도 있다.
var b = function b () { }
console.log(b); // (1)
b = 'bbb'; // 수집 대상 1 (변수 선언)
console.log(b); // (2)
console.log(b); // (3)
}
a();
호이스팅을 다루는 김에 함께 알아두면 좋은 것은 함수 선언문(function declaration)과 함수 표현식(fuction expression)이다. 둘 모두 함수를 새롭게 정의할 때 쓰이는 방식인데, 함수 선언문은 function 정의부만 존재하고 별도의 할당 명령이 없는 것을 의미하고, 함수 표현식은 정의한 function을 별도의 변수에 할당하는 것을 말한다. 또한 함수 선언문의 경우 반드시 함수명이 정의되어 있어야 하는 반면, 함수 표현식은 없어도 된다.
함수 표현식 중에서 이름이 정의된 것이 기명 함수 표현식, 이름이 없는 것을 익명 함수 표현식이라고 하는데 일반적으로 함수 표현식은 익명 함수 표현식이다. 또한 기명 함수 표현식의 경우, 외부에서 해당 이름으로 함수를 호출할 수 없다.
function a () { } // 함수 선언문. 함수명 a가 곧 변수명.
a(); // 실행 OK.
var b = function () { } // (익명) 함수 표현식. 변수명 b가 곧 함수명.
b(); // 실행 OK.
var c = function d () { } // 기명 함수 표현식. 변수명은 c, 함수명은 d.
c(); // 실행 OK.
d(); // 에러!
기명 함수 표현식에서 함수명은 오로지 함수 내부에서만 접근할 수 있는, 재귀 함수를 호출하는 용도로 쓰인다. 그러나 함수명보다는 변수명으로 호출하는 경우가 많다.
함수 선언문의 경우, 호이스팅 과정에서 함수 전체를 끌어올리며 변수에 할당된 함수 표현식의 경우 변수 선언문만 끌어올린다. 따라서 아래 코드의 경우 (1)에서는 3, (2)에서는 변수 multiply가 정의되지 않았다는 오류가 발생한다.
console.log(sum(1, 2));
console.log(multipy(3, 4));
function sum (a, b) {
return a + b;
}
var mulitply = function (a, b) {
return a * b;
};
그러나 함수 선언문의 경우, 오류가 발생하지 않는다는 장점이 있지만 가급적이면 '선언한 후에야 호출이 가능하도록' 함수를 작성하는 게 좋다. 자신이 인지하지 못한 사이 오류가 발생할 수 있기 때문이다. 또한 동일한 함수명이 정의될 가능성을 고려하여 함수 표현식으로 작성하는 것이 낫다.
2.3.2 스코프, 스코프 체인, outerEnvironmentReference
스코프(scope)란 식별자에 대한 유효 범위다. 어떤 경계 A의 외부에서 선언한 변수는 A의 외부 뿐만 아니라 A의 내부에서도 사용이 가능하지만, A의 내부에서 선언한 변수는 A의 내부에서만 접근할 수 있다.
ES5까지의 자바스크립트는 전역공간을 제외하면 오직 함수에 의해서만 스코프가 생성된다. 이러한 '식별자의 유효범위'를 안에서부터 바깥으로 차례대로 검색해나가는 것을 스코프 체인(scope chain)이라고 하며, 이를 가능하게 하는 것이 LexicalEnvironment의 두 번째 수집 자료인 outerEnvironmentRefererence다.
outerEnvironmentReference는 현재 호출될 함수가 '선언될 당시'의 LexicalEnvironment를 참조한다. '선언하다'라는 행위가 실제로 일어날 수 있는 시점이란 콜 스택 상에서 어떤 실행 컨텍스트가 활성화된 상태일 때뿐이다. 어떤 함수를 선언(정의)하는 행위 자체도 하나의 코드에 지나지 않으며, 모든 코드는 실행 컨텍스트가 활성화 상태일 때 실행되기 때문이다.
var a = 1;
var outer = function () {
var inner = function() {
console.log(a);
var a = 3;
};
inner();
console.log(a);
};
outer();
console.log(a);
위 코드를 보면 inner함수의 outerEnvironmentReference는 함수 outer의 LexicalEnvironment를 참조한다. 이런 식으로 outerEnvironmentReference는 연결 리스트(linked list)의 형태를 띄어, '선언 시점의 LexicalEnvironment'를 계속 찾아 올라가 그 마지막에 전역 텍스트의 LexicalEnvironment와 마주한다.
또한 각 outerEnvironmentReference는 오직 자신이 선언된 시점의 LexicalEnvironment만 참조하고 있으므로 가장 가까운 요소부터 차례로 접근할 수 있고 다른 순서로 접근하는 것은 불가능하다. 이런 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하다.
위 코드에서는 먼저 호이스팅으로 변수 a와 변수 outer을 끌어올리고, 전역 컨텍스트의 environmentRecord에 {a, outer} 식별자를 저장하고 시작한다. 이때 전역 컨텍스트의 outerEnvirionmentReference에는 아무것도 담겨있지 않다. (this : 전역 객체)
이후 변수 a에는 1을, 변수 outer에는 함수를 할당한다.
outer();의 호출에 따라 outer 함수의 선언부인 2라인으로 이동한다.
그곳에서 다시 한 번 호이스팅을 통해 outer 실행 컨텍스트의 environmentRecord에 식별자 {inner}을 저장한다. outerEnvironmentReference에는 outer 함수가 선언될 당시의 LexicalEnvironment가 담긴다. outer 함수는 전역 공간에서 선언됐으므로 outer 함수의 outerEnvironmentReference는 전역 컨텍스트의 LexicalEnvironment를 참조한다. 이를 [ GLOBAL, {a, outer} ]라고 표기한다. 첫번째는 실행 컨텍스트의 이름, 두번째는 해당 컨텍스트의 environmentRecord 객체다. (this : 전역 객체)
outer 스코프에 있는 inner 변수에 함수를 할당한다.
inner 함수를 호출하고, 이에 따라 inner 함수의 선언부로 이동한다.
inner 실행 컨텍스트의 environmentRecord에 식별자 {a}를 저장하고, inner 실행 컨텍스트의 outerEnvironmentReference는 outer 함수의 LexicalEnvironment가 담긴다. 즉, [ outer, {inner} ]을 참조 복사한다. (this : 전역 객체)
식별자 a에 접근하지만, 현재 inner 컨텍스트의 environmentRecord에는 a가 있지만 해당 값이 할당되지 않았으므로 undefined를 출력한다.
inner 스코프에 있는 변수 a에 3을 할당한다.
이후 inner 함수 실행이 종료되고 inner 컨텍스트가 콜 스택에서 제거되며, 바로 아래의 outer 컨텍스트가 다시 활성화된다. 이후 식별자 a에 접근하게 되는데, outer 컨텍스트에 a가 저장되었는지 확인하고 만약 없으면 outerEnvironmentReference에 있는 environmentRecord를 검색한다. outerEnvironmentReference에 있는 (전역 컨텍스트의) environmentRecord 안의 a에는 1이 할당되어 있으므로 1을 출력한다.
이후 outer 함수 실행이 종료되고 outer 컨텍스트가 콜 스택에서 제거되며, 바로 아래의 전역 컨텍스트가 다시 활성화된다. 이후 식별자 a에 접근하게 되는데, 전역 컨텍스트의 environmentRecord안의 a에 1이 할당되어 있으므로 1을 출력한다.
▼ 스코프 체인
[0] 전역 컨텍스트 활성화 - LexicalEnvironment, VariableEnvironment, thisBinding | |||
전역 컨텍스트 [ LexicalEnvironment ] environmentRecord : a, outer outerEnvironmentReference : 없음 (this : 전역 객체) |
[1], [2] a에 1, outer에 함수 할당 | ||
[10] outer 함수 호출. 전역 컨텍스트 비활성화 [2] outer 실행 컨텍스트 활성화 |
|||
outer 컨텍스트 [ LexicalEnvironment ] environmentRecord : inner outerEnvironmentReference : GLOBAL(전역) LexicalEnvironement (this : 전역 객체) |
[3] inner에 함수 할당 | ||
[7] inner 함수 호출. outer 컨텍스트 비활성화 [3] inner 실행 컨텍스트 활성화 |
|||
inner 컨텍스트 [ LexcialEnvironment ] environmentRecord : a outerEnvironmentReference : outer LexicalEnvironment (this : 전역 객체) |
[4] inner의 L.E에서 a검색 → undefined 출력 |
||
[5] a에 3 할당 | |||
[6] inner 함수 종료. inner 실행 컨텍스트 제거 [7] outer 실행 컨텍스트 재활성화 |
|||
[8] outer의 LexicalEnvironment에서 a 탐색 → GLOBAL의 LexicalEnvironment에서 a 탐색 → 1 출력 |
|||
[9] outer 함수 종료. outer 실행 컨텍스트 제거 [10] 전역 컨텍스트 재활성화 |
|||
[11] GLOBAL의 LexcialEnvironment에서 a 탐색 → 1 출력 | |||
[] 전체 종료. 전역 컨텍스트 제거 |
위 표의 전체적인 윤곽을 왼쪽에서 오른쪽으로 바라보면, '전역 컨텍스트 → outer 컨텍스트 → inner 컨텍스트' 순으로 규모가 점차 작아지는 반면 스코프 체인을 타고 접근 가능한 변수의 수는 늘어난다.
단, inner 실행 컨텍스트에서 a 변수를 탐색한 경우 inner 실행 컨텍스트에서도 전역 컨텍스트에서도 a 변수가 존재했다. 그러나 inner 스코프의 LexicalEnvironment에 a 식별자가 존재하므로 스코프체인을 더 이상 진행하지 않고 즉시 inner LexicalEnvironment 상의 a를 반환하게 된다. 즉, inner 함수 내부에서 a 변수를 선언했기 때문에 전역 공간에서 선언한 동일한 이름의 a 변수에는 접근할 수 없는 것이다. 이를 변수 은닉화(variable shadowing)이라 한다.
★ 크롬 브라우저에서는 스코프 체인 중 현재 실행 컨텍스트를 제외한 상위 스코프 정보들을 개발자 도구의 콘솔을 통해 간단하게 확인할 수 있다.
전역 변수(global variable)은 전역 스코프에서 선언한 변수이며, 지역 변수(local variable)은 함수 내부에서 선언한 변수다. 함수 선언식 역시 전역 변수에 해당하는데, 이런 변수를 지역 변수로 만들기 위해 외부에 X라는 함수를 하나 더 만들면 함수 선언식으로 만들어진 함수는 지역 변수가 된다. 이 경우 함수를 호출할 수 있는 영역은 X 내부로 국한된다. 이처럼 코드의 안정성을 위해 전역변수의 사용은 최소화 하는 것이 좋다.
2.4 this
실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장된다. 실행 컨텍스트의 활성화 당시에 this가 지정되지 않은 경우 this에는 전역 객체가 저장된다. 그 밖에는 함수를 호출하는 방법에 따라 this에 저장되는 대상이 다르다. (Chapter 3. 참조)
'Javascript' 카테고리의 다른 글
인프런 리액트 강의 - Javascript 요약 ① (1) | 2023.12.07 |
---|---|
Chapter 5 요약 (1) | 2022.09.21 |
Chapter 4 요약 (0) | 2022.09.15 |
Chapter 3 요약 (0) | 2022.09.13 |
Chapter 1 요약 (0) | 2022.09.04 |