JavaScript 키워드 실행 컨텍스트 Part 2
😃 이번에 다뤄볼 내용
이전에는 실행 컨텍스트가 무엇인지를 다뤄보았다. 이번에는 그러면 실제 간단한 코드를 가지고 어떠한 구조로 실행 컨텍스트가 생성되고 이용되는지 알아보려고 한다. 바로 드가자.
🤯 실행 컨텍스트의 생성 및 구조
var x = 1;
const y = 2;
function foo(a) {
var x = 3;
const y = 4;
function bar(b) {
const z = 5;
console.log(a + b + x + y + z);
}
bar(10);
}
foo(20);
이 코드를 대상으로 한번 어떠한 순서로 실행 컨텍스트가 생성되고 내부는 어떻게 되어있는지 확인해보려 한다.
소스코드는 총 4가지 타입으로 구분되고 한다. 갑자기 왜 소스코드 이야기를 하냐면 이 소스코드의 종류에 따라서 동작이 약간씩 다르기 때문이다.
소스코드의 타입은 크게 전역 코드, 함수 코드, eval 코드, 모듈 코드 이렇게 4가지의 종류로 나눠진다.
이중에서 전역과 함수 위주로 이번에는 동작을 살펴보려 한다. 둘의 동작이 약간은 다르나 아래 흐름으로 이어진다. 이 흐름을 기억하고 한번 살펴보자.
-
실행 컨텍스트 생성
-
렉시컬 환경 생성
-
환경 레코드 생성
-
this 바인딩
-
외부 렉시컬 환경에 대한 참조 결정
-
자 이제 한번 들어가보자
👴🏻 전역 코드 부분
전역 객체 생성
전역 객체는 전역 코드가 평가되기 이전에 생성된다. 이때 전역 객체에서는 빌트인 전역 프로퍼티와 빌트인 전역 함수, 그리고 표준 빌트인 객체가 추가되며 동작환경에 따라 호스트 객체를 포함한다.
전역 코드 평가
전역 코드 평가 순서를 우선 알아보고 가자 앞서 살펴본 순서와 약간 다른 부분이 있다.
-
전역 실행 컨텍스트 생성
-
전역 렉시컬 환경 생성
-
전역 환경 레코드 생성
-
객체 환경 레코드 생성
-
선언적 환경 레코드 생성
-
-
this 바인딩
-
외부 렉시컬 환경에 대한 참조 결정
-
환경 레코드 생성 부분이 약간 다르다. 객체 환경 레코드와 선언적 환경 레코드가 나눠져있다. let, const랑 var는 다르게 동작하는 것을 기억하는가? 이 다름을 만드는 것이 이 두 레코드이다. 차후 알아보자.
var x = 1;
const y = 2;
function foo(a) {
// 생략
}
foo(20);
전역 환경 입장에서 코드를 줄여보면 다음과 같이 보인다. 이 코드들이 실행 컨텍스트 상에서 어떻게 구성될까?
전역 실행 컨텍스트 생성
우선 실행 컨텍스트 스택에 깡통 전역 실행 컨텍스트(Global Excution Contect)를 생성한다.
전역 렉시컬 환경 생성
전역 렉시컬 환경을 생성하고 전역 실행 컨텍스트에 바인딩한다.
위 그림은 간단하게 표현한 그림인데 전역 렉시컬 환경에는 어떤게 들어있을까? 우리는 앞전에 확인할 때 렉시컬 환경 내부에는 환경 레코드와 외부 렉시컬 환경에 대한 참조, 이렇게 두 개가 있을 것을 추측할 수 있다. 하나씩 살펴본다.
전역 환경 레코드 생성
뭔가 갑자기 많이 생겼다. 당황스럽지만 하나씩 살펴보자.
-
전역 환경 레코드(Global Envirnoment Recode)가 생겼다. 전역 환경 레코드는 전역 변수를 관리하는 전역 스코프, 전역 객체의 빌트인 전역 프로퍼티와 전역함수, 표준 빌트인 객체를 재공한다.
-
객체 환경 레코드(Object Environment Recode), 선언적 환경 레코드(Declarative Environment Recode)가 생기고 연결되었다.
-
객체 환경 레코드: var 키워드로 선언한 전역 변수와 함수 선언문으로 정의한 전역 함수, 빌트인 전역 프로퍼티와 빌트인 전역 함수, 표준 빌트인 객체를 관리한다.
- 내부의 BindingObject는 전역 객체 생성시점에 생성된 전역 객체이다.
- var로 생성한 전역 변수와 함수 선언문으로 생성한 전역 함수는 BindingObject, 즉 전역 객체의 프로퍼티와 메서드가 된 모습을 볼 수 있다.
-
선언적 환경 레코드: let, const 키워드로 선언한 전역 변수를 관리한다.
- 전역 객체의 프로퍼티가 아닌 것을 볼 수 있다.
- 일시적 사각지대(초기화 단계가 아직 미반영된 시점)가 반영된 모습이다.
대략적으로 전역 환경 레코드에는 전역 객체, 전역 변수, 전역 함수를 다루는 객체 환경 레코드와 let과 const 같은 키워드로 선언한 전역변수를 다루는 선언적 환경 레코드가 있다는 것을 알면 된다.
this 바인딩
전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에 this가 바인딩된다. 일반적으로는 전역 코드에서 this는 전역 객체를 가리키므로 전역 환경 레코드의 내부 슬롯에는 전역 객체가 바인딩 된다.
이러한 this 바인딩은 전역 환경 레코드와 차후 알아볼 함수 환경 레코드에서만 존재한다.
외부 렉시컬 환경에 대한 참조
외부 렉시컬 환경에 대한 참조는 현재 평가 중인 소스코드를 포함하는 외부 소스코드의 렉시컬 환경, 즉 상위 스코프를 가리킨다. 현재 평가 중인 소스코드는 전역이고, 전역 코드를 포함하는 소스코드는 없으므로 전역 렉시컬 환경의 외부 렉시컬 환경에 대한 참조는 null이 할당된다.
전역 코드 실행
이제 어려운 구간은 끝났고 선언문을 제외한 나머지 라인을 실행하면서 위 그림과 같이 값이 할당이 되야 할 것이다.
변수 할당이나 함수 호출문을 실행한다면 어떻게 동작할까? 먼저 변수 또는 함수 이름이 선언된 식별자인지 확인해야 할 것이다. 만약 동일한 이름이 여러개 존재한다면 어떨까? 이런 상황을 위해서 어느 스코프의 식별자를 참조하면 되는지 결정이 필요하다.
이를 식별자 결정이라 한다. 식별자 결정을 하기 위해 식별자를 검색할 때는 실행 중인 실행 컨텍스트에서 식별자를 검색하기 시작한다. 지금 상황에서는 전역 렉시컬 환경에서 검색을 할 것이다.
만약 지금 찾는 것이 현 렉시컬 환경에서 없다고 하면 어떨까? 그럴 때 이용하는 것이 외부 렉시컬 환경에 대한 참조가 가리키는 렉시컬 환경으로 이동해서 식별자를 검색한다. 익숙하지 않은가? 이게 스코프 체인인 것이다.
👨🏻 함수 코드 부분
함수 코드의 평가 순서는 맨 처음 알려준 것과 동일하다.
-
함수 실행 컨텍스트 생성
-
함수 렉시컬 환경 생성
-
함수 환경 레코드 생성
-
this 바인딩
-
외부 렉시컬 환경에 대한 참조 결정
-
전역 코드 부분에서 본 객체 환경 레코드와 선언적 환경 레코드가 없는 것을 볼 수 있다. 전역 코드와의 차이 위주로 알아보고 나머지 비슷한 내용은 생략하겠다.
// 생략
function foo(a) {
var x = 3;
const y = 4;
function bar(b) {
// 생략
}
bar(10);
}
// 생략
함수 코드 평가
함수 실행 컨텍스트 생성
동일하게 함수 실행 컨텍스트를 생성한다. 생성된 함수 실행 컨텍스트는 함수 렉시컬 환경이 완성된 다음 실행 컨텍스트 스택에 푸쉬된다.
함수 환경 레코드 생성
함수 환경 레코드에서는 매개변수, arguments 객체, 함수 내부에서 선언한 지역 변수와 중첩 함수를 등록하고 관리한다. 내부를 확인해보면 알 수 있다.
여기서 한 가지 의문점이 드는 것은 왜 함수의 경우에는 객체 환경 레코드, 선언적 환경 레코드로 나뉘지 않았을까?
생각해보면 전역 환경 레코드에서는 전역 변수와 전역 함수의 경우에 기존에 있던 전역 객체의 프로퍼티로 추가가 되야하고 아닌 경우는 따로 저장해둬야 하기 때문이지 않았을까 생각이 든다. (도움이 될만한 레퍼런스가 있거나 틀린 말이면 댓글 부탁드립니다. 😂)
this 바인딩
키워드 this를 떠올려보면 일반 함수 어쩌구 이야기가 있었다. 떠올려보자. foo 함수는 일반 함수로 호출되었으므로 this는 전역 객체를 가리킨다.
외부 렉시컬 환경에 대한 참조
이전에 렉시컬 스코프는 함수를 어디서 호출했는지가 아니라 어디에 정의했는지에 따라 상위 스코프를 결정한다고 이야기 한 적이 있다. 왜 그럴까?
객체를 생성할 때의 렉시컬 환경을 저장하기 때문에 그렇다. 자바스크립트 엔진은 함수 정의를 평가하여 함수 객체를 생성할 때 현재 실행 중인 실행 컨텍스트의 렉시컬 환경, 즉 함수의 상위 스코프를 함수 객체 내부 슬롯 [[Environment]]에 저장한다.
함수 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 할당되는 것이 바로 함수의 상위 스코프를 가리키는 함수 객체의 내부 슬롯 [[Environment]]에 저장된 렉시컬 환경의 참조이다.
이걸 알고있으면 차후 클로저의 이해에 도움이 된다.
함수 코드 실행
함수 코드중 선언문을 제외한 코드가 실행되고 내부의 값이 저장된다.
👶🏻 중첩 함수 부분
이 부분의 실행 순서도 비슷하니 이번에는 순서를 둬서 하는 것을 생략하고 완성된 결과물을 가지고 한번 다른 점을 확인해보자.
크게 다른 점은 없지만 외부 렉시컬 환경에 대한 참조에 할당되는 렉시컬 환경 참조값이 foo 렉시컬 환경이라는 점이 달라졌다. 또한 객체 내부 슬롯 [[Environment]]에 저장된 위치도 달라진 것을 볼 수 있다.
이는 동일하게 함수가 평가될 시점에 연결된 것이기 때문이다.
🤮 console.log 메서드 동작 확인
var x = 1;
const y = 2;
function foo(a) {
var x = 3;
const y = 4;
function bar(b) {
const z = 5;
console.log(a + b + x + y + z);
}
bar(10);
}
foo(20);
이번 글의 마지막으로 console.log 메서드가 어떠한 순서로 동작하는지 살펴보자
일단 지금 우리의 위치는 bar 함수 실행 컨텍스트에 있다. 먼저 할 일은 무엇일까? 바로 console 이란 식별자를 찾는 일이다.
- console 식별자 어디있니?
-
우리는 bar 함수 실행 컨텍스트에 있으니 bar 함수의 렉시컬 환경에서 console을 검색한다. 하지만 이곳에는 console이 없다. 그러면 어떻게 해야할까? 바로 외부 렉시컬 환경에 대한 참조를 타고 올라간다.
-
이제 우리는 foo 함수 실행 컨텍스트에 와있다. 여기서 또 렉시컬 환경을 뒤져보며 console을 찾아본다. 또 없다. 그러면 이전과 같이 참조를 타고 이제 전역으로 이동한다.
-
전역 실행 컨텍스트에 도착했다. 전역 렉시컬 환경을 뒤져보면서 console을 찾는다. 드디어 발견했다! 전역 렉시컬 환경 > 객체 환경 레코드 > BindingObject > console 이 있다. 이제 어떻게 동작할까?
- log 메서드가 있나?
-
console 객체를 찾았으니 이제 이 console이라는 객체에서 log 메서드를 검색한다. 이때는 프로토타입 체인을 따라서 검색을 시작한다.
-
다행인점은 log 라는 메서드는 상위 프로토타입 객체에 있는 것이 아닌 직접 소유하는 메서드다.
- a + b + x + y + z 값을 평가해야 한다.
-
console.log 안에 넣어준 것은 값으로 평가되는 문이다.
-
저 안에 들어있는 변수들의 값을 일일이 찾아봐야 한다. 해야할 일은 동일하다. 현 실행 컨텍스트에서 참조를 따라서 각각 찾아낸다.
-
a 는 foo 함수 렉시컬 환경, b 식별자는 bar 함수 렉시컬 환경, x 와 y 는 foo 함수 렉시컬 환경에서, 마지막으로 z 는 bar 함수 렉시컬 환경에서 찾을 수 있다.
-
x 와 y 는 전역에도 있는데 왜 foo 함수 렉시컬 환경 값을 참조했을까? 이는 현제 실행중인 실행 컨텍스트의 렉시컬 환경에서 참조를 따라서 이동하면서 식별자를 검색하기 때문이다. 이미 foo 함수 렉시컬 환경에 값이 있는데 더 내려갈 필요가 없는 것이다.
😃 정리 후기
막연하게만 생각했던 내부 구조들이 이제는 조금 틀이 잡힌 느낌이다. 아직 내부구조를 다 안다고 하기에는 모르는 것이 많아서 더 학습해야 될 것 같다. 하지만 이번 기회에 실행 컨텍스트를 학습하니 클로저에 대해 학습할 준비는 어느정도 된 것 처럼 느껴진다. 다음 글은 클로저로 이어질 것 같다.