본문 바로가기
FRONT-END/JavaScript

02. 실행 컨텍스트 (코어 자바스크립트)

by 랄라J 2024. 9. 21.

책을 읽은지는 꽤 지났지만, 다시 한번 복습 겸 기록을 남겨보려고 합니다. 책을 읽고 요약해 정리한 내용의 전문은 제 개인 노션에 정리해놓았습니다.

 

이전 글 : [FRONT-END/JavaScript] - 01. 데이터 타입 (코어 자바스크립트)

 

01. 데이터 타입 (코어 자바스크립트)

책을 읽은지는 꽤 지났지만, 다시한번 복습 겸 기록을 남겨보려고 합니다. 책을 읽고 요약해 정리한 내용의 전문은 제 개인 노션에 정리해놓았습니다. 데이터 타입데이터 타입의 종류자바스크

rarla-j.tistory.com

 

실행 컨텍스트

스택과 큐

실행 컨텍스트를 알아보기 전에 스택과 큐에 대한 간단한 이해가 필요합니다.

스택은 우물 같은 데이터 구조로 나중에 들어간 것이 가장 먼저 나오는 후입선출(LIFO) 구조이고,

큐는 양쪽이 모두 열려있는 파이프 같은 데이터 구조로 가장 먼저 들어간 것이 가장 먼저 나오는 선입선출(FIFO) 구조입니다.

 

실행 컨텍스트란?

정의하면 실행할 코드에 제공할 환경 정보들을 모아놓은 객체입니다.

클로저를 지원하는 대부분의 언어에서 이와 유사하거나 동일한 개념이 적용되어 있다고 합니다.
(하나의 언어를 깊이 알면 다른 언어를 배우기 쉬워지는 이유가 이런 부분 때문이지 않을까 싶습니다.)

실행컨텍스트는 자바스크립트의 동적 언어로서 성격을 가장 잘 파악할 수 있는 개념입니다.
변수를 끌어올리는 호이스팅의 이유, 외부 환경 정보 구성, this 값 설정이 모두 실행 컨텍스트가 활성화되는 시점에 일어나기 때문입니다.

동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고 이를 콜 스택에 쌓아 올렸다가, 가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장합니다.

동일한 환경은 하나의 실행 컨텍스트를 구성할 수 있는 방법을 의미하는데요. 실행 컨텍스트를 구성할 수 있는 방법은 3가지가 있습니다.
1)전역공간, 2) eval() 함수, 3) 함수입니다. 흔히 실행 컨텍스트를 구현하는 방법은 함수겠죠!

 

실행 컨텍스트가 콜스택에 쌓이는 순서

이제 코드를 통해 실행컨텍스트가 콜스택에 어떻게 쌓이는지 알아봅시다.

// -------------------- (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
  전역 컨텍스트 outer
전역 컨텍스트
inner
outer
전역 컨텍스트
outer
전역 컨텍스트
전역 컨텍스트  

위 코드가 콜스택에 쌓이는 순서는 위 테이블의 우측 방향 ➡ 으로 흘러가는 구조입니다.

1. 가장 먼저 자바스크립트 코드를 실행하는 순간 전역 컨텍스트가 콜스택에 담깁니다.
최상단의 공간은 코드 내부에서 별도의 실행 명령이 없어도 브라우저에서 자동으로 실행하므로 자바스크립트 파일이 열리는 순간 전역 컨텍스트가 활성화됩니다.

2. 콜스택에서 전역 컨텍스트와 관련된 코드를 순차적으로 실행하다가 (3)에 해당하는 outer 함수를 호출하는 코드를 만납니다.
그럼 자바스크립트 엔진은 outer에 대한 환경 정보를 수집해 outer 실행 컨텍스트를 생성한 후 콜스택에 담습니다.
그럼 콜스택의 최상단에는 outer 실행 컨텍스트가 되었기 때문에 전역 컨텍스트의 실행을 일시 중단하고 outer 실행 컨텍스트와 관련된 코드를 순차적으로 실행합니다.

3. 그러다 (2)에 해당하는 inner 함수를 호출하는 코드를 만나면 2번과 마찬가지로 inner에 대한 환경 정보를 수집해 inner 실행 컨텍스트 생성 후 콜스택에 담고, 그럼 콜스택 최상단이 inner 실행 컨텍스트가 되어 outer 실행 컨텍스트는 일시 중단하고 inner 실행 컨텍스트와 관련된 코드를 순차적으로 실행합니다.

4. var a = 3을 하는 코드가 끝나게 되면 inner 함수 내부에 더 이상 실행할 코드가 없기 때문에 inner 실행 컨텍스트가 콜스택에서 제거됩니다. 그럼 outer 실행 컨텍스트가 콜스택의 최상단이 되어 중단되었던 코드 (2) 다음부터 다시 실행하게 됩니다.

5. console.log(a)를 하는 코드가 끝나게 되면 outer 함수 내부에 더 이상 실행할 코드가 없기 때문에 outer 실행 컨텍스트가 콜스택에서 제거됩니다. 그럼 전역 실행 컨텍스트가 콜스택의 최상단이되어 중단되었던 코드 (3) 다음부터 다시 실행하게 됩니다.

6. 마지막 console.log(a)를 하는 코드가 끝나게 되면 전역 공간에 더이상 실행할 코드가 없기 때문에 전역 실행 컨텍스트가 콜스택에서 제거되어 콜스택이 비게 됩니다.

 

사실 한번 이해하면 어렵지 않은 흐름이죠?
전역부터 실행하다가 함수 호출되면 그 함수의 실행컨텍스트 생성되고 끝나면 종료되는 형태입니다!

즉, 특정 실행 컨텍스트가 콜스택의 맨 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점이 됩니다.

어떤 실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드를 실행하는데 필요한 환경 정보들을 수집해서 실행 컨텍스트에 저장합니다. (이 객체는 자바스크립트 엔진이 활용할 목적으로 생성되어 개발자가 코드를 통해 확인할 수는 없다고 합니다.)

 

실행 컨텍스트의 수집 정보

VariableEnvironment, LexicalEnvironment, ThisBinding 3가지입니다.

 

VariableEnvironment

- 현재 컨텍스트 내의 식별자들에 대한 정보와 외부 환경 정보를 저장합니다.

- 선언 시점의 LexicalEnvironment의 스냅샷으로, 변경사항은 반영되지 않습니다.

- 실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 가장 먼저 담고 이를 복사해 LexicalEnvironment를 만듭니다. 그 이후로는 주로 LexicalEnvironment를 활용하게 됩니다.

 

LexicalEnvironment

- 처음에는 VariableEnvironment와 같지만 변경사항이 실시간으로 반영됩니다.

- 컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아놓은 것으로 이해하면 됩니다.

- LexicalEnvironment의 environmentRecord + outerEnvironmentReference에 의해 변수의 유효범위인 스코프가 결정되고 스코프 체인이 가능해집니다.

 

ThisBinding

- this 식별자가 바라봐야 할 대상 객체를 의미합니다.


여기서 더 들어가서 LexicalEnvironment의 enviromentRecord + outerEnvironmentReference에 대해 알아보겠습니다.

enviromentRecord, outerEnvironmentReference

실행 컨텍스트를 구성할 때 실행할 코드에게 제공해주기 위해 내가 가진 정보와 외부에 나와 연결되어 있는 정보가 필요하겠죠.

그 역할을 하는 것이 바로 enviromentRecord, outerEnvironmentReference입니다.

위 둘은 VariableEnvironment와 LexicalEnvironment의 내부 구성요소입니다.

 

enviromentRecord

- 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됩니다.
(컨텍스트를 구성하는 함수에 지정된 매개변수 식별자, 선언함 함수가 있는 경우 함수 그 자체, var로 선언된 변수의 식별자)

 

outerEnvironmentReference

- 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조하며, 스코프와 연관이 있습니다.

outerEnvironmentReference의 자세한 설명은 이어 나올 스코프에 대한 설명에서 조금 더 풀어보기로 하겠습니다.

 

추가적으로 알아두면 좋은 정보)
전역 실행 컨텍스트의 경우 변수 객체를 생성하는 대신에 자바스크립트 구동 환경이 별도로 제공하는 객체인 전역 객체를 활용합니다. 전역 객체에는 브라우저의 window, Node.js, global 객체가 있는데, 이들은 자바스크립트 내장 객체가 아닌 호스트 객체로 분리됩니다.

 

위에까지 이해했다면 코드를 실행하기 전에도 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명들을 모두 알고 있게 된다는 것을 알 수 있습니다. 자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행한다고 하는 의미를 이해할 수 있겠죠?

하지만 코드로 보기 전까진 이해하기 어려우니 코드를 보고 호이스팅에 대한 개념을 다시 한번 확인해 이해해 봅시다.

 

변수 호이스팅

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

a(1);

여기서 잠깐 멈춰서 위 console.log들이 출력될 때의 값을 예상해 봅시다!

왜 그렇게 생각했는지도 생각한 뒤에 정답을 함께 맞혀봅시다 :)

 

우선 위 코드를 매개변수를 변수 선언 및 할당과 같다고 간주해서 변환하면 다음과 같습니다.

function a() {
    var x = 1; // 매개변수 선언 및 할당
    console.log(x);
    var x;
    console.log(x);
    var x = 2;
    console.log(x);
}

a();

 

그리고 호이스팅을 마친 상태로는 다음과 같습니다.

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가 출력됩니다.

여기서 눈여겨볼 것은 environment Record는 현재 실행될 컨텍스트의 대상 코드 내 어떤 식별자들이 있는지만 관심이 있고, 각 식별자에 어떤 값이 할당될지는 관심이 없다는 점입니다. 따라서 변수 호이스팅 시 변수명만 끌어올리고 할당 과정은 원래 자리에 그대로 남겨둡니다. 매개변수의 경우도 마찬가지입니다.

 

함수 호이스팅

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()

그래서 결과는 b 함수, 'bbb', 'bbb'가 출력됩니다.

여기서 함수 선언은 함수 전체를 끌어올린다는 것을 알 수 있습니다.

호이스팅이 끝난 상태의 함수 선언문은 함수명으로 선언한 변수에 함수를 할당한 것처럼 여길 수 있습니다.
즉, function b() {}; 코드는 var b = function b() {}; 로 볼 수 있는 거죠. 

 

여기서 또 새로운 용어 '함수 선언문'이 등장했습니다.
함수 선언문이 있다면 함수 표현식도 있죠! 이 둘의 차이를 간단히 알아봅시다.

함수 선언문과 함수 표현식

함수 선언문은 function의 정의부만 존자해고 별도의 할당 명령이 없는 것을 의미합니다.

function a {}

함수 표현식은 정의한 function을 별도의 변수에 할당하는 것을 의미합니다.

var a = function {} // 익명함수 표현식
var b = function c {} // 기명함수 표현식

 

위 둘의 차이로는 함수 선언문은 반드시 함수명이 정의되어야 하지만, 함수 표현식은 없어도 된다는 차이가 있습니다.

그리고 호이스팅에서도 차이가 있습니다!

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) {
    return a + b;
}

// 함수 표현식은 변수 선언부만 끌어올림
var multiply;

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

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

호이스팅의 차이가 확실히 느껴지죠? 그래서 결과는 3이 출력되고, multiply는 함수가 아니라는 에러가 출력되게 됩니다!

 

스코프

이제 outerEnvironmentReference와 연관이 있다고 잠깐 언급하고 지나갔던 스코프에 대해 알아봅시다.

- 식별자에 대한 유효범위를 의미합니다. (ES5까지는 전역공간을 제외하면 함수에 의해서만 스코프가 생성되었습니다. ES6부터는 블록 레벨 스코프로 변경되었습니다.)

- 스코프 체인이란 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해 나가는 것을 의미합니다.

 

outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조합니다.

A 함수 내부에 B 함수를 선언, B 함수 내부에 C 함수를 선언한 경우를 간단히 예를 들어보겠습니다.

C의 outerEnvironmentReference는 함수 B의 LexicalEnvironment를 참조합니다.

B의 outerEnvironmentReference는 함수 A의 LexicalEnvironment를 참조합니다.

A의 outerEnvironmentReference는 전역 컨텍스트의 LexicalEnvironment를 참조합니다.

즉, outerEnvironmentReference는 연결리스트 형태를 띱니다.

이러한 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우 무조건 스코프 체인에서 가장 먼저 발견된 식별자에만 접근 가능하게 됩니다. 

 

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} 식별자가 저장됩니다. 전역 컨텍스트는 선언 시점이 없기 때문에 outerEnvironement-Reference에는 아무것도 담기지 않습니다.

2. 전역 스코프에 있는 변수 a에 1, outer에 함수를 할당합니다.

3. outer 함수 호출되고 전역 실행 컨텍스트 임시 중단, outer 실행 컨텍스트 활성화됩니다. outer 실행 컨텍스트의 environmentRecord에 { inner } 식별자가 저장됩니다. outerEnviromentReference에는 함수 선언 당시 LexicalEnvironment가 담기는데 전역 컨텍스트의 LexicalEnvironment를 참조 복사합니다. [GLOBAL, {a, outer} ]

4. outer 스코프에 inner 함수를 할당합니다.

5. inner 함수 호출되고 outer 실행 컨텍스트 임시 중단, inner 실행 컨텍스트가 활성화됩니다. inner 실행 컨텍스트의 environmentRecorddp { a } 식별자가 저장됩니다. outerEnviromentReference에는 outer 함수의 LexicalEnvironment를 참조 복사합니다. [outer, { inner }]

6. 식별자 a에 접근하는데, 호이스팅에 의해 해당 코드 실행 시점에는 undefined 이므로 undefined가 출력됩니다.

7. inner 스코프 변수 a에 3을 할당합니다.

8. inner 함수 실행이 종료되어 inner 실행 컨텍스트가 콜스택에서 제거되고, outer 실행 컨텍스트가 활성화됩니다.

9. 식별자 a에 접근하는데, outer environmentRecord에는 a가 없어 outerEnvironmentReference에 있는 environment로 넘어가며 검색합니다. 전역 LexicalEnvironment에 a를 찾아 값 1을 반환합니다.

10. outer 함수 실행이 종료되어 outer 실행 컨텍스트가 콜스택에서 제거되고, 전역 실행 컨텍스트가 활성화됩니다.

11. 전역 컨텍스트의 environmentRecord에서 a를 검색 후 반환합니다.

12. 전역 컨텍스트가 콜스택에서 제거되고 종료됩니다.

 

추가로 알아두면 좋은 정보)
크롬 브라우저 환경에서 console.dir, debugger를 활용해 실행하면 스코프 체인 중 현재 실행 컨텍스트를 제외한 상위 스코프 정보들을 개발자 도구의 콘솔을 통해 간단히 확인할 수 있습니다.

 

전역변수와 지역변수

전역 변수는 전역 공간에서 선언한 변수를 의미합니다.

지역 변수는 함수 내부에서 선언한 변수를 의미합니다.

위 코드에서 inner 함수 내부에서 a를 선언해 전역공간에서 선언한 동일한 이름의 a 변수로 접근할 수 없는 것을 변수 은닉화라고 합니다.

코드의 안전성을 위해 가급적 전역 변수 사용을 최소화하고자 노력하는 것이 좋습니다.

 

참고로 thisBinding에 대한 부분은 다음글 this 편에서 알아보겠습니다!

다음 글 : [FRONT-END/JavaScript] - 03. this (코어 자바스크립트)

 

03. this (코어 자바스크립트)

책을 읽은 지는 꽤 지났지만, 다시 한번 복습 겸 기록을 남겨보려고 합니다. 책을 읽고 요약해 정리한 내용의 전문은 제 개인 노션에 정리해 놓았습니다. 이전 글 : [FRONT-END/JavaScript] - 02. 실행

rarla-j.tistory.com

 

728x90

댓글