코어 자바스크립트 | 2. 실행 컨텍스트

🚀·2021년 9월 16일
0

코어자바스크립트

목록 보기
2/3
post-thumbnail

1. 실행 컨텍스트란?


실행 컨텍스트(execution context)는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로, 자바스크립트의 동적 언어로서의 성격을 가장 잘 파악할 수 있는 개념이다.

자바스크립트는 어떤 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 위로 끌어올리고(호이스팅), 외부 환경 정보를 구성하고, this 값을 설정하는 등의 동작을 수행한다.

본격적으로 실행 컨텍스트를 살펴보기전 스택(stack)과 큐(queue)의 개념을 살펴보자.

스택 ⇒ 출입구가 하나뿐인 깊은 우물 같은 데이터 구조. 선입후출
큐 ⇒ 양쪽이 모두 열려있는 파이프의 데이터 구조. 선입선출

앞서 실행 컨텍스트를 실행할 코드에 제공할 환경 정보들을 모아놓은 객체라고 했다. 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 콜 스택에 쌓아올렸다가, 가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다. 우리가 흔히 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것뿐이다. 예제를 보자.

// -----------------------------  (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
  1. 자바스크립트 코드를 실행하는 순간 (1) 전역 컨텍스트가 콜 스택에 담긴다. 최상단의 공간은 코드 내부에서 별도의 실행 명령이 없어도 브라우저에서 자동으로 실행하므로 자바스크립트 파일이 열리는 순간 전역 컨텍스트가 활성화된다고 이해하면 된다.
  2. 전역 컨텍스트와 관련된 코드들을 순차로 실행하다가 (3)에서 outer 함수를 호출하면 자바스크립트 엔진은 outer에 대한 환경 정보를 수집해서 outer 실행 컨텍스트를 생성한 후 콜 스택에 담는다. 콜 스택의 맨 위에 outer 실행 컨텍스트가 놓인 상태가 됐으므로 전역 컨텍스트와 관련된 코드의 실행을 일시중단하고 대신 outer 실행 컨텍스트와 관련된 코드, 즉 outer 함수 내부의 코드들을 순차로 실행한다.
  3. 다시 (2)에서 inner 함수의 실행 컨텍스트가 콜 스택의 가장 위에 담기면 outer 컨텍스트와 관련된 코드의 실행을 중단하고 inner 함수 내부의 코드를 순서대로 진행할 것이다.
  4. inner 함수 내부에서 a 변수에 값 3을 할당하고 나면 inner 함수의 실행이 종료되면서 inner 실행 컨텍스트가 콜 스택에서 제거된다. 그러면 아래에 있던 outer 컨텍스트가 콜 스택의 맨 위에 존재하게 되므로 중단했던 (2)의 다음 줄부터 이어서 실행한다. a변수의 값을 출력하고 나면 outer 함수의 실행이 종료되어 outer 실행 컨텍스트가 콜 스택에서 제거되고, 콜 스택에는 전역 컨텍스트만 남아 있게 된다. 그런 다음 실행을 중단했던 (3)의 다음 줄부터 이어서 실행한다.
  5. a변수의 값을 출력하고 나면 전역 공간에 더는 실행할 코드가 남아 있지 않아 전역 컨텍스트도 제거되고, 콜 스택에는 아무것도 남지 않은 상태로 종료된다.

스택 구조를 잘 생각해보면 한 실행 컨텍스트가 콜 스택의 맨 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점임을 알 수 있다!

어떤 실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는 데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장한다. 여기에 담기는 정보들을 다음과 같다.

VariableEnvironment : 현재 컨텍스트 내의 식별자에 대한 정보 + 외부 환경 정보.
선언 시점의 LexicalEnvirionment의 스냅샷으로, 변경 사항은 반영되지 않음
LexicalEnvironment : 처음에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영됨.
ThisBinding : this 식별자가 바라봐야 할 대상 객체.

위의 그림은 활성화된 실행 컨텍스트의 수집 정보이다.

2. VariableEnvironment


VariableEnvrionment에 담기는 내용은 LexicalEnvironment와 같지만 최초 실행 시의 스냅샷을 유지한다는 점이 다르다. 실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LexicalEnvironment를 만들고, 이후에는 LexicalEnvironment를 주로 활용하게 된다.

3. LexicalEnvironment


LexicalEnvironment는 '사전적인'이 어울리는 표현이다. "현재 컨텍스트의 내부에는 a,b,c와 같은 식별자들이 있고 그 외부 정보는 D를 참조하도록 구성돼있다"라는, 컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아놓은 것이다.

1) environmentRecord와 호이스팅

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

  • 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자
  • **선언한 함수가 있을 경우 그 함수 자체**
  • var로 선언된 변수의 식별자

⇒ 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집.

변수 정보를 수집하는 과정을 모두 마쳤더라도 아직 코드들은 실행되기 전의 상태. 코드가 실행 전임에도 불구하고 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명들을 모두 알고있다.

'자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 코드를 실행한다'고 생각해도 문제가 없다!

여기서 호이스팅이라는 개념이 등장

⇒ 호이스팅이란 '끌어올리다'라는 의미로, 변수 정보를 수집하는 과정을 더욱 이해하기 쉬운 방법으로 대체한 가상의 개념이다. 자바스크립트 엔진이 실제로 끌어올리지는 않지만 편의상 끌어올린 것으로 간주하는 것.

매개 변수와 변수에 대한 호이스팅

function a (x) {  // 수집 대상 1 (매개변수)
	console.log(x)  // (1) -- 1 출력(예상)
	var x           // 수집 대상 2 (변수 선언)
	console.log(x)  // (2) -- undefined 출력(예상)
	var x = 2       // 수집 대상 3 (변수 선언)
	console.log(x)  // (3) -- 2 출력(예상)
}
a(1);

우선 호이스팅이 되지 않았을 때, (1), (2), (3)에서 어떤 값들이 출력될지를 예상해보자.

(1)에는 함수 호출 시 전달한 1이 출력되고,

(2)에는 선언된 변수 x에 할당한 값이 없으므로 undefined가 출력되고

(3)에는 2가 출력될 것 같다.

실제로는 어떤 결과가 나오고 왜 그렇게 되는지 알아보자!

function a(){
	var x;         // 수집 대상 1의 매개변수 선언 부분
	var x;         // 수집 대상 2의 변수 선언 부분
	var x;         // 수집 대상 3의 변수 선언 부분
	x = 1          // 수집 대상 1의 할당 부분
	console.log(x) // (1) -- 1 출력
	console.log(x) // (2) -- 1 출력
	x = 2          // 수집 대상 3의 할당 부분
	console.log(x) // (3) -- 2 출력
}
a(1)

(1)은 1, (2)는 2, (3)은 2라는 결과가 나온다. (2)에서 undefined가 아닌 1이 출력된다는 건 호이스팅 개념을 이해하고 있어야한다.

함수 선언의 호이스팅

function a() {
	console.log(b);  // (1) -- undefined 출력(예상)
	var b = 'bbb';   // 수집 대상 1 (변수 선언)
	console.log(b);  // (2) -- 'bbb' 출력(예상)
	function b(){};  // 수집 대상 2 (함수 선언)
	console.log(b);  // (3) -- b함수 출력(예상)
}
a();

출력 결과를 미리 예상해보자. (1)에는 b의 값이 없으니 undefined가 나올 것 같다. (2)는 'bbb', (3)은 b함수가 출력될 것같다. 실제로도 그럴지 한번 보자!

function a(){
	var b           // 수집 대상 1. 변수는 선언부만 끌어올린다. 
	function b(){}  // 수집 대상 2. 함수 선언은 전체를 끌어올린다. 
	console.log(b)  // (1) -- 함수 b 출력
	b = 'bbb'       // 변수의 할당부는 원래 자리에 남겨둔다. 
	console.log(b)  // (2) -- 'bbb' 출력
	console.log(b)  // (3) -- 'bbb' 출력
}
a()

해석의 편의를 위해 한 가지 더 바꿔보자. 호이스팅이 끝난 상태에서의 함수 선언문은 함수명으로 선언한 변수에 함수를 할당한 것처럼 여길 수 있다.

function a(){
	var b           // 수집 대상 1. 변수는 선언부만 끌어올린다. 
	var b = function b(){}  // 바뀐 부분!!! 🌟🌟
	console.log(b)  // (1) -- 함수 b 출력
	b = 'bbb'       // 변수의 할당부는 원래 자리에 남겨둔다. 
	console.log(b)  // (2) -- 'bbb' 출력
	console.log(b)  // (3) -- 'bbb' 출력
}
a()

함수 선언문과 함수 표현식

둘 다 함수를 새롭게 정의할 때 쓰이는 방식
1. 함수 선언문 ⇒ function 정의부만 존재하고 별도의 할당 명령이 없는 것을 의미

  • 반드시 함수명의 정의되어 있어야 함
  1. 함수 표현식 ⇒ 정의한 function을 별도의 변수에 할당하는 것 의미
    • 함수명이 정의되어 있지 않아도 됨
    • 함수명을 정의한 함수 표현식 ⇒ "기명 함수 표현식"
    • 함수명을 정의하지 않은 함수 표현식 ⇒ "익명 함수 표현식"
function a () { ... } // 함수 선언문. 함수명 a가 곧 변수명
a() // 실행 ok
var b = function() { ... } // 익명 함수 표현식. 변수명 b가 곧 함수명
b() // 실행 ok
var c = function d () { ... } // 기명 함수 표현식. 변수명은 c, 함수명은 d
c() // 실행 ok
d() // 에러 !!

함수 선언문과 함수 표현식의 실질적인 차이를 살펴보자 !

console.log(sum(1, 2))
console.log(multiply(3, 4));
function sum (a, b) {             // 함수 선언문 sum
	return a + b;
}
var multiply = function (a, b) {  // 함수 표현식 multiply
	return a + b
}

호이스팅을 마친 최종 상태를 바로 확인해보자

var sum = function sum (a, b) {   // 함수 선언문은 전체를 호이스팅한다. 
	return a + b;
}
var multiply                      // 변수는 선언부만 끌어올린다. 
console.log(sum(1, 2))            // 3출력
console.log(multiply(3, 4));      // Error! 'multiply is not a function'
multiply = function (a, b) {      // 변수의 할당부는 원래 자리에 남겨 놓는다.
	return a + b
}

sum함수는 선언 전에 호출해도 아무 문제없이 실행된다. 어떻게 작성해도 오류를 내지 않는다는 면에서 초급자들이 자바스크립트를 좀 더 쉽게 접근할 수 있게 해주는 측면도 있지만, 반대로 큰 혼란을 일으키는 원인이 되긴도 한다.

함수 선언문보다는 함수 표현식이 어떻게 보면 더 안전하다고 생각하면 된다.

2) 스코프, 스코프 체인, outerEnvironmentReference

스코프란 식별자에 대한 유효범위이다. 어떤 경계 A의 외부에서 선언한 변수는 A의 외부뿐 아니라 A의 내부에서도 접근이 가능하지만, A의 내부에서 선언한 변수는 오직 A의 내부에서만 접근할 수 있다.

자바스크립트는 특이하게도 ES5까지는 전역공간을 제외하면 오직 함수에 의해서만 스코프가 생성된다. 이러한 '식별자의 유효범위'를 안에서부터 바깥으로 차례로 검색해나가는 것을 스코프 체인이라고 한다. 그리고 이를 가능케 하는 것이 바로 LexicalEnvironment의 두 번째 수집 자료인 outerEnvironmentReference이다.

스코프 체인

outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다. 과거 시점인 '선언될 당시'에 주목하자. '선언하다'라는 행위가 실제로 일어날 수 있는 시점이란 콜 스택 상에서 어떤 실행 컨텍스트가 활성화된 상태일 때뿐이다. 어떤 함수를 선언(정의)하는 행위 자체도 하나의 코드에 지나지 않으며, 모든 코드는 실행 컨텍스트가 활성화 상태일 때 실행되기 때문이다.

예를들어 A함수 내부에 B함수를 선언하고, 다시 B함수 내부에 C함수를 선언했다고 가정하자!

함수 C의 outerEnvironmentReference는 함수 B의 LexicalEnvironment를 참조한다.
⇒ 함수 B의 LexicalEnvironment 에 있는 outerEnvironmentReference는 다시 함수 B가 선언되던 때 A의 LexicalEnvironment를 참조할것이다.
⇒ 이처럼 outerEnvironmentReference 는 연결리스트 형태를 띈다.
⇒ '선언 시점의LexicalEnvironment'를 계속 찾아 올라가면 마지막엔 전역 컨텍스트의 LexicalEnvironment가 있을 것이다.
⇒ 또한 각 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)

다음 코드를 보자 !

  1. 전역 컨텍스트 활성화. 전역 컨텍스트의 environmentRecord에 { a, outer } 식별자 저장. 전역 컨텍스트는 선언 시점이 없으므로 outerEnvironmentReference에는 아무것도 담기지 않는다. (this : 전역 객체)
  2. 1번째과 2번째 줄 : 전역 스코프에 있는 변수 a에 1을, outer에 함수를 할당.
  3. 10번째 줄 : outer함수 호출. 이에 따라 전역 컨텍스트의 코드는 10번째 줄에서 임시중단. outer 실행 컨텍스트가 활성화 되어 2번째 줄로 이동
  4. 2번째 줄: outer 실행 컨텍스트의 environmentRecord에 { inner } 식별자 저장. outerEnvironmentReference에는 outer함수가 선언될 당시의 LexicalEnvironment가 담긴다.
    outer 함수는 전역 공간에서 선언됐으므로 전역 컨텍스트의 LexicalEnvironment를 참조복사한다. 이를 [ Global, { a, outer } ]라고 표기하자. 첫 번째는 실행 컨텍스트의 이름, 두 번째는 environmentRecord 객체이다. (this : 전역 객체)
  5. 3번째 줄 : outer 스코프에 있는 변수 inner에 함수를 할당.
  6. 7번째 줄 : inner 함수 호출. 이에 따라 outer 실행 컨텍스트의 코드는 7번째 줄에서 임시 중단되고, inner 실행 컨텍스트가 활성화되어 3번째줄로 이동.
  7. 3번째 줄 : inner 실행 컨텍스트의 environmentRecord에 { a } 식별자 저장. outerEnvironmentReference에는 inner 함수가 선언될 당시의 LexicalEnvironment가 담긴다. inner 함수는 outer 함수 내부에서 선언됐으므로 outer 함수의 LexicalEnvirionment, 즉 [ outer, { inner } ]를 참조복사. (this : 전역객체)
  8. 4번째 줄 : 식별자 a에 접근하고자 함. 현재 활성화 상태인 inner 컨텍스트의 environmentRecord에서 a를 검색. a가 발견됐는데 여기에는 아직 할당된 값 없음 (undefined 출력)
  9. 5번째 줄 : inner 스코프에 있는 변수 a에 3을 할당.
  10. 6번째 줄 : inner 함수 실행 종료. inner 실행 컨텍스트가 콜 스택에서 제거되고, 바로 아래의 outer 실행 컨텍스트가 다시 활성화되면서, 앞서 중단했던 7번째 줄의 다음으로 이동.
  11. 8번째 줄 : 식별자 a에 접근하고자 한다. 이때 자바스크립트 엔진은 활성화된 실행 컨텍스트의 LexicalEnvironment에 접근한다. 첫 요소의 environmentRecord에서 a가 있는지 찾아보고, 없으면 outerEnvirionmentReference에 있는 environmentRecord로 넘어가는 식으로 계속해서 검색. 예제에서는 두 번째, 즉 전역 LexicalEnvironment에 a가 있으니 그 a에 저장된 값 1을 반환.
  12. 9번째 줄 : outer 함수 실행이 종료. outer 실행 컨텍스트가 콜 스택에서 제거되고, 바로 아래의 전역 컨텍스트가 다시 활성화되면서, 앞서 중단했던 10번째 줄의 다음으로 이동.
  13. 11번째 줄 : 식별자 a에 접근하고자 함. 현재 활성화 상태인 전역 컨텍스트의 environmentRecord에서 a를 검색. 바로 a를 찾을 수 있음 (1 출력). 이로써 모든 코드의 실행 완료. 전역 컨텍스트가 콜 스택에서 제거되고 종료.

정리 !
environmentRecord
⇒ 컨텍스트 내부의 식별자 정보.
outerEnvironmentReference
⇒ 외부 Scope의 주소 참조. 현재 호출된 함수가 선언될 당시의 LexicalEnvironment참조.
스코프
⇒ 식별자에 대한 유효범위.
스코프 체인
⇒ '식별자의 유효범위'를 안에서부터 바깥으로 차례로 검색해 나가는 것. 이를 가능하게 하는 것이 outerEnvironmentReference.

4. this


실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장된다. 실행 컨텍스트 활성화 당시에this가 지정되지 않은 경우 this에는 전역 객체가 저장된다.

그 밖에는 함수를 호출하는 방법에 따라 this에 저장되는 대상이 다르다.

5. 마지막 정리


1. 실행 컨텍스트
⇒ 실행할 코드에 제공할 환경 정보들을 모아놓은 객체. 실행 컨텍스트의 객체는 활성화 되는 시점에 VariableEnvironment, LexicalEnvironment, ThisBinding의 세 가지 정보를 수집.
⇒ 실행 컨텍스트를 생성할 때는 VariableEnvironment와 LexicalEnvironment가 동일한 내용으로 구성되지만 LexicalEnvironment는 함수 실행 도중에 변경되는 사항이 즉시 반영되는 반면 VariableEnvironment는 초기 상태를 유지.
⇒ VariableEnvironment와 LexicalEnvironment는 매개변수명, 변수의 식별자, 선언한 함수의 함수명 등을 수집하는 environmentRecord와 바로 직전 컨텍스트의 LexicalEnvironment 정보를 참조하는 outerEnvironmentReference로 구성.


2. 호이스팅
⇒ 코드 해석을 좀 더 수월하게 하기 위해 environmentRecord의 수집 과정을 추상화한 개념으로, 실행 컨텍스트가 관여하는 코드 집단의 최상단으로 이들을 '끌어올린다'고 해석하는 것.
⇒ 변수 선언과 값 할당이 동시에 이뤄진 문장은 '선언부'만을 호이스팅 하고, 할당 과정은 원래 자리에 남아있게 되는데, 여기에서 함수 선언문과 함수 표현식의 차잉가 발생.


3. 스코프
⇒ 스코프는 변수의 유효범위를 뜻함.
⇒ outerEnvironmentReference는 해당 함수가 선언된 위치의 LexicalEnvironment를 참조. 코드 상에서 어떤 변수에 접근하려고 하면 현재 컨텍스트의 LexicalEnvironment를 탐색해서 발견되면 그 값을 반환하고, 발견하지 못할 경우 다시 outerEnvironmentReference에 담긴 LexicalEnvironment를 탐색하는 과정을 거친다. 전역 컨텍스트의 LexicalEnvironment까지 탐색해도 해당 변수를 찾지 못하면 undefined를 반환.


4. 전역변수와 지역변수
⇒ 전역 컨텍스트의 LexicalEnvironment에 담긴 변수를 전역변수라 하고, 그 밖의 함수에 의해 생성된 실행 컨텍스트의 변수들은 모두 지역변수. 안전한 코드 구성을 위해 가급적 전역변수의 사용은 최소화 하는 것이 좋음


5. this
⇒ 실행 컨텍스트를 활성화하는 당시에 지정된 this가 지정. 함수를 호출하는 방법에 따라 그 값이 달라지는데, 지정되지 않은 경우에는 전역 객체가 저장.

📚 Reference

  • 코어 자바스크립트

0개의 댓글