상세 컨텐츠

본문 제목

#8. 실행 컨텍스트

내일배움캠프 학습/JavaScript

by 남민우_ 2024. 11. 7. 20:51

본문

실행 컨텍스트

실행할 코드들, 우리가 작성한 코드들의 환경정보들을 모아놓은 '객체' 를 말한다.

이를 '콜 스택' 에 저장하여 코드의 실행 순서를 보장하는 과정을 거친다.

콜 스택

Call Stack 이라고 부른다. 스택(Stack)과 큐(Queue) 를 먼저 이야기하면

다음과 같이 Stack 은 Last in, First Out 의 LIFO, Queue 는 First In, First Out 의 FIFO 구조를 갖고 있다.

실행 컨텍스트는 이 스택의 형태로 가장 마지막에 호출된 코드가 제일 먼저 동작하는 방식이다.

하위 개념으로 Scope, 변수, 객체, Hoisting 등이 있는데 먼저 구성에 대해서 살펴본다.

구성

먼저 함수에 집중해서 살펴보자.

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

이 코드가 콜스택로 실행되는 순서를 살펴보면,

1. 전역 컨텍스트 (var a = 1;) in

2. outer() 실행 - 전역 컨텍스트 정지

3. inner() 호출 - outer() 중단, inner() 컨텍스트 in + 활성화

4. inner() 실행 이후 종료 - outer() 활성화

5. outer() 실행 이후 종료 - 전역 컨텍스트 활성화

6. 전역 컨텍스트 종료 - 코드 종료

로 진행되는 LIFO 의 순서로 진행된다.

 

그림으로 그려서 예시를 들어보면

다음과 같이 그릴 수 있다. 이 전체 틀을 콜 스택(Call Stack), 칸 하나하나를 각자의 실행 컨텍스트라고 부를 수 있다.

이 실행 컨텍스트들은 그림 상에서 가장 위에 있을 때, 말 그대로 맨 위에 노출되어 있을 때 활성화 된다.

 

실행 컨텍스트 객체의 실체

이 컨텍스트에 담기는 정보 라고 말해도 되겠다.

세가지로 나눌 수 있는데

1. VariableEnvironment (VE)

2. LexicalEnvironment (LE)

3. ThisBinding

이다. 1번 VE와 2번 LE는 사실 같은 정보를 담고 있는데 둘의 차이점으로는 변경사항을 '유지하는지/변화하는지' 라고 볼 수 있다.

이 같은 정보라 함은

1. Record : 식별자 정보

2. Outer : 외부 환경 정보

이 두가지를 담고 있으며 VE 와 LE의 차이점, 선언할 당시의 정보를 유지하는지를 SanpShot 이라고 부른다.

VE는 이 SnapShot 을 유지하고, LE는 유지하지 않는다. 코드 동작에 따라 실시간으로 변동된다는 말이다.

조금 더 자세히 들여다보자.

VE vs LE

앞서 말했듯이 담기는 정보 자체는 완전히 동일하다. SnapShot 을 유지하는지의 여부만 차이를 두는데, VE가 유지, LE가 변동 가능이다.

따라서 실행 컨텍스트의 관점에서 컨텍스트가 처음 생성될 때 VE에 이 정보를 반영하고 이를 복사한 LE가 생성되며 코드 진행 중에는 이 LE를 활용한다.

이 안에 담긴 정보에는 Record, Outer 두 종류가 있는데 다시 하나씩 살펴보자.

 

Record

식별자 정보가 저장되는데, 이 Record 에서는 'Hoisting'(이하 호이스팅) 이라는 개념이 같이 등장한다.

먼저 record 는 컨텍스트가 가지고 있는 식별자 정보를 저장하는 것으로, 함수에 지정된 식별자 정보나 함수 자체 등을 저장한다. 실행하는 것이 아니라 정보를 오직 수집하기만 하는 것이다.

이 수집과정에서 호이스팅이 나타나는데, 정보 수집 과정의 가상 개념이다 라고 이해하면 되겠다.

 

호이스팅은 이 수집한 식별자 정보를 코드의 맨 위로 끌어올리는 동작을 하는데, 이게 Record 를 수집하는 과정이라고 보면 된다.

그 과정의 규칙은 총 3가지가 있다.

 

1. 매개변수/변수는 선언부를 호이스팅

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

이런 코드가 있다고 해보자. 이대로 코드를 시행한다고 하면 결과가 1 / undefined / 2 로 나올 것 같지만 실제는 다르다.

규칙대로 매개변수/변수의 선언부를 호이스팅, 코드의 맨 위로 끌어올리게 되면 

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

다음과 같이 정리된다. 출력은 1 / 1 / 2 로 나타난다.

호이스팅의 개념을 모르면 예상하기 어려운 결과인 것을 알 수 있다.

 

2. 함수 선언은 전체를 호이스팅

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);
    b = 'bbb';
    console.log(b);
    console.log(b);
}
a();

다음처럼 진행되고 출력은 function / bbb / bbb 로 나타나며 또다시 기대와는 다른 결과가 나타난다.

특히나 함수 function b() {} 는 규칙대로 함수 선언이기에 내용까지 전체를 호이스팅해서 위로 끌어올린다. 함수 b 본문에 내용이 있다고 해도 마찬가지로 올려진다.

 

3. 함수라고 해서 전부 다 호이스팅하진 않는다.

먼저 함수 정의의 방식에는 총 3가지가 있는데,

1. 함수 선언문 : 정의부만 존재

2. 함수 표현식 : 변수에 함수를 할당

 2-1. 이명 함수 표현식 : 변수명이 곧 함수명으로 취급 ( ex) var b = function() { } )

 2-2. 기명 함수 표현식 : 변수명 외에도 함수명을 적는 것 ( ex) var b = function a () {  } )

다만 2-2는 활용성이 거의 없어 2-1까지만 본다.

 

코드로 예시를 들어보면 다음과 같다.

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;
}

두 방식은 호이스팅 규칙 2~3번에 의해 다르게 동작한다.

// 함수 선언문은 전체를 hoisting
function sum (a, b) { // 함수 선언문 sum
	return a + b;
}

// 변수는 선언부만 hoisting

var multiply; 

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

multiply = function (a, b) { // 변수의 할당부는 원래 자리
	return a + b;
};

 

여기서 함수 선언문은 호이스팅 과정에서 위험을 내포하고 있다.

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

function sum (x,y) { return (x*2) + y;}

다음과 같이 내부 구현이 다르지만 이름이 같은 두 함수가 있을 때, 호이스팅에 의해서 첫번째 Sum 함수의 내용이 변질되어 버릴 수 있다는 점이다.

다른 언어라면 같은 이름의 변/함수 선언 자체를 허용하지 않아 발생하지 않을 상황이지만, 자바스크립트의 특성 상 일어날 수 있는 일이다.

 

다만 표현식으로 작성할 경우는 이를 방지할 여지가 있다.

console.log(sum(3, 4));
var sum = function (x, y) {
	return x + y;
}
var a = sum(1, 2);

var sum = function (x, y) {
	return x + ' + ' + y + ' = ' + (x + y);
}
...
var c = sum(1, 2);
console.log(c);

다음처럼 function 을 표현식으로 작성했을 경우, 해당 함수가 작성된 시기 이후의 코드만 영향을 받는다는 점에서 의도치 않은 상황이 발생할 여지가 줄어든다.

 

Outer

두가지 개념을 먼저 설명하고 가야한다.

1. Scope

모든 언어에 존재하는 개념으로, 식별자가 영향을 끼칠 수 있는 유효 범위를 말한다.

2. Scope Chain

outer 는 함수가 선언될 당시의 외부 LE를 참조한다는 내용으로, 이 연계되는 속성을 스코프 체인이라고 부른다.

다음과 같은 콜 스택이 있다고 할 때, B의 경우를 들어보자.

B가 실행될 당시의 환경 정보는 A 이다. 해서 B의 Outer 에는 A가 들어가는 것이다.

왜 이 환경정보를 저장해야 할까? 바로 코드의 의도대로 동작하기 위해 당시의 변수 정보 등을 참조하기 위해서다.

 

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

이 코드가 호이스팅 과정을 거치고 어떻게 출력되는지 먼저 예상을 해보길 바란다.

 

아래는 내가 진행한 해석의 설명 그림이다.

 

콜 스택에는 먼저 1. 전역 컨텍스트, 2. Outer(), 3. Inner() 순으로 들어간다.

이후 Inner() 가 실행될 때 호이스팅을 거친 코드는 사진 오른쪽에 표시한 것처럼

var a;
console.log(a);
a = 1;

으로 정리되고, 이때 출력하는 a는 undefined 가 된다. 이후에 a에 1이 대입된다.

Inner() 가 이렇게 종료되고 나면 다음엔 Outer() 컨텍스트가 활성화되는데, 이 Outer() 컨텍스트의 환경 정보는 Outer() 가 실행되기 이전, 즉 전역 컨텍스트의 정보를 가지고 있는다.

이는 var a = 1; 의 정보를 가짐을 말하며 파란색의 console.log(a); 는 1이 출력된다.

이후 전역 컨텍스트가 활성화 되고 마지막 console.log(a); 또한 1이 출력된다.

해서 결과적으로 모든 출력은

undefined
1
1

로 마무리되는 것이다.

 

이 과정을 이해하는데 꽤 시간이 들었다.

얼핏 봤을 때는 순차적으로 진행될 것 같은 코드가 호이스팅이라는 과정으로 예상과는 다르게 진행되다보니 결과를 예측하기가 어려웠던 점이 있다.

 

이번 항목의 내용을 총 정리하자면

각각의 실행 컨텍스트는 LE 안에 Record / Outer 를 가지고 있고, Outer 안에는 그 실행 컨텍스트가 선언될 당시의 LE 정보가 들어 있어, Scope Chain 에 의해 상위 컨텍스트의 Record 를 읽어올 수 있다.

라고 말할 수 있겠다.

'내일배움캠프 학습 > JavaScript' 카테고리의 다른 글

#10. 3주차 숙제  (1) 2024.11.08
#9. This  (0) 2024.11.08
#7. 데이터 타입 심화  (6) 2024.11.06
#6. 숙제 - 문자열 임의대로 정렬  (0) 2024.11.05
#5. 일급 객체, Map  (4) 2024.11.05

관련글 더보기