실행 컨텍스트

Ordinary·2023년 5월 22일
0

실행 컨텍스트란,

실행 컨텍스트란, 실행할 코드에 제공할 환경정보들을 모아놓은 객체를 말합니다.

프로그램이 처음 실행될 때 혹은 함수가 호출될 때, 자바스크립트 엔진에서 자동으로 생성하는 객체이고, 콜스택을 통해서 관리합니다. 프로그래머가 함수를 정의하고 호출하는 행위로써 실행 컨텍스트를 구성하는 것과 달리 프로그램 처음 실행 시 자동으로 구성되는 실행 컨텍스트를 “전역 컨텍스트”라는 이름으로 따로 구분합니다.

함수가 종료되거나 프로그램이 종료되면 실행 컨텍스트는 콜스택에서 제거됩니다.

실행 컨텍스트를 사용하기 위해 콜 스택을 사용하는 이유

스택 자료구조는 LIFO의 특징을 가진 자료구조로, 가장 마지막 들어온 요소가 가장 먼저 나가게 되는 구조입니다.

실행 컨텍스트들이 생성되어 콜 스택에 쌓아올려질 경우, 가장 위에 있는 컨텍스트가 현재 실행해야하는 컨텍스트이며, 가장 먼저 종료되는 컨텍스트라는 것을 의미하게 됩니다. 즉, 스택을 통해서 전체 코드의 환경과 순서를 보장할 수 있습니다.

실행 컨텍스트를 생성하는 방법

실행 컨텍스트를 생성하는 방법은 총 3가지가 있습니다.

  • 프로그램이 처음 실행될 때
  • eval()함수를 호출할 때
  • 프로그래머가 정의한 함수를 호출할 때

자바스크립트 엔진은 현재 코드를 실행하기 위해서 다양한 정보를 수집합니다. 여기서의 정보는 현재 컨텍스트(전역 스코프나 현재 호출함 함수 내부)에서 사용된 식별자와 만약 함수 내부에서 호출되었다면 참조할 외부 스코프에 대한 정보, 그리고 this식별자가 바라봐야할 대상 객체 등을 말합니다. 자바스크립트 엔진은 이러한 정보를 실행 컨텍스트로써 객체로 저장하고 콜 스택에 추가합니다.

ES6에서는 블록을 통해서도 컨텍스트를 생성합니다. for반복문에서 사용되는 let의 경우, for문 블록에 대한 실행컨텍스트를 생성합니다. 만약 let i와 같이 변수를 사용하고 for반복문이 종료된 경우, let i에 대한 컨텍스트도 종료되면서 사라지기 대문에 for반복문 밖에서는 let i변수에 접근할 수 없어 코드를 더 안정적으로 작성할 수 있습니다.

실행 컨텍스트의 환경 정보

실행 컨텍스트에는 총 3가지 정보를 저장합니다.

  1. variableEnvironment
    현재 컨텍스트의 정보와 외부 환경에 대한 정보를 가지고 있습니다. 최초 실행 시의 환경을 계속 유지하고 있고, variableEncironment의 정보를 복사해서 LexicalEnvironment를 생성합니다.
  1. LexicalEncironment
    variableEnvironment와 동일하게 현재 컨텍스트 정보와 외부 환경에 대한 정보를 가지고 있습니다. 초기에는 variableEnvironment와 동일하나, 이후 코드 진행에 따라 서로 달라지게 됩니다. lexicalEnvironment는 다시 environmentRecord와 outerEnvironmentreference로 나뉘어서 구성됩니다.
  • environmentRecord
    lexicalEnvironment에서 저장하는 현재 컨텍스트의 정보를 environmentRecord라고 합니다. 즉, 내부에서 사용되는 매개변수, 지역변수 식별자, 내부 선언된 함수의 식별자 등에 대한 정보를 말합니다.
    컨텍스트 내부 전체를 순서대로 훑어나가면서 수집하는데, 이 과정이 마치 식별자들을 최상단으로 끌어올려 놓는 것과 같다하여 “호이스팅”이라고 합니다.
  • outerEnvironmentReference
    스코프라는 것은 식별자를 사용할 수 있는 유효범위를 말합니다. 스코프는 서로 연결리스트의 형태로 이어져 있는데, 이는 식별자의 유효범위를 안에서부터 바깥 방향으로 검색할 수 있게 해줍니다. 이를 스코프 체인이라고 합니다.
    outerEnvironmentReference는 이런 스코프 체인이 만들어질 수 있도록 현재 호출된 함수가 선언될 당시, 자신을 호출한 함수의 LexicalEnvironment정보를 참조복사하여 가지고 있습니다. 즉 A라는 함수 내부에서 B함수가 선언되고 호출되었다면 B함수의 실행 컨텍스트의 outerEnvironmentReference는 A함수의 LexicalEnvrionment 정보가 들어있습니다.
    이런 스코프 체인은 안에서 바깥 이외 다른 순서로는 검색이 불가능하기 때문에 만약, 동일한 이름의 변수를 선언했다면 가장 가까운 변수만 접근 가능하게 되고, 이를 “변수 은닉화”라고 합니다.
  1. ThisBinding
    this로 지정된 객체가 저장되며, 별다른 설정이 없는 경우 전역 객체가 저장됩니다.

호이스팅

자바스크립트 엔진이 실행 컨텍스트를 구성할 때, 컨텍스트 내부 전체를 훑어나가면서 모든 식별자들을 수집하게 되는데, 이렇게 변수 정보를 수집하는 과정이 마치 최상단으로 끌어올리는 것과 같다하여 호이스팅이라고 합니다.

이런 변수 정보 수집 방식으로 인해 var키워드를 사용하여 변수 생성 시, 코드 실행되기 이전에 이미 모든 식별자 정보를 알고 있기 때문에 선언되기 이전에 변수에 접근해도 이미 선언된 것처럼 사용할 수 있었습니다.

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

위 코드 예시에 대해서 출력 결과를 예상해보자면 (1)의 경우, 매개변수 x 값이 출력되어 1이 출력되고, (3)의 경우, 바로 이전에 var x = 2를 통해서 선언되었기 때문에 2가 출력된다고 예상해볼 수 있습니다. (2)의 경우에는 var x가 선언만 되고, 초기화가 되지 않아서 undefined 혹은 매개변수 x를 그대로 따라 1이라고 생각할 수 있습니다. 하지만 결과값은 (1) 1, (2) 1, (3) 2가 됩니다.

function a (){ 
	var x;
	var x;
	var x;

	x = 1;
	console.log(x); // (1)
	console.log(x); // (2)
	x = 2;
	console.log(x); // (3)
}
a(1)

실제 자바스크립트에서 이뤄지는 동작은 아니지만, 호이스팅 동작의 이해를 위해 위와 같이 코드를 다시 작성했습니다. 코드 실행 전에 environmentRecord에 저장되는 변수에 대한 정보는 사실 식별자에 대한 정보만 수집됩니다. 변수에 실제 어떤 값이 저장되었는지는 관심사가 아닙니다. (함수는 조금 다릅니다)
따라서, var x에 대해 위와 같이 수집되고, x = 1, x = 2가 할당된 것처럼 동작하게 됩니다.

ES6에서는 let, const 키워드의 등장으로, 블록 스코프를 가지게 되었고, 초기화가 되기 전에 변수에 접근하려고 하면 참조 에러가 발생합니다

만약, 함수를 선언한 경우에는 호이스팅이 함수 선언문인지, 표현식인지에 따라서 다르게 동작하게 됩니다.

function a () {
	console.log(b); //(1)
	var b = 'bbb';
	console.log(b); //(2)
	function b () {};
	console.log(b); //(3)
}
a();

위와 같이 함수 b를 선언한 경우를 함수 선언문이라고 합니다. 이 때의 호이스팅을 통한 변수 식별자 수집은 다음과 같습니다.

function a () {
	var b;
	var b = function b () {} // funtion b() {} 에서 변환
	console.log(b); // (1)
	b = 'bbb';
	console.log(b); // (2)
	console.log(b); // (3)
}
a();

함수 선언문으로 함수를 정의한 경우, 마치 할당과 동시에 초기화가 이뤄진 것처럼 수집됩니다. 즉, 변수 식별자 수집 과정에서 함수 그 자체에 대한 정보까지 같이 저장하게 되는 것입니다. 따라서 전체 코드 실행 결과를 살펴보면 var b를 통해서 문자열 bbb를 저장한 변수를 선언했지만 함수 b가 덮어씌워버렸으므로 (1)에서는 bbb가 아니라 함수 b 즉, function b () {}가 출력됩니다.

함수 선언문과 표현식의 차이와 함수 표현식의 문제점

함수 선언문이란, 함수 정의부만 존재하는 것을 말합니다. 따라서, 함수명이 곧 변수명이 됩니다. 함수 표현식은 정의한 함수를 별도에 변수에 저장하는 방식으로, 함수명이 있다면 “기명 함수 표현식”, 없다면 “익명 함수 표현식”이라고 합니다.

fuction a () {} // 함수 선언식
var a = function () {} //함수 표현식

함수 선언문과 함수 표현식은 호이스팅 과정에서 차이를 보입니다. 함수 선언문은 함수 전체가 수집되는 방면, 함수 표현식은 함수 변수의 선언부만 수집됩니다.

console.log(sum(1,2));
console.log(multi(3, 4));

function sum(x, y){
	return x + y
}

var multi = function (x, y){
	return x * y
}

두가지의 함수가 표현식과 선언식으로 작성되어 있을때, 호이스팅이 일어난 직후를 살펴보게 되면 다음과 같습니다.

var sum = function sum(x, y){
	return x + y
}

var multi;

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

multi = function (x, y){
	return x * y
}

즉, 함수 선언문은 정의되기 이전에 호출되도 이미 호이스팅으로 전체가 수집되어 아무런 에러 없이 실행되는 반면에 함수 표현식의 경우, 정의되기 이전에 호출되면 런타임 에러가 발생합니다.

이런 함수 선언식의 호이스팅은 초급자들이 자바스크립트를 좀 더 쉽게 접근할 수 있게 해주는 측면도 존재하지만 반대로 큰 혼란을 일으키곤 합니다. 함수 선언문으로 동일한 이름의 함수를 여러 번 작성한 경우 같은 컨텍스트 내부에서 가장 마지막의 함수가 이전 모든 함수를 덮어씌우게 되지만, 함수 표현식으로 작성한 경우 런타임 안에서 정의된 함수가 할당되기 때문에 이런 문제를 해결할 수 있습니다.

스코프, 스코프 체인, outerEnviromentReference

스코프란 식별자에 대한 유효범위를 말합니다.
A 외부에서 선언한 변수는 A의 외부뿐 아니라 A의 내부에서도 접근이 가능하지만, A의 내부에서 선언한 변수는 오직 A 외부에서는 접근할 수 없습니다. 이러한 ‘식별자의 유효범위’를 안에서부터 바깥으로 차례로 검색해 나가는것을 스코프 체인이라고 하고, outerEnviromentReference를 통해 스코프 체인이 가능해 집니다.

스코프체인

outerEnviromentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnviroment를 참조하고 있습니다. ‘선언하다’ 라는 행위가 실제로 일어날 수 있는 시점이란 콜 스택 상에서 어떤 실행 컨텍스트가 활성화된 상태일 때 뿐입니다. 어떤 함수를 선언(정의)하는 행위 자체는 하나의 코드에 지나지 않으며, 실행 컨텍스트가 활성화되어야 코드를 읽어나가면서 함수 선언 정보를 수집할 수 있기 때문입니다.

예를 들어, A함수 내부에 B함수를 선언하고 다시 B함수 내부에 C함수를 선언한 경우,

  1. 함수C의 outerEnviromentReference는 함수 B의 lexicalEnviroment를 참조합니다.
  2. 함수B의 outerEnviromentReference는 다시 함수 B가 선언되던 때 즉, 함수 A의 lexicalEnviroment를 참조합니다.

이처럼 outerEnviromentReference는 연결 리스트형태를 띱니다.

‘선언 시점의 lexicalEnviroment’를 계속 찾아 올라가면 제일 마지막에 만나는 것은 전역 컨텍스트의 LexicalEnviroment입니다. 또한 각 outerEnviromentReference 는 오직 자신이 선언된 시점의 lexicalEnviroment만 참조하고 있으므로 가장 가까운 요소부터 차례대로만 접근할 수 있습니다. 이런 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인상에서 가장 먼저 발견된 식별자에게만 접근 가능하게 됩니다.

0개의 댓글