JavaScript: 실행 컨텍스트 (Execution Context)

Sangmin Na·2022년 4월 12일
0

JavaScript

목록 보기
6/7

이전에 학습한 내용들을 복기하기 위하여 작성한 내용입니다. (업데이트: 2022년 4월 12일)

🌐 실행 컨텍스트란?

✔️ 실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다.

  • 자바스크립트의 동적 언어로서의 성격을 가장 잘 파악할 수 있는 개념이다
  • 자바스크립트는 어떤 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 위로 끌어올리고(호이스팅), 외부 환경 정보를 구성하고, this 값을 설정하는 등의 동작을 수행하는데, 이로 인해 다른 언어에서는 발견할 수 없는 특이한 현상들이 발생한다.
  • 실행 컨텍스트는 자바스크립트에서 가장 중요한 핵심 개념중 하나이며, 클로저를 지원하는 대부분의 언어에서 이와 유사하거나 동일한 개념이 적용되어있다.
  • 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 콜 스택(Call Stack)에 쌓아 올렸다가, 가장 위에 쌓여있는 컨텍스트와 관련있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다.
  • 여기서, '동일한 환경', 즉 하나의 실행 컨텍스트를 구성할 수 있는 방법으로 1) 전역공간, 2) eval() 함수, 3) 함수 실행 등이 있습니다.
    자동으로 생성되는 전역공간과, eval을 제외하면 우리가 흔히 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것뿐입니다.

📌 예제로 살펴보기

// ----------------------- (1)
var a = 1;
function outer() {
	function inner() {
     	console.log(a);
      	var a = 3;
    }
  	inner(); // ---------- (2)
  	console.log(a); // 1
}
outer(); // ------------- (3)
console.log(a); // 1

✔️ 자바스크립트를 실행하는 순간 (1) 전역 컨텍스트가 콜 스택에 담긴다.
✔️ (3)에서 outer 함수를 호출하면 자바스크립트 엔진은 outer 함수에 대한 환경 정보를 수집해서 outer 실행 컨텍스트를 생성한 후 콜 스택에 담는다.
✔️ 콜스택의 맨 위에 outer 실행 컨텍스트가 놓이므로 전역 컨텍스트와 관련된 코드의 실행은 일시중단하고 대신 outer 실행 컨텍스트와 관련된 코드, 즉 outer 함수 내부의 코드들을 순차적으로 실행한다.
✔️ 다시 (2) 에서 inner 함수의 실행 컨텍스트가 콜 스택의 가장 위에 담기면 outer 컨텍스트와 관련된 코드의 실행을 중단하고 inner 함수 내부의 코드를 순서대로 진행한다.
✔️ 이렇게 inner, outer, 전역 컨텍스트가 차례대로 실행되고 종료되면서 콜스택은 아무것도 남지 않은 상태로 종료된다.

  • 전역 컨텍스트라는 개념은 일반적인 실행 컨텍스트와 특별히 다를 것이 없습니다. 최상단의 공간은 코드 내부에서 별도의 실행 명령이 없어도 브라우저에서 자동으로 실행하므로 자바스크립트가 열리는 순간 전역 컨텍스트가 활성화된다고 이해하면 됩니다.
  • 콜 스택의 맨 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점이 됩니다.
  • 이렇게 어떤 실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장합니다.

🌐 활성화된 실행 컨텍스트의 수집 정보


사진 출처: Velog (사진에 오타가 조금 있습니다)

📌 Variable Environment

✔️ 현재 실행 컨텍스트 내의 식별자들에 대한 정보 (environmentRecord) + 외부 환경 정보 (outerEnvironmentReference)의 스냅샷(Snapshot)을 유지한다.

  • 실행 컨텍스트를 생성할 때 Variable Environment 정보를 먼저 담은 다음, 이를 그대로 복사해서 Lexical Environment를 만들고, 이후에는 주로 Lexical Environment를 활용하게 된다.

  • 풀어서 이야기하면, 내부 환경정보는 Environment Record에 저장하고 외부 환경 정보는 Outer Environment에 저장한다. 그리고 Lexical Environment는 Variable Environment(스냅샷)을 카피를 떠서 사용한다. 그러므로, 최초 실행시에는 사실상 완전히 동일하고 이후 코드 진행에 따라 서로 달라지게 된다.

📌 Lexical Environment

✔️ 렉시컬 환경은 매개 변수명(Parameter), 변수의 식별자(Identifier), 선언한 함수의 함수명(Function)등을 수집하는 "environmentRecord"바로 직전 실행 컨텍스트의 LexicalEnvironment 정보를 참조하는 "outerEnvironment"로 구성되어 있다.

  • 백과사전에 '바나나'를 검색하면 '칼로리가 가장 높고 당질이 많은 알칼리성 식품으로 칼륨, 카로틴, 비타민 C를 함유하고 있다'라는 문구가 등장한다.
  • 이와 같은 느낌으로, 현재 컨텍스트 내부에는 a, b, c와 같은 식별자들이 있고 그 외부 정보는 D를 참조하도록 구성돼있다" 라는 컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 이해하면 된다.
  • 최초 실행 시점에는 VariableEnvironment와 완전히 동일하다. 그러나 코드 한줄 한줄 내려가면서 변경될 수 있다.

🔗 environmentRecord와 호이스팅

environmentRecord

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

  • 매개변수, 선언된 변수의 식별자, 선언된 함수 자체가 식별자 정보에 속한다.
  • 컨텍스트 내부 전체를 처음부터 끝까지 훑어나가며 순서대로 수집한다.

호이스팅

✔️ 호이스팅은 자바스크립트 인터프레터가 매개변수, 선언된 변수의 식별자, 선언된 함수 자체가 유효 범위(Scope)의 최상단으로 끌어올려진 것을 의미한다.
✔️ 호이스팅이란 변수의 선언을 유효 범위(Scope)의 최상단으로 끌어올리는 행위를 말합니다. (같은 의미)

  • 실제로는 끌어올리지는 않지만 편의상 끌어올린 것으로 간주한다.

호이스팅 규칙

선언 방식Hoisted?Initial ValueScope
함수 선언문✅ YES실제 함수Block (in Strict Mode)
var✅ YESundefinedFunction
let, const✅ YES and ❌ NOUninitizlied (TDZ)Block
함수 표현식 및 화살표 함수🤷🏻‍♂️ Depends if using var or let/const🤷🏻‍♂️ Depends if using var or let/const🤷🏻‍♂️ Depends if using var or let/const

호이스팅 예제 (1)

  • 아래 코드의 결과가 어떻게 나올지 우선 예상을 해봅시다.
functuon 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) undefined
  • (3) 2

정답은

  • (1) 1
  • (2) 1
  • (3) 2

그럼 위의 예제를 다르게 한번 봐볼까요?

functuon a() {    
	var x;			// 수집 대상 1의 변수 선언 부분
  	var x;			// 수집 대상 2의 변수 선언 부분
  	var x;			// 수집 대상 3의 변수 선언 부분
  
  	x = 1;
  	console.log(x); // (1)
  	console.log(x); // (2)
  	x = 2;
  	console.log(x); // (3)
}
a(1);
  • 위의 코드는 호이스팅이 되었다는 것을 전제로 한 코드입니다. 이제 호이스팅의 감이 오시지 않나요?

호이스팅 예제 (2) - 함수 선언식

  • 원본 코드
function a() {
 	console.log(b); 	// (1)
 	var b = "bbb";		// 수집 대상 1 (변수 선언)
  	console.log(b);		// (2)
  	function b () { }	// 수집 대상 2 (함수 선언) 
  	console.log(b);
}

a();
  • 호이스팅을 마친 상태의 코드
function a() {
  var b;			// 수집 대상 1. 변수는 선언부만 끌어올린다.
  function b() { }	// 수집 대상 2. 함수 선언은 전체를 끌어올린다.
  
  console.log(b);	// (1)
  b = "bbb";		// 변수의 할당부는 원래 자리에 남겨둔다.
  console.log(b);	// (2)
  console.log(b);	// (3)
}

a();

console.log의 결과

  • (1) 함수
  • (2) "bbb"
  • (3) "bbb"

원본 코드만 볼때는 (1) 부분은 undefined가 나거나 오류가 날 것 같지만, 함수 선언은 함수 전체가 호이스팅이 되기 때문에 위와 같은 결과를 갖게된다.

호이스팅 예제 (3) - 함수 표현식

  • 원본 코드
console.log(sum(1,2));
console.log(multiply(3, 4));

function sum(a, b) {
	return a + b; 
}

var multiply = function (a, b) {
 	return a * b; 
}
  • 호이스팅을 마친 코드
var sum = function sum (a, b) { // (1) // (3)
	return a + b; 
}
var mulitply;				// (2)
console.log(sum(1, 2)));	// (4)
console.log(multiply(3, 4));// (5)

multiply = function (a, b) {
	return a * b; 
}

위의 코드를 메모리 관점에서도 해석을 하자면

  • (1) 메모리 공간을 확보하고 확보된 공간의 주솟값을 변수 sum에 연결한다.
  • (2) 또 다른 메모리 공간을 확보하고 그 공간의 주솟값을 변수 multiply에 연결한다.
  • (3) sum 함수를 또 따른 메모리 공간에 저장하고, 그 주솟값을 앞서 선언한 변수 sum의 공간에 할당한다. 이로써, 변수 sum은 함수 sum을 바라보는 상태가 된다.
  • (4) sum을 실행한다. 정삭적으로 실행되어 3이 나온다.
  • (5) 현재 multiply는 할당되어 있지 않다. 그러므로, 'multiply is not a function'이라는 에러 메시지가 출력된다. 그 뒤에 코드는 이 에러로 인해 실행되지 않은 채 런타임이 종료된다.

❓ 언제 함수 선언문(Function Declaration)과 함수 표현식(Function Expression)을 사용해야 하나요?

  • 일관성만 있다면 어떠한 방식을 사용해도 상관없다.
  • 단, 함수 선언식을 사용할 때는 이미 같은 이름으로 정의되어있는 변수를 변경할 수도 있으니 이 점을 유의하며 사용해야한다.

스코프, 스코프 체인, outerEnvironmentReference

✔️ 스코프란 식별자에 접근할 수 있는 유효범위입니다.

  • 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색하나가는 것을 "스코프 체인"이라고 한다.
  • 이를 가능하게 하는 것이 바로 LexicalEnvironment의 두 번째 수집 자료인 outerEnvironmentReference이다.
  • outerEnvironmentReference는 현재 호출된 함수가 선언될 당시에 LexicalEnvironment를 참조한다.

예시를 보고 설명하는게 가장 이해하기 쉽다.

스코프체인 예제 (1)

var a = 1;						// (1)
var outer = function () {		// (2)
  	var inner = function () {	// (4)
      	console.log(a);			// (6)
    	var a = 3;				// (7)
    };
  	inner();					// (5)
  	console.log(a);				// (8)
}
outer();						// (3)
console.log(a);					// (9)
  • (시작) 전역 컨텍스트가 활성화가 된다. 전역 컨텍스트의 environmentRecord에 { a, outer } 식별자를 저장한다. 전역 컨텍스트는 선언 시점이 없으므로 전역 컨텍스트의 outerEnvironmentReference에는 아무것도 담기지 않는다. 그리고 this는 전역 객체가 된다.
  • (1), (2) 변수 a에 1을 할당하고, outer에 함수를 할당한다.
  • (3) outer 함수를 호출한다. 이로 인해 전역 컨텍스트의 코드는 일시중단되고, outer 실행 컨텍스트가 활성화 되어 (2)로 이동한다.
  • (2) environmentRecord에 { inner } 식별자를 저장한다. outerEnvironmentReference에는 outer 함수가 선언될 당시의 LexicalEnvironment가 담긴다. outer 함수는 전역 컨텍스트에서 선언되었으므로 전역 컨텍스트의 LexicalEnvironment를 참조복사한다. 이를 [ GLOBAL, { a, outer } ]라고 표기한다. 첫 번째는 실행 컨텍스트의 이름, 두 번째는 environmentRecord 객체이다. this는 전역 객체가 된다.
  • (4) inner 변수에 함수를 할당한다.
  • (5) inner 함수를 호출한다. 이에 따라, outer 함수의 실행 컨텍스트는 일시중단되고 inner 실행 컨텍스트가 활성화 되어 (4)로 이동한다.
  • (4) inner 실행 컨텍스트의 environmentRecord에 { a } 식별자를 저장한다. outerEnvironmentReference에는 inner 함수가 선언될 당시의 LexicalEnvironment가 담긴다. inner 함수는 outer 함수 내부에서 선언되었으므로 outer 함수의 LexicalEnvironment, 즉 [ outer, { inner } ]를 참조복사한다. this는 전역 객체가 된다.
  • (6) 식별자 a에 접근한다. 현재 활성화 상태인 inner 컨텍스트의 environmentRecord에서 a를 검색한다. 아직 할당된 값이 없으므로 undefeind를 출력 하게된다.
  • (7) inner 스코프에 있는 변수 a에 3을 할당한다.
  • inner 함수 실행이 종료된다. inner 실행 컨텍스트가 콜 스택에서 제거되고, 바로 아래의 outer 실행 컨텍스트가 다시 활성화 되면서, (8)을 실행한다.
    • (8) 식별자 a에 접근하려고 한다. 이때, 자바스크립트 엔진은 활성화된 실행 컨텍스트의 LexicalEnvironment에 접근한다.
    • 첫 요소의 environmentRecord에서 a가 있는지 찾아보고 없으면 outerEnvironmentReference에 있는 environmentRecord로 넘어가는 식으로 계석해서 검색을 해나간다.
    • 예제에서는 두 번째, 즉 전역 LexicalEnvironment에 a가 있으니 그 a에 저장된 값 1을 반환하여 출력한다.
  • (3) outer 함수가 종료된다.
  • (9) 식별자 a에 접근한다. 현재 활성화 상태인 전역 컨텍스트의 environmentRecord에서 a를 검색한다. 바로 a를 찾을 수 있으므로 1을 출력한다.
  • 전역 컨텍스트가 콜 스택에서 제거되고 종료된다.

위의 내용을 정리하자면, 새로운 함수 실행문을 만나면 새로운 실행 컨텍스트가 된다. 이 함수(inner)의 environmentRecord에는 수집한 식별자 정보를 저장하고, outerEnvironmentReference에는 이 함수가 선언 되었을 당시 실행 컨텍스트(outer)의 LexicalEnvironment가 된다.

마치, 방 안에서 부터 변수 a를 찾는 것이라고 생각하면 된다. 안방(inner)에 변수 a가 있으면 이를 사용한다. 없다면 거실(outer)로 나가서 변수 a를 찾는다. 없다면 아예 집 밖(global)으로 나가서 변수 a를 찾는다.

📌 ThisBinding

✔️ 실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장된다.
✔️ this는 함수를 호출하는 방식에 따라 달라진다.

1. Regular Function Call
- 일반 함수로 호출하는 경우 this는 window 객체를 가리킨다. Strict Mode인 경우에는 undefined를 리턴한다.

2. Dot Notation
- 메소드를 사용하는 Dot Notation인 경우, this는 Dot(.) 앞에 있는 것을 가리킨다.

3. Call, Apply, Bind
- 첫 번째 매개변수가 this가 무엇을 가리키는지 결정한다.
- 두 번째 인자부터는 함수의 인자로 사용한다.
- apply는 두 번째 인자를 배열로 받는다.
- bind는 함수를 즉시 실행하지 않고 binding된 새로운 함수를 반환한다. 즉, 변수에 저장할 수 있게된다.

4. "new" Keyword
- 함수 앞에 new 키워드를 사용하게 되면 this가 {}를 가리킨다. 즉, 빈 객체(새 인스턴스)를 생성하고 그 객체(인스턴스)를 가리킨다.

🌐 정리

  • 실행 컨텍스트실행할 코드에 제공할 환경정보를 모아놓은 객체이다.
  • 전역 공간에서 자동으로 생성되는 전역 컨텍스트, eval(), 함수 실행에 의한 실행 컨텍스트 등이 있다.
  • 실행 컨텍스트가 활성화 되는 시점VariableEnvironment, LexicalEnvironment, ThisBinding의 세 가지 정보를 수집한다.
  • VariableEnvironment는 초기 상태를 유지하며, LexicalEnvironment는 함수 실행 도중에 변경되는 사항을 즉시 반영한다.
  • LexicalEnvironment는 매개변수명(Parameter), 변수의 식별자(Identifier), 선언언 함수의 함수명 등을 수집하는 environmentRecord와 바로 직전 컨텍스트의 LexicalEnvironment 정보를 참조하는 outerEnvironmentReference로 구성 되어있다.
  • 호이스팅은 코드 해석을 좀 더 수월하게 하기 위해 environmentRecord의 수집 과정을 추상화한 개념이다.
  • 스코프는 식별자에 접근할 수 있는 유효범위이다.
  • 전역 컨텍스트의 LexicalEnvironment에 담긴 변수를 전역변수라 하고, 그 밖의 함수에 의해 생성된 실행 컨테스트의 변수들은 모두 지역변수라고 한다.

0개의 댓글