실행 컨텍스트는 실행할 코드에 관한 환경 정보들을 모아놓은 객체입니다.
실행 컨텍스트는 scope, hoisting, this, closure 등의 많은 개념을 포함하고 있습니다.
그래서 동적 언어인 JavaScript를 이해하는 핵심 개념이기도 합니다.
이 글을 통해 실행 컨텍스트의 구조 및 동작 방식에 대해 알아보겠습니다.
먼저 실행 컨텍스트가 구성되는 코드들은 다음과 같습니다.
- 전역 코드
- eval 코드
- 함수 코드
- 모듈 코드
1. 실행 컨텍스트의 구조
JavaScript 엔진은 실행 컨텍스트의 추상화된 정보들을 물리적 객체 형태로 관리합니다.
1.1 변수 객체 (VO, Variable Object)
변수 객체가 담고 있는 정보들은 다음과 같습니다.
- 변수
- 매개변수(parameter)와 인수 정보(arguments)
- 함수 선언 (함수 표현식은 제외)
변수 객체는 JavaScript 엔진에 의해 참조됩니다.
그래서 코드로 직접 접근할 수는 없습니다.
또한 변수 객체는 실행 컨텍스트의 종류에 따라 참조(바인딩)하는 값이 달라집니다.
🔎 전역 컨텍스트의 변수 객체
전역 컨텍스트의 변수 객체(VO)는 유일하며 최상위에 위치합니다.
변수 객체는 전역 객체(GO, Global Object)를 가리킵니다.
- 전역 객체(GO) : 모든 전역 변수, 전역 함수의 정보를 프로퍼티로 갖는 객체
🔎 함수 컨텍스트의 변수 객체
함수 컨텍스트의 변수 객체(VO)는 활성 객체(AO, Activation Object)를 가리킵니다.
- 활성 객체(AO) : 내부 함수, 지역 변수, arguments 객체를 프로퍼티로 갖는 객체
- arguments 객체 : 함수의 매개변수, 인수 정보를 배열의 형태로 담고 있는 객체
1.2 스코프 체인 (SC, Scope Chain)
스코프 체인은 특정 함수 또는 전역의 유효 범위를 순서대로 담고 있는 리스트입니다.
스코프 체인 리스트는 전역 객체(GO)나 활성 객체(AO)를 참조합니다.
우리는 스코프 체인을 통해 변수의 탐색 순서를 파악할 수 있습니다.
(변수가 아닌 객체의 탐색 순서는 프로토타입 체인을 통해 파악합니다.)
스코프 체인 참조 순서
먼저 1) 현재 실행 컨텍스트의 활성 객체(AO)를 처음으로 참조합니다.
이후 2) 상위 실행 컨텍스트의 활성 객체(AO)를 순서대로 참조합니다.
마지막엔 3) 전역 객체(GO)를 가리킵니다.
🔎 렉시컬 스코프 (Lexical Scope, Static Scope)
렉시컬 스코프는 함수를 어디서 호출하는지가 아닌 어디서 선언하는지에 따라 결정됩니다.
함수를 어디서 호출하는가는 스코프 결정에 어떤 영향도 주지 않습니다.
JavaScript 엔진은 스코프 체인을 토대로 렉시컬 스코프를 파악합니다.
함수 실행 중 변수를 만나면 스코프 체인은 첫 순서인 현재 활성 객체(AO)를 먼저 탐색합니다.
만약 탐색에 실패하면 다음 순서를 따라 탐색을 이어갑니다.
(이러한 탐색 방식 때문에 스코프 체인이라 불리게 됩니다.)
전역 객체(GO)까지 도달해도 탐색에 실패했다면 정의되지 않은 변수로 판단합니다.
정의되지 않은 변수에 접근하는 것이니 Reference 에러를 발생시키게 됩니다.
1.3 this value
this 프로퍼티에는 this 값이 할당됩니다.
JavaScript의 this 값은 함수 호출 방식에 따라 동적으로 결정됩니다.
자세한 내용은 이 포스팅을 참고하면 됩니다.
2. 실행 컨텍스트의 생성 과정
var x = 'xxx';
function foo () {
var y = 'yyy';
function bar () {
var z = 'zzz';
console.log(x + y + z);
}
bar();
}
foo();
다음 코드를 예시로 어떻게 실행 컨텍스트가 생성되는지 알아보겠습니다.
2.1 전역 객체(GO) 생성
가장 먼저 전역 객체(GO)가 생성됩니다.
전역 객체의 프로퍼티는 어떤 위치에서도 코드로 접근이 가능합니다.
초기 전역 객체는 빌트인 객체(Math, String, Array 등)와 BOM, DOM 등이 설정되어 있습니다.
2.2 전역 실행 컨텍스트 생성
전역 객체가 생성된 이후, 전역 코드로 진입하면 전역 실행 컨텍스트가 생성됩니다.
생성된 전역 실행 컨텍스트는 실행 컨텍스트 스택에 쌓이게 됩니다.
2.3 전역 실행 컨텍스트의 스코프 체인 생성 및 초기화
이때 스코프 체인 리스트는 전역 객체(GO)를 첫 순서로 참조합니다.
2.4 전역 실행 컨텍스트의 변수 객체화
스코프 체인의 생성과 초기화가 완료되면 변수 객체화를 실행합니다.
- 변수 객체화(Variable Instantiation) : 변수 객체(VO)에 프로퍼티와 값을 추가하는 것
🔎 변수 객체화 순서
가장 먼저 1) 변수 객체(VO)를 전역 객체(GO) 또는 활성 객체(AO)와 연결합니다.
이후엔 2) 아래의 순서대로 변수 객체(VO)에 프로퍼티와 값 쌍을 추가합니다.
- (함수 코드인 경우) 매개변수(parameter)를 프로퍼티, 인수(argument)를 값으로 가집니다.
- (코드 내의 함수 선언) 함수명을 프로퍼티, 생성된 함수 객체를 값으로 가집니다.
- (코드 내의 변수 선언) 변수명을 프로퍼티, undefined를 값으로 가집니다. (var인 경우만)
2.5 함수 foo의 선언 처리
함수 foo를 만나면 변수 객체(VO)에는 함수명 foo가 프로퍼티로, 생성된 함수 객체가 값으로 설정됩니다.
🔎 클로저 (Closure)
생성된 함수 객체는 [[Scopes]]
프로퍼티를 가집니다.
[[Scopes]]
는 현재 스코프 체인이 참조하고 있는 객체를 값으로 가집니다.
즉, 내부 함수의 [[Scopes]]
는 자신과 외부 함수와 전역 객체의 실행 환경을 가지게 됩니다.
그래서 내부 함수에서 외부 함수의 변수를 참조할 수 있게 되는 것입니다.
외부 함수의 실행 컨텍스트가 사라져도 [[Scopes]]
엔 여전히 외부 함수의 실행 환경이 존재하기 때문입니다.
이러한 원리를 클로저라고 부릅니다.
🔎 함수 호이스팅
위의 과정들은 코드가 실행되기 이전에 발생합니다.
보면 함수 선언식은 초기에 변수 객체(VO)에 할당되는 것을 볼 수 있습니다.
그래서 함수선언식은 선언문을 만나기 전부터 호출할 수 있게 됩니다.
함수 선언들이 초기에 끌어올려지는 현상을 함수 호이스팅이라 합니다.
2.6 변수 x의 선언 처리
🔎 변수의 등록 과정
변수의 등록 과정은 3단계로 나뉩니다.
- 선언 단계 : 변수 객체(VO)에 변수를 등록합니다.
- 초기화 단계 : 변수 객체(VO)에 등록된 변수를 undefined로 초기화한 후 메모리에 할당합니다.
(var로 선언된 변수는 선언 단계와 초기화 단계가 동시에 이루어집니다.) - 할당 단계 : undefined로 초기화된 변수에 실제 값을 할당합니다.
🔎 변수 호이스팅
var, let, const를 포함한 모든 선언문 (var, let, const, function, class)은 호이스팅이 발생합니다.\
하지만 var와 let, const는 초기화 단계에서 차이를 보입니다.
function do_something() {
console.log(bar); // undefined
console.log(foo); // ReferenceError
var bar = 1;
let foo = 2;
}
var는 선언 단계와 undefined로 초기화 단계가 동시에 발생합니다.
반면 let, const는 선언 단계는 발생하지만 초기화 단계는 발생하지 않습니다.
코드가 실행되어 변수 선언 코드를 만났을 때 비로소 초기화가 발생합니다.
이러한 변수 선언부터 초기화 이전까지의 구간을 TDZ(Temporal Dead Zone)라고 합니다.
이 구간에서는 초기화가 되기 전의 변수에 접근하므로 ReferenceError가 발생합니다.
2.7 this 값 결정
변수 객체화까지 끝나면 this 값을 결정합니다.
(this 값이 결정되기 이전엔 this는 전역 객체를 가리키게 됩니다.)
2.8 코드 실행
var x = 'xxx';
function foo () {
var y = 'yyy';
function bar () {
var z = 'zzz';
console.log(x + y + z);
}
bar();
}
foo();
위의 예제 코드를 다시 가져와봤습니다.
지금까지의 과정은 코드가 실행되기 이전입니다.
이제 코드를 실행합니다.
2.9 전역 변수 x에 값 할당
전역 변수 x에 문자열 ‘xxx’를 할당하는 방식을 보겠습니다.
먼저 현재 실행 컨텍스트의 스코프 체인이 참조하는 변수 객체(VO)를 순서대로 탐색합니다.
만약 변수명 x에 해당하는 프로퍼티를 발견하면 값(‘xxx’)을 할당합니다.
2.10 foo 함수 실행 컨텍스트 생성
함수 foo가 실행되면서 함수 foo의 실행 컨텍스트가 생성됩니다.
마찬가지로 스코프 체인 생성 및 초기화 → 변수 객체화 → this 값 결정 과정을 거칠 것입니다.
2.11 foo 함수 실행 컨텍스트의 스코프 체인 생성 및 초기화
함수 foo의 실행 컨텍스트가 생성되면 먼저 1) 활성 객체(AO)를 생성합니다.
이때 스코프 체인 리스트는 2) 활성 객체(AO)를 첫 순서로 참조합니다.
이후 3) Caller(전역 컨텍스트)의 스코프 체인이 참조하는 객체를 함수 foo의 스코프 체인에 푸시합니다.
2.12 foo 함수 실행 컨텍스트의 변수 객체화
함수 foo의 변수 객체(VO)는 앞에서 생긴 활성 객체(AO)를 참조합니다.
이후 변수 객체화 과정을 진행합니다.
- 함수 bar를 활성 객체(AO)에 등록합니다.
(생성과 동시에 bar 함수의[[Scopes]]
는 현재의 스코프 체인을 참조하게 됩니다. - 변수 y를 활성 객체(AO)에 등록한 뒤 undefined로 초기화합니다. (var로 선언됐기 때문)
2.13 foo 함수 실행 컨텍스트의 this 값 결정
마지막으로 함수 foo의 this 값을 결정합니다.
일반적인 함수 호출이므로 this는 전역 객체인 window가 될 것입니다.
2.14 foo 함수 실행
이제 마지막 줄의 foo 함수 실행 구문 만나 함수가 실행됩니다.
2.15 지역 변수 y 값 할당
지역 변수 y에 문자열 ‘yyy’를 할당하는 방식도 같습니다.
먼저 현재 실행 컨텍스트의 스코프 체인이 참조하는 변수 객체(VO)를 순서대로 탐색합니다.
만약 변수명 y에 해당하는 프로퍼티를 발견하면 값(‘yyy’)을 할당합니다.
2.16 내부 함수 bar의 실행 컨텍스트 생성
예시 덕분에 상당히 오래 걸렸던 포스팅이 되었습니다..
처음에 스코프, 클로저, 호이스팅의 개념을 따로 놓고 배울 땐 이해하기가 어려웠던 기억이 납니다.
하지만 실행 컨텍스트를 이해하고 보니 모든 원리들이 자연스럽고 당연하다는 생각이 듭니다.
JavaScript 개념을 암기에서 이해로 바꿀 수 있던 의미 있던 시간이었습니다. 😊
'프론트엔드 > JavaScript' 카테고리의 다른 글
[JavaScript] getElementById와 querySelector 차이점 (0) | 2022.05.03 |
---|---|
[JavaScript] JavaScript 엔진 구조 (Call Stack, Memory Heap) (0) | 2022.04.27 |
[JavaScript] 자료형 (Data Type) (0) | 2022.04.21 |
[JavaScript] 로딩 최적화 (0) | 2022.04.01 |
[JavaScript] 렌더링 최적화 (Reflow와 Repaint) (0) | 2022.03.24 |