📍 소스코드 평가와 실행
모든 소스코드는 실행에 앞서 평가 과정을 거치며, 코드를 실행하기 위한 준비를 한다. 즉 자바스크립트 엔진은 소스코드를 2개의 과정 ('소스코드 평가' , '소스코드 실행') 으로 나누어 처리한다.
1. 소스코드 평가 과정
실행 컨텍스트를 생성하고 변수, 함수 등의 선언문만 먼저 실행하여 생성된 변수나 함수 식별자를 키로 실행 컨텍스트가 관리하는 스코프(렉시컬 환경의 환경 레코드)에 등록한다.
2. 소스코드 실행 과정
소스코드 평가 과정이 끝나면 비로소 선언문을 제외한 소스코드가 순차적으로 실행된다. 즉, 런타임이 시작되어 소스코드 실행에 필요한 정보 (변수,함수의 참조)를 실행 컨텍스트가 관리하는 스코프에서 검색하여 취득한다. 그리고 변수 값의 변경 등 소스코드 실행 결과는 다시 실행 컨텍스트가 관리하는 스코프에 등록된다.
예시
var x;
x = 1;
위와 같은 코드가 있을 때 자바스크립트 엔진은 아래의 두번의 과정으로 나누어서 처리한다.
1. 평가과정: 변수 선언문 var x;
를 먼저 실행하여 생성된 변수 식별자 x가 실행 컨텍스트가 관리하는 스코프에 등록되고, undefined
로 초기화 된다.
2. 실행과정 : 변수 할당문 x = 1;
이 실행되며, 이 때 먼저 x 변수가 선언된 변수인지 확인하기 위해 실행 컨텍스트가 관리하는 스코프에 x변수가 등록되어 있는지 확인한다. 이 후 할당된 결과를 실행 컨텍스트에 등록하여 관리한다.
📍 소스코드의 타입
소스코드를 아래의 4가지 타입으로 구분하는 이유는, 소스코드의 타입에 따라 실행 컨텍스트를 생성하는 과정과 관리 내용이 다르기 때문이다.
1. 전역 코드
- 전역에 존재하는 소스코드를 말하며, 전역에 정의된 함수/클래스 등의 내부 코드는 포함되지 않는다.
- 전역 변수를 관리 하기 위해 최상위 스코픤 전역 스코프를 생성해야한다.
- var 키워드로 선언된 전역 변수와 함수 선언문으로 정의된 전역 함수를 전역 객체의 프로퍼티와 메서드로 바인딩하고 참조하기 위해 전역 객체와 연결되어야 한다.
2. 함수 코드
- 함수 내부에 존재하는 소스코드를 말하며, 함수 내부에 중첩된 함수/클래스 등의 내부 코드는 포함되지 않는다.
- 지역 스코프를 생성하고 지역 변수, 매개변수, arguments 객체를 관리해야 한다.
- 생성한 지역 스코프를 전역 스코프에서 시작하는 스코프 체인의 일원으로 연결해야한다.
3. eval 코드
- 빌트인 전역 함수인 eval 함수에 인수로 전달되어 실행되는 소스코드를 말한다.
- strict mode(엄격모드)에서 자신만의 독자적인 스코프를 생성한다.
4. 모듈 코드
- 모듈 내부에 존재하는 소스코드를 말하며, 모듈 내부의 함수/클래스 등의 내부 코드는 포함되지 않는다.
- 모듈별로 독립적 모듈 스코프를 생성한다.
📍 실행 컨텍스트
실행 컨텍스트의 역할
- 전역 코드 평가
- 전역 코드 실행
- 함수 코드 평가
- 함수 코드 실행
위와 같은 일련의 과정을 거치기 위해서 스코프, 식별자, 코드 실행 순서 등의 관리가 필요하다.
- 선언에 의해 생성된 모든 식별자(변수,함수,클래스 등)를 스코프를 구분하여 등록하고 상태변화(식별자에 바인딩 된 값의 변화)를 지속적으로 관리 할 수 있어야 한다.
- 스코프는 중첩 관계에 의해 스코프 체인을 형성해야 한다. 이를 통해 체인으로 상위 스코프로 이동하여 식별자를 검색 할 수 있어야 한다.
- 현재 실행 중인 코드의 실행 순서 변경을 할 수 있어야 하며 다시 되돌아 갈 수 있어야 한다.
즉, 실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아 놓은 객체를 말하며, 소스코드를 실행하는데 필요한 환경을 제공하고, 코드의 실행 결과를 실제로 관리하는 영역을 말한다.
- 실행 컨텍스트는 식별자를 등록하고, 관리하는 스코프와 실행 순서 관리를 구현한 내부 메커니즘으로, 모든 코드는 실행 컨텍스트를 통해 실행 / 관리 된다.
- 식별자와 스코프는 실행 컨텍스트의 렉시컬 환경으로 관리하고, 코드 실행 순서는 실행 컨텍스트 스택으로 관리한다.
필요한 환경 정보
- 변수 : 전역변수, 지역변수, 매개변수, 객체의 프로퍼티
- 함수 선언
- 변수 유효 범위 - Scope
- This
실행 컨텍스트 스택
아래 예시를 통해 실행컨텍스트가 어떻게 구성되는지 알아보자.
function func1() {
let a = 1
function fun2() {
console.log('함수 실행 컨텍스트2')
}
console.log('함수 실행 컨텍스트1')
func2()
}
console.log('글로벌 실행 컨텍스트')
func1()
- 가장 먼저 코드 실행전 글로벌 실행 컨텍스트가 쌓이고, '글로벌 실행 컨텍스트' 가 콘솔에 찍힌다.
- 이후 func1() 함수를 호출함으로써 func1 함수의 실행 컨텍스트가 쌓이고 '함수 실행컨텍스트 1' 가 콘솔에 찍힌다.
- func1() 함수 내부에서 func2() 함수를 호출함으로써, func2() 함수 실행 컨텍스트가 쌓이고 '함수 실행 컨텍스트 2' 가 콘솔에 찍힌다.
- func2() 함수가 종료됨으로써 func2() 함수 실행 컨텍스트가 콜스택에서 빠진다.
- func1() 함수가 종료됨으로써 func1() 함수 실행 컨텍스트가 콜스택에서 빠진다.
- 모든 코드를 다 실행하여 스크립트 맨 아래에 도달했으므로 글로벌 실행 컨텍스트가 콜스택에서 빠지고, 코드가 종료된다.
예시를 통해 알 수 있는 것
- 실행 컨텍스트 스택은 코드의 실행 순서를 관리한다.
- 컨텍스트를 구성하는 것은 함수를 실행할 때와 동일 시점이다.
- 어떠한 실행 컨텍스트가 활성화 될 때, 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는데 필요한 환경정보를 수집하여 실행 컨텍스트 객체에 저장한다.
📍실행 컨텍스트의 종류
1. Variable Environment (VE)
- 현재 컨텍스트 내의 식별자 정보 및 외부 환경정보 (Outer Environment Reference)를 가진다.
- 선언 시점의 LE의 스냅샷으로 추후에 변경사항이 반영되지 않는다.
- 실행 컨텍스트를 생성 할 때, VE에 먼저 정보를 담은후 이를 그대로 복사하여 LE 를 만든다.
- 이후에는 LE를 주로 사용한다.
2. Lexical Environment (LE)
- 식별자와 식별자에 바인딩된 값, 그리고 상위 스코프에 대한 참조를 기록하는 자료구조이다.
- 처음에는 VE 와 같지만, 변경 사항이 실시간으로 반영 된다.
- LE 객체의 프로퍼티로는 🟡 Environment Record와 🟡 Outer Environment Reference이 있다.
🟡 Outer Environment Reference (외부 렉시컬 참조)
- 중첩 유효범위를 가질 수 있는 환경에서 상위의 Lexical Environment를 참조한다.
- 글로벌 컨텍스트의 경우 null을 참조한다.
- Outer Environment Reference를 통해
스코프 체인이 연결
된다.
🟡 Environment Record (환경 레코드)
-
식별자들의 바인딩을 기록하는 객체로서 변수, 함수 등이 기록된다.
-
현재 컨텍스트와 관련된 코드의 식별자 정보
가 저장 되며 , 컨텍스트 내부 전체를 순서대로 수집한다.
-
따라서 코드가 실행되기도 전에 자바스크립트 엔진이 이미 해당 환경에 속해있는 코드의 변수명을 모두 알게 된다==호이스팅
-
Environment Record는 ✔️ Declarative Environment Record
와 ✔️ Object Environment Record
그리고 ✔️ Global Environment Record
로 구성되어 있다.
✔️ Declarative Environment Record (DER)
- 변수 선언과 함수 선언을 저장한다.
- 자식으로는 1) Function Environment Record와 2) Module Environment Record가 있다.
1) Function Environment Record
- DER 내부의 Function Environment Record 에는 new.target, super, this에 대한 정보를 갖는다.
- 이전에는 This binding을 실행 컨텍스트에서 관리했다면, ES6 이후 부터는 This binding을 Environment Record 내부인 DER => Function Environment Record 에서 관리한다.
2) Module Environment Record
- DER 내부의 Module Environment Record 에는
상위 레벨에서 정의된 모듈에 대한 바인딩을 저장
한다.
- 또한 모듈에서 명시적으로 import 된 바인딩도 포함한다.
- Module Environment Record의 [[OuterEnv]] 는 Global Environment Record 이다.
✔️ Object Environment Record (OER)
- 글로벌 환경에 대한 Lexical Environment는 Object Environment Record를 포함한다.
- OER는 글로벌 객체를 기록한다.
✔️ Global Environment Record (GER)
- 가장 바깥 스코프인 글로벌 스코프를 정의를 하는데 사용된다.
- Glboal Environment Record의 [[OuterEnv]] 는 null 이다.
📍 실행컨텍스트 생성 과정
실행 컨텍스트의 객체 종류를 알아보았으니, 그에 따라서 실행 컨텍스트의 생성 과정을 더 자세히 알아보도록 하겠다.
실행 컨텍스트는 위에서도 알아봤듯이, 실행 가능한 코드를 실행하기 위해 필요한 환경이다. 이 때 실행 가능한 코드가 실행될때 실행하기 위해 필요한 환경 (실행 컨텍스트) 는 평가단계와 실행단계로 생성된다.
컴파일러는 실제 코드를 '실행(2단계)' 하기 전에 코드를 두번 실행해준다.
1. 첫번째 실행 : 모든 함수 선언을 선택하여 참조와 함께 메모리에 저장한다.
2. 두번째 실행 : 모든 변수를 선택하여 데이터에 undefined를 할당한다. 이 때 함수선언과 변수선언 사이의 충돌이 일어날 시 변수는 무시된다.
두번 실행을 통해서 Variable Environiment 와 Lexical Environment가 생성된다. 생성된 Lexical Environment는 다음과 같은 일련의 과정을 거친다.
1. Environment Record를 초기화한다.
2. This를 바인딩한다.
3. 바깥 환경을 참조해 스코프 체인을 생성한다.
변수 호이스팅과 TDZ
ES6에 도입된 let과 const 는 var와 달리 TDZ가 생성되므로, 변수 선언 이전에는 참조 할 수 없다. 이는 마치 직관적으로 ' 호이스팅 '을 막아놓은 것 처럼 보이지만, 실제적으로 호이스팅을 막은게 아니라 TDZ라는 개념을 추가한 것 뿐이다.
함수 선언식과 함수 표현식
- 함수 선언식은 함수 호이스팅으로 인해 함수 전체를 호이스팅하지만, 표현식은 변수 호이스팅으로 선언부만 호이스팅한다.
- 따라서 표현식은 함수 선언 이전에 함수를 호출하면 '~ is not a function' 이라는 에러를 띄운다.
📍 전역 실행 컨텍스트 생성 과정
1. 전역 객체 생성
전역 객체는 전역 코드가 평가 되기 이전에 생성된다. 전역 객체에는 빌트인 전역 프로퍼티와 빌트인 전역 함수, 그리고 표준 빌트인 객체가 추가되며, 동작 환경에 따라 클라이언트 사이드 Web API 또는 특정 환경을 위한 호스트 객체를 포함한다.
2. 전역 코드 평가
- 전역 실행 컨텍스트를 생성한다.
- 전역 렉시컬 환경을 생성한다.
2-1. 전역 환경 레코드를 생성한다.
2-1-1. 객체 환경레코드를 생성한다.
2-1-2. 선언적 환경 레코드를 생성한다.
2-2. this 바인딩을 한다.
2-3. 외부 렉시컬 환경에 대한 참조를 결정한다.
1. 전역 실행 컨텍스트 생성
비어있는 전역 실행 컨텍스트를 생성하여 실행 컨텍스트 스택에 푸시한다.
2. 전역 렉시컬 환경 생성
전역 렉시컬 환경을 생성하고 전역 실행 컨텍스트에 바인딩한다. 렉시컬 환경은 환경레코드 (Environment Record) 와 외부 렉시컬 환경에 대한 참조 (Outer Lexical Environment Reference)로 구성된다.
2-1. 전역 환경 레코드를 생성한다.
- 전역 변수를 관리하는 전역 스코프, 전역 객체의 빌트인 전역 프로퍼티와 빌트인 전역 함수, 표준 빌트인 객체를 제공한다.
- ES6의 let, const 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 되지 않고 개념적인 블록 내에 존재한다.
2-1-1. 객체 환경 레코드 (Object Environment Record)를 생성한다.
- 기존의 전역 객체가 관리하던 var 키워드 로 선언한 전역 변수와 함수 선언문으로 정의한 전역 함수, 빌트인 전역 프로퍼티와 빌트인 전역 함수, 표준 빌트인 객체를 관리한다.
- BindingObject라고 부르는 객체와 연결되며 이는 전역 객체 생성에서 생성된 전역 객체이다.
- 전역 코드 평가 과정에서 var 키워드로 선언한 전역 변수와 함수 선언문으로 정의된 전역 함수는 전역 환경 레코드의 객체 환경 레코드에 연결된 BindingObject를 통해 전역 객체의 프로퍼티와 메서드가 된다.
- 이에 따라 var 키워드로 선언된 전역 변수와 함수 선언문으로 정의된 전역 함수가 전역 객체를 가리키는 식별자 (window) 없이 전역 객체의 프로퍼티를 참조 할 수 있는 메커니즘이다.
- 따라서 var 키워드로 선언한 변수는 코드 실행단계에서 선언문 이전에도 참조 할 수 있다. (호이스팅 원리)
2-1-2. 선언적 환경 레코드 (Declarative Environment Record)를 생성한다.
- let, const 키워드로 선언한 전역 변수를 관리한다.
- 객체 환경 레코드와 달리, let , const 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 되지 않는다.
- const 키워드로 선언한 변수는 "선언 단계"와 "초기화 단계"가 분리되어 진행한다. 따라서, 초기화 단계에 도달 하기 전까지 TDZ가 적용된다.
2-2. This Binding을 한다.
전역 환경 레코드에 this가 바인딩 된다. 이 때 전역 객체가 바인딩된다.
2-3. 외부 렉시컬 환경에 대한 참조 결정
전역 코드를 포함하는 소스코드는 없으므로 전역 겕시컬 환경의 외부 렉시컬 환경에 대한 참조에 null이 할당된다.
📍 함수 실행 컨텍스트 생성 과정
1. 코드 제어권 이동
함수 호출 시 전역 코드의 실행을 일시 중단하고, 호출된 함수의 내부로 코드의 제어권이 이동한다.
2. 함수 코드 평가
- 함수 실행 컨텍스트를 생성한다.
- 함수 렉시컬 환경을 생성한다.
2-1. 함수 환경 레코드를 생성한다.
2-2. this 바인딩
2-3. 외부 렉시컬 환경에 대한 참조를 결정한다.
1. 함수 실행 컨텍스트를 생성한다.
생성된 함수 실행 컨텍스트는 함수 렉시컬 환경이 완성된 다음 실행 컨텍스트 스택에 푸시 된다.
2. 함수 렉시컬 환경을 생성한다.
함수 렉시컬 환경을 생성하고, 위에서 생성된 실행 컨텍스트에 바인딩한다.
2-1. 함수 환경 레코드를 생성한다.
- 매개변수, arguments객체, 함수 내부에서 선언한 지역변수와 중첩 함수를 등록하고 관리한다.
2-2. this 바인딩
- 함수 호출 방식에 따라 this에 바인딩 될 객체가 달라진다.
2-3. 외부 렉시컬 환경에 대한 참조를 결정한다.
- 함수가 호출된 시점이 아니라, 정의된 시점에 따라 외부 렉시컬 환경이 결정된다.
📍 스코프 체인
Lexical Environment 중에서 Outer Environment Reference에 의해서 형성되는 스코프 체인에 대해 알아보도록 하겠다.
스코프 ?
- 스코프란 식별자에 대한 유효범위를 말한다.
- 즉, 전체 코드 내에서 특정 변수에 접근 할 수 있는 범위를 일컫는다.
스코프의 종류
- 글로벌 스코프 : 코드에서 함수 내부나 블록 내부에 있지 않는 변수를 '글로벌 스코프'에 있다고 한다. 글로벌 스코프는 코드 모든 곳에서 접근 가능하다.
var greeting = 'hello!'
function greet() {
console.log(greeting)
}
greet()
- 함수 스코프 : 함수 내부에 선언된 변수를 말한다. 함수 내부에서만 접근 가능하다.
function greet() {
var greeting = 'hello!'
console.log(greeting)
}
greet()
console.log(greeting)
- 블록 스코프 : let과 const 에서만 적용되는 스코프로서, 블록' { } ' 에 의해 생성되는 스코프이다. 블록 내부에서 선언된 let , const 변수는 외부에서 접근 불가능하다.
- 블록 스코프가 포함된 코드 블록이 실행되면, 해당 코드 블록을 위한 블록 레벨 스코프를 생성한다. 이를 위해 선언적 환경 레코드 (DER)를 갖는 렉시컬 환경을 새롭게 생성하여 기존의 전역 렉시컬 환경을 교체한다. 이 때 새롭게 생성된 렉시컬 환경의 외부 렉시켤 환경에 대한 참조는 해당 코드 블록이 실행되기 이전의 전역 렉시컬 환경을 가리킨다.
{
let greeting = 'hello!'
var foo = 'bar'
}
console.log(foo)
console.log(greeting)
스코프 체인이란 ?
-
자바스크립트 엔진이 현재 스코프 내부에서 특정 변수를 찾지 못하면 한단계 바깥 스코프로 나가 해당 변수를 찾는 것을 말한다. 해당 변수를 찾을 때 까지 계속해서 한단계 바깥으로 나간다.
-
여러 스코프에서 동일한 식별자를 선언한 경우, 무조건 스코프 체니상에서 가장 먼저 발견된 식별자에만 접근 가능하다.
변수은닉화
: 함수 내부와 글로벌 공간에서 동일한 식별자를 선언했다면, 함수 내부에서는 오로지 내부에 선언된 변수에만 접근 가능
- Lexical Environment 가 생성될 때, Outer Environment Reference에 외부 스코프의 Lexical Environment를 참조한다.
클로저
: 외부 함수의 실행 컨텍스트가 스택에서 사라지더라도 Outer Environment Reference가 가리키는 외부 함수의 실행 환경은 소멸되지 않고 참조 가능하다.
- 즉, 외부 함수의 실행 컨텍스트가 소멸되어도 내부 함수의 스코프 체인에 의해 참조되고 있으므로 내부 함수에서 접근 가능하다.
스코프 체인 예시
var name = 'voldMort'
function Outer() {
var name = 'Tom Mavolo Riddle'
function Inner() {
var name = 'You Know Who'
console.log(name)
}
Inner()
console.log(name)
}
Outer()
console.log(name)
위의 예시는 name이라는 같은 식별자를 가진 변수를 1. 글로벌 스코프 2. outer 함수 스코프 3. inner 함수 스코프에서 각각 선언했다. 이를 통해서 각 스코프 내부에 선언된 name에만 접근 할 수 있음을 알 수 있다.
참고
코어 자바스크립트 - 정재남 저
모던 자바스크립트 Deep Dive - 이웅모 저
[Javascript] Execution Context와 Lexical Environment
ECMAScript® 2021
What is the Temporal Dead Zone (TDZ) in JavaScript?
Understanding Scope and Scope Chain in JavaScript
자바스크립트 함수(3) - Lexical Environment