[Javascript] 실행 컨텍스트

Yeon Jeffrey Seo·2022년 8월 26일
0

JavaScript

목록 보기
6/7
post-custom-banner

실행 컨텍스트

실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체. 동일한 환경에 있는 코드들을 실행할 때, 필요한 환경 정보들을 모아 컨텍스트를 구성하고, 이를 call stack에 쌓아 올린다. 가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다.
하나의 실행 컨텍스트를 구성할 수 있는 방법으로 전역공간, eval(), 함수 등이 있다.

콜 스택에서 새로운 스택이 쌓일 때, 자바스크립트 엔진은 새로운 스택에 대한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장한다.

실행 컨텍스트에 담기는 정보는 아래와 같다.

  • VariableEnvironment: 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보, 선언 시점의 LexicalEnvironment의 스냅샷.

  • LexicalEnvironment: 처음에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영됨.

  • ThisBinding: this 식별자가 바라봐야할 대상 객체

1. VariableEnvironment

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

지난 번 렉시컬 환경에 대한 포스팅...

VariableEnvironmentLexicalEnvironment의 내부는 environmentRecordouterEnvironmentReference로 구성되어 있다.

2. LexicalEnvironment

environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 컨텍스트 구성 함수에 지정된 매개볂수 식별자, 선언한 함수, var로 선언한 변수의 식별자 등을 저장하며, 자바스크립트 엔진이 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 그 정보를 순서대로 수집한다.

💡 전역 실행 컨텍스트는 자바스크립트 구동환경이 별도로 제공하는 객체를 사용한다.
브라우저의 경우 window, Node.js의 경우 global 객체 등이 이에 해당한다.
이들은 자바스크립트 내장 객체가 아닌 호스트 객체로 분류된다.

변수 정보를 수집하는 과정을 모두 마쳐도, 실제 코드의 실행은 되지 않은 상태이다. 자바스크립트 엔진은 코드가 실행되지 않았지만, 해당 환경에 속한 식별자들을 모두 알고 있다. 여기서 호이스팅(hoisting)의 개념이 등장한다.
'코드가 실행되지 않았지만 자바스크립트 엔진은 해당 환경의 식별자들을 모두 알고 있다.' 이 말을 우리가 알고 있는 '자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음, 실제 코드를 실행한다.' 라는 설명으로 바꾼 것이다. 이렇게 서술해도 문제될 것이 전혀 없고, 개념을 받아들이기 더 쉬워서 이런 가상의 개념을 만든 것으로 보인다.

2-1. EnvironmentRecord와 호이스팅

호이스팅 규칙

environmentRecord에는 매개변수의 이름, 함수 선언, 변수명이 담긴다. 할당, 초기화에는 관심이 없으며 그대로 남겨두고, 오직 변수명만 가져 온다. 이를 코드로 가시화를 하자면 아래와 같이 된다고 볼 수 있다.

// 우리가 작성하는 코드
function a() {
  var x = 1;
  console.log(x);
  var x;
  console.log(x);
  var x = 2;
  console.log(x);
}

a();

// Hoisting의 가시화
function a() {
  var x;
  var x;
  var x;
  
  x = 1;
  console.log(x);	// 1
  console.log(x);	// 1
  x = 2;
  console.log(x);	// 2
}

a();

호이스팅은 실행 컨텍스트가 구성될 때 일어난다. 다시 말해 함수가 실행 될 때 일어난다.

그렇다면 변수 선언이 아닌 함수 선언의 호이스팅은 어떻게 작동할까?

// 호이스팅 전
function a () {
	console.log(b); 
	var b = 'bbb';
	console.log(b); 
	function b () {}
	console.log(b) 
}

a();

// 호이스팅 후 
function a() {
  var b;
  function b() {};
  
  console.log(b); 	// [Function: b]
  b = 'bbb';
  console.log(b); 	// 'bbb'
  console.log(b);	// 'bbb'
}

a();

변수는 선언부와 할당 부를 나누어 선언부만 끌어올리는 반면, 함수 선언은 함수 전체를 끌어올린다.

함수 선언문과 함수 표현식

예전 함수 선언문과 함수 표현식에 대해 작성했던 포스팅

함수 선언문은 function 정의부만 존재하고, 할당 명령이 없다. 반면 함수 표현식은 function의 정의를 별도의 변수에 할당한다.

함수의 이름을 작성하느냐, 작성하지 않느냐에 따라 익명 함수, 기명 함수로 나눌 수 있는데 함수 선언문의 경우 반드시 함수의 이름이 표기되어야 한다. 반면 함수 표현식은 함수 이름을 작성하지 않고 함수를 정의할 수 있다.

// 함수 선언문
function a () {
	console.log('a');
}
// 함수 표현식 - 익명 함수 표현식
var b = function () {
	console.log('b');
}
// 함수 표현식 - 기명 함수 표현식
var c = function d () {
	console.log('c')
}

a();	// a
b();	// b
c();	// c
d();	// Reference Error

기명 함수 표현식의 경우, 함수 외부에서 함수 이름으로 호출이 불가능하다. 하지만 내부에서는 함수 호출이 가능한데, 재귀 함수에서 이를 활용할 수 있다.

let i = 0;
var c = function d () {
	console.log('c')
	i++;
	if(i === 10) return;
	d();
}
c();

호이스팅이 일어 날 때, 변수는 선언부만을, 함수는 함수의 선언 전체를 호이스팅 시킨다고 했다. 함수 표현식은 함수의 선언을 변수에 저장하므로 변수의 호이스팅처럼 호이스팅이 일어난다. 반면 함수 선언문의 경우, 함수 선언 전체가 호이스팅된다. 따라서 아래 코드의 실행 결과를 예측할 수 있다.

console.log(sum(1,2)); // 3
console.log(multi(2,8)) // TypeError

// 함수 선언문 - 함수 선언 전체를 호이스팅
function sum (a, b) {
	return a + b;
}

// 함수 표현식 - 변수 호이스팅
var multi = function(a, b) {
	return a * b;
}

2번째 줄 multi()의 경우, undefined가 아닌 TypeError를 반환하는 이유는 실행 컨텍스트 구성 시 multi를 변수로 다루기 때문에 호출자를 붙일 경우, 함수가 아니라는 에러를 반환하게 된다. 해당 줄을 console.log(multi) 로 바꾸게 될 경우, undefined가 출력되는 것을 확인할 수 있다.

2-2. 스코프와 스코프 체인, outerEnvironmentReference

스코프란 식별자에 대한 유효 범위이다. 식별자의 유효 범위를 안에서부터 바깥으로 차례로 검색해 나가는 것을 스코프 체인이라 한다. 이때 바깥이긴 한데, 어느 쪽 바깥으로 검색을 해 나갈지 그 참조를 저장하는 곳이 LexicalEnvironment의 두 번째 수집 자료인 outerEnvironmentReference 이다.

예를 들어 A 함수 내부에 B 함수를 선언하고, B 함수 내부에 C 함수를 선언한 경우를 생각해보자. C 함수의 outerEnvironmentReference는 B 함수의 LexicalEnvironment를 참조하고 B 함수의 outerEnvironmentReference는 A 함수의 LexicalEnvironment를 참조하게 된다. C -> B -> A 와 같은 형태를 띄는 outerEnvironmentReference연결 리스트(linked-list) 형태를 갖게 된다.

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

위 코드의 실행 결과를 확인하면서, 실행 컨텍스트와 렉시컬 환경, 스코프 체인에 대해 좀 더 확실하게 이해할 수 있었다.

  • 우선 함수의 선언들을 보면 전역 환경에서 outer 함수 표현식이 정의되어 있고, outer 내부에 inner 함수 표현식이 정의되어 있다. 스코프 체이닝은 inner -> outer -> global 로 연결되어 있음을 알 수 있다.
  • 자바스크립트 엔진이 실행되면서, 우선 전역 실행 컨텍스트를 구성한다. var a, var outer 를 호이스팅한 뒤 undefined로 초기화 한다. outer는 함수 표현식이므로 변수만 호이스팅이 일어나고 실제 함수 선언의 할당은 해당 코드에 도달해야 일어난다.
  • var a = ..., var outer = function() {...} 코드를 실행하며 변수에 값을 할당한다.
  • outer를 실행한다. 초반에 말했듯, 함수의 실행은 실행 컨텍스트를 구성한다. 실행 컨텍스트의 LexicalEnviromentouterEnvironmentReference 는 global을 가리킨다. EnvironmentRecord 는,var inner 를 호이스팅한다. 그 뒤 코드를 실행한다. inner에 익명 함수가 할당되고, inner()를 실행한다.
  • inner를 실행한다. 이 때 실행 컨텍스트가 구성되며, inner의 outerEnvironmentReference는 outer를 가리킨다. EnvironmentRecordvar a 를 호이스팅한다.
  • inner 내부에 있는 console.log(a) 를 실행한다. 나는 여기서 당연히 1이 출력될 거라 생각했는데, undefined 가 출력된다. 여기서 각각의 함수는 고유한 렉시컬 환경을 갖는다는 것을 깨달았다. inner의 LexicalEnviroment에서는 var a = 3 이라는 코드가 존재하기 때문에, 이를 호이스팅한 뒤 undefined로 초기화한다.
  • inner 함수가 종료되고, outer로 돌아온다. 그 다음 console.log(a)를 실행한다. 함수 b의 LexicalEnviroment에는 변수 a가 존재하지 않는다. 이 때 스코프 체인이 일어난다. 자바스크립트 엔진은 함수 b의 outerEnvironmentReference 를 참조하여 전역 실행 컨텍스트에서 변수 a를 찾는다. 전역 실행 컨텍스트에는 변수 a의 호이스팅, 값 할당까지 완료가 되었으므로, 변수 a에서 1이라는 값을 읽은 뒤 이를 console.log 로 출력한다. 따라서 이 때는 1이 출력된다.
  • outer의 실행이 끝났으므로, 마지막 console.log(a)를 실행한다. 1을 출력한다.

결론

왜 이렇게 어렵게 만들어놨을까.

참고 자료

  • 정재남, 코어 자바스크립트, 위키북스
profile
The best time to plant a tree was twenty years ago. The second best time is now.
post-custom-banner

0개의 댓글