[JavaScript] 실행 컨텍스트란?
코어 자바스크립트 2장을 학습하며, 항상 헷갈리던 개념인 실행 컨텍스트에 대해 정리해 보았다.
실행 컨텍스트는 JS에서 정말 중요한 핵심 개념 중 하나이다. 완벽하게 이해해보자.
실행 컨텍스트란?
실행 컨텍스트(execution context)란 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다.
- 코드가 실행될 때 구성된다.
Call Stack
에 쌓인다.- 맨 위의 실행 컨텍스트와 관련된 코드들부터 실행된다.
Call Stack
— 실행 컨텍스트를 저장하는 자료구조
1. 실행 컨텍스트의 구성
실행 컨텍스트는 다음과 같은 상황에서 구성되고 Call Stack
에 쌓인다.
- 전역 공간 (해당 파일이 열려 실행되면, 자동으로
Call Stack
에 쌓인다.) - eval() 함수 실행
- 함수 실행
- 블록 생성 (ES6+)
일반적으로는 함수를 이용한 실행 컨텍스트가 주로 구성된다.
val a = 1; // 전역 컨텍스트
function outer() { // outer 실행 컨텍스트
function inner() { // inner 실행 컨텍스트
console.log(a); // undefined
var a = 3;
}
inner();
console.log(a); // 1
}
outer();
console.log(a); // 1
위와 같은 코드에서 실행 컨텍스트와 Call Stack
은 다음과 같다.
- 처음 자바스크립트 코드를 실행하는 순간 [전역 컨텍스트]
- outer 함수 호출 [전역 컨텍스트, outer 실행 컨텍스트]
- inner 함수 호출 [전역 컨텍스트, outer 실행 컨텍스트, inner 실행 컨텍스트]
- inner 함수 종료 [전역 컨텍스트, outer 실행 컨텍스트]
- outer 함수 종료 [전역 컨텍스트]
- 이외의 코드 모두 실행 완료(종료) []
스택 구조와 실행되는 함수를 연관지어 생각해보면, Call Stack
의 맨 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점임을 알 수 있다.
이렇게 어떤 실행 컨텍스트가 활성화될 때, JS 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장한다.
이러한 정보들은 다음과 같이 3가지로 분류할 수 있다.
1-1. VariableEnvironment
현재 컨텍스트 내 식별자들(변수)에 대한 정보 + 외부 환경 정보를 뜻한다.
LexicalEnvironment
의 스냅샷과 같다. 즉 최초 실행 시의 정보가 저장되고, 이후의 변경사항이 반영되지 않는다.
1-2. LexicalEnvironment
최초 실행 시 VariableEnvironment
가 복사되어 생성된다. 하지만 이후의 변경 사항이 실시간으로 반영된다.
따라서 주로 활용하는 것은 LexicalEnvironment
이다.
1-3. ThisBinding
this
식별자가 바라봐야 할 대상 객체를 뜻한다.
여기서 VariableEnvironment
와 LexicalEnvironment
는
environmentRecord
와 outer-EnvironmentReference
로 이루어져 있다.
2. environmentRecord와 호이스팅
environmentRecord
에는 현재 컨텍스트와 관련된 코드의 식별자(변수) 정보들이 저장된다.
- 매개변수 식별자
- 선언된 함수 자체
- var로 선언된 변수의 식별자 등
2-1. 호이스팅
위의 식별자 정보들은 컨텍스트 내부 전체를 처음부터 끝까지 훑어나가며 순서대로 수집된다.
여기서 호이스팅이라는 개념이 등장한다.
JS 엔진의 실제 동작 방식 대신에, ‘자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행한다’ 라고 생각해도 코드를 해석하는데 전혀 문제가 되지 않는다.
따라서 호이스팅은 자바스크립트 엔진이 코드를 실제로 끌어올리지는 않지만, 편의상 끌어올린 것 처럼 간주하자는 것이다.
2-2. 변수의 호이스팅
environmentRecord
에는 매개변수의 이름, 함수 선언, 변수명 등이 담긴다.
function a(x) {
console.log(x);
var x;
console.log(x);
var x = 2;
console.log(x);
}
a(1);
해당 코드는 다음과 같이 해석된다.
function a(x) {
var x; // 변수 x 선언, 메모리 저장 공간 확보 및 공간의 주솟값 x에 연결
var x; // 위에서 x가 선언되었으므로 무시
var x; // 위에서 x가 선언되었으므로 무시
x = 1; // x에 1 할당 = x와 연결된 메모리 공간에 숫자 1을 가리키는 메모리 주솟값 입력
console.log(x); // 1
console.log(x); // 1
x = 2; // x에 2 할당
console.log(x); // 2 = x와 연결된 메모리 공간에 숫자 2을 가리키는 메모리 주솟값으로 교체
}
a(1);
2-3. 함수의 호이스팅
함수 선언의 호이스팅은 변수 호이스팅과 약간 다르게 동작한다.
function a() {
console.log(b);
var b = "b";
console.log(b);
function b() {}
console.log(b);
}
a();
해당 코드는 다음과 같이 해석된다.
function a() {
var b; // 변수는 선언부만 끌어올린다.
function b() {} // 함수 선언은 전체를 끌어올린다.
console.log(b); // b 함수
b = "b";
console.log(b); // 'b'
console.log(b); // 'b'
}
위에서 확인할 수 있듯이, 변수는 선언부와 할당부를 나누어 선언부만 끌어올리는 반면
함수 선언은 함수 전체를 끌어올린다.
(*) 함수 선언문과 함수 표현식
함수 선언문과 함수 표현식은 호이스팅에서 각각 다른 결과를 보여준다.
먼저 함수를 정의하는 세 가지 방식은 다음과 같다.
// 1. 함수 선언문
function a() {
/* */
}
a();
// 2. (익명) 함수 표현식
var b = function () {
/* */
};
b();
// 3. 기명 함수 표현식
var c = function name() {
/* */
};
c();
name(); // error (기명 함수 표현식 외부에서는 함수명으로 함수를 호출할 수 없다.)
위의 함수 선언문과 함수 표현식이 어떤 차이를 가지고 있는지 알아보자.
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;
};
해당 예시는 다음과 같이 해석될 수 있다.
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. outer-EnvironmentReference, 스코프, 스코프 체인
3-1. 스코프(scope)
스코프란 식별자에 대한 유효 범위이다.
A의 내부에서 선언한 변수는 오직 A의 내부에서만 접근할 수 있다.
스코프의 개념은 대부분의 언어에 존재한다.
하지만 ES5까지의 JS는 특이하게도 전역 공간을 제외하면 오직 함수에 의해서만 스코프가 생성된다.
3-2. 스코프 체인(scope chain)
스코프 체인이란 식별자의 유효범위 즉 스코프를 안에서부터 바깥으로 차례로 검색해나가는 것이다.
그리고 이를 가능케 하는 것이 바로 LexicalEnvironment
의 두 번째 수집 자료인 outer-EnvironmentReference
이다.
outerEnvironmentReference
는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment
를 참조한다.
모든 코드는 실행 컨텍스트가 활성화 상태일 때 실행되기 때문에, ‘선언하다’라는 행위가 실제로 일어날 수 있는 시점이란, Call Stack
상에서 어떤 실행 컨텍스트가 활성화된 상태일 때 뿐이다.
outerEnvironmentReference
는 연결 리스트(linked list) 형태를 띈다.
따라서 구조적인 특성 때문에 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하다.
val a = 1; // 전역 컨텍스트
function outer() { // outer 실행 컨텍스트
function inner() { // inner 실행 컨텍스트
console.log(a); // ------- (2)
var a = 3;
}
inner();
console.log(a); // ------- (1)
}
outer();
console.log(a); // 1
(1)번의 console.log(a)는 어떤 값이 출력될까
첫 요소의 environmentRecord
에서 a가 있는지 찾아보고 없으면 outerEnvironmentReference
에 있는 environmentRecord
로 넘어가는 식으로 계속해서 검색한다. (스코프 체인)
따라서 첫 번째 스코프에는 a가 존재하지 않으므로, 두 번째, 즉 전역 LexicalEnvironment
에서 a를 찾아 저장된 값인 1을 반환하게 된다.
그렇다면 (2)번의 console.log(a)에서는 어떤 값이 출력될까
inner 스코프의 LexicalEnvironment
에 a 식별자가 존재하므로 스코프 체인 검색을 더 이상 진행하지 않고 즉시 inner LexicalEnvironment
상의 a를 반환하게 된다.
다만 선언부는 호이스팅 되어 a는 존재하지만, 할당부는 제자리인 console.log(a) 밑에 위치하므로, 값이 할당되지 않아 undefined를 출력하게 된다.
4. thisBinding
실행 컨텍스트의 thisBinding
에는 this로 지정된 객체가 저장된다.
이 부분에 대해서는 다음 글에 정리할 예정이다. (코어 자바스크립트 3장 완독 후)
요약
실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다.
- 전역 컨텍스트, eval() 함수 컨텍스트, 함수 실행에 의한 컨텍스트 등이 있다.
실행 컨텍스트의 객체는 활성화되는 시점에 VariableEnvironment
, LexicalEnvironment
, ThisBinding
세 가지 정보를 수집한다.
- 생성할 때
VariableEnvironment
,LexicalEnvironment
는 동일한 내용으로 구성되지만,LexicalEnvironment
는 함수 실행 도중 변경사항이 반영된다.
VariableEnvironment
와 LexicalEnvironment
는 (1) 매개변수명, 변수의 식별자, 선언한 함수의 함수명 등을 수집하는 environmentRecord
와 (2) 바로 직전 컨텍스트의 LexicalEnvironment
정보를 참조하는 outerEnvironmentReference
로 구성된다.
- 호이스팅은 끌어올린다는 의미이며,
environmentRecord
의 수집 과정을 추상화한 개념이다. - 변수 선언부와 함수 선언문에 호이스팅이 발생한다.
- 함수 표현식의 경우, 선언부만 호이스팅이 발생한다.
outerEnvironmentReference
는 상위 컨텍스트의 LexicalEnvironment
정보를 참조한다.
- 이것 때문에 변수의 유효범위인 스코프가 형성되고, 스코프 체인을 통해 상위 컨텍스트에 단방향으로 접근할 수 있다.
댓글남기기