[코어 자바스크립트] 2. 실행 컨텍스트
실행 컨텍스트의 구조와 Creation Phase, Execution Phase 흐름을 중심으로 호이스팅, TDZ, 스코프 체인을 하나의 개념으로 정리해보기
✍🏻 핵심 정리
- 실행 컨텍스트는 JavaScript 엔진이 코드를 실행하기 위한 환경 정보를 저장하는 내부 구조이다.
- JavaScript는 코드를 실행하기 전에 먼저 실행 컨텍스트를 생성하고, 변수와 함수 선언을 미리 등록한다. (Creation Phase)
- 이 과정에서 function은 바로 사용 가능하고, var는 undefined로 초기화되며, let/const는 초기화되지 않아 TDZ가 발생한다.
- 이후 실제 코드가 실행되면서 값 할당과 연산이 이루어진다. (Execution Phase)
- 변수는 항상 현재 스코프에서 먼저 찾고, 없으면 외부 스코프로 올라가며 탐색한다. (스코프 체인)
- 함수는 호출 위치가 아니라 선언된 위치 기준으로 상위 스코프가 결정된다. (Lexical Scope)
실행 컨텍스트란?
JavaScript 엔진이 코드를 실행하기 위해 필요한 환경 정보를 저장하는 내부 구조이다.
엔진은 실행 컨텍스트를 생성하여 콜스택(Call Stack)에 쌓고 코드를 실행한다.
생성 시점
| 종류 | 생성 시점 |
|---|---|
| Global Execution Context | 프로그램이 시작될 때 한 번 |
| Function Execution Context | 함수가 호출될 때 |
| eval Execution Context | eval() 함수가 실행될 때 (거의 사용되지 않음) |
내부 구조
1
2
3
4
5
Execution Context
├ LexicalEnvironment
├ VariableEnvironment
├ ThisBinding
└ PrivateEnvironment (ES2022+)
- ThisBinding:
this식별자가 참조해야 할 객체이다.- PrivateEnvironment: Class의 private field (
#field)를 관리하기 위해 추가된 환경으로, private identifier 정보를 관리한다.
1
2
3
LexicalEnvironment / VariableEnvironment
├ EnvironmentRecord
└ OuterEnvironmentReference
LexicalEnvironment (LE)
Lexical: 어휘적, 사전적인
현재 실행 컨텍스트의 식별자와 스코프 정보를 관리하는 환경이다.
- Execution Context 생성 시점에 VariableEnvironment와 동일한 Environment를 참조
- 이후 코드 실행 중 발생하는 식별자 변경 사항을 반영
VariableEnvironment
Execution Context 생성 시점의 LexicalEnvironment를 보존하는 환경이다.
with,catch,eval처럼 LexicalEnvironment가 교체되는 상황에서도 초기var선언을 보존하기 위해 존재- ES6 이후
let,const,class, block scope가 추가되면서 대부분의 스코프 관리는 LexicalEnvironment에서 이루어지게 됨
EnvironmentRecord
현재 환경에서 선언된 식별자와 바인딩 값을 저장하는 구조다.
(함수 매개변수, 함수 선언문, var, let, const, class 등)
OuterEnvironmentReference
현재 LexicalEnvironment의 외부 LexicalEnvironment를 참조하는 링크다. 이를 통해 스코프 체인이 형성된다.
함수의 경우 선언 시점의 LexicalEnvironment가 OuterEnvironmentReference로 연결된다
→ Lexical Scope(정적 스코프)
Lexical Scope (정적 스코프)
함수의 OuterEnvironmentReference는 호출 위치가 아닌 선언 시점의 위치 기준으로 결정되는 것을 말한다.
실행 단계
Creation Phase
코드를 실행하기 전에 스코프와 변수 구조를 먼저 준비하는 단계
- Execution Context 생성
- LexicalEnvironment 생성
- VariableEnvironment 설정 (초기에는 LexicalEnvironment와 동일)
this바인딩 결정- EnvironmentRecord 초기화
EnvironmentRecord 초기화 순서:
- 매개변수 (parameter binding)
- 함수 선언문 등록
var선언 등록 →undefined로 초기화let/const/class선언 등록 → 초기화되지 않음 (TDZ)
Execution Phase
Creation Phase에서 준비된 환경을 기반으로 코드를 실제로 실행하는 단계
- 변수 값 할당
- 표현식 평가
- 함수 호출
- 연산 수행
호이스팅 (Hoisting)
JavaScript에서 선언이 스코프의 최상단으로 끌어올려지는 것처럼 보이는 현상이다.
실제로는 Creation Phase에서 선언이 먼저 EnvironmentRecord에 등록되기 때문에 발생한다.
| 선언 종류 | Creation Phase 동작 | 결과 |
|---|---|---|
function 선언문 | 함수 객체 생성 후 바인딩 | 선언 이전 호출 가능 |
var | undefined로 초기화 | 선언 이전 접근 가능 |
let / const / class | 바인딩만 생성 (초기화 안됨) | TDZ 발생 |
호이스팅 우선순위
Creation Phase에서 EnvironmentRecord에 등록되는 순서이다.
- 매개변수 (parameter binding)
- 함수 선언문
var선언
동일한 이름의 식별자가 여러 방식으로 선언된 경우,
우선순위가 높은 것이 먼저 등록되어 이미 바인딩이 존재하는 경우에는 덮어쓰지 않는다.
케이스 1. 매개변수와 var가 같은 이름인 경우
1
2
3
4
5
6
7
8
9
function a(x) {
console.log(x); // ① 1
var x;
console.log(x); // ② 1
var x = 2;
console.log(x); // ③ 2
}
a(1);
- ①: 매개변수
x가var x보다 우선순위가 높아 먼저 바인딩됨. 이미 바인딩이 존재하므로var x선언은 무시됨 →1 - ②:
var x;는 선언만 있고 할당이 없으므로x의 값은 그대로 →1 - ③: Execution Phase에서
x = 2할당 →2
케이스 2. 함수 선언문과 var가 같은 이름인 경우
1
2
3
4
5
6
7
8
9
function a() {
console.log(b); // ① function b() {}
var b = "bbb";
console.log(b); // ② 'bbb'
function b() {}
console.log(b); // ③ 'bbb'
}
a();
- ①: 함수 선언문
b가var b보다 우선순위가 높아 Creation Phase에서 함수 객체가 먼저 바인딩됨 →function b() {} - ②: Execution Phase에서
b = 'bbb'할당 →'bbb' - ③:
function b() {}는 Creation Phase에서 이미 처리됨. Execution Phase에서 실행되는 코드가 아니므로b값은 그대로 →'bbb'
TDZ (Temporal Dead Zone)
let, const, class로 선언된 변수는 Creation Phase에서 바인딩은 생성되지만 초기화되지 않는다.
선언문이 실행되기 전까지의 구간을 TDZ라고 하며, 이때 변수에 접근하면 ReferenceError가 발생한다.
선언 이전에 변수를 사용하는 것을 방지하는 역할이다.
스코프 체인 (Scope Chain)
식별자를 찾을 때 외부 환경을 따라가며 탐색하는 구조이다.
1
2
3
4
5
현재 EnvironmentRecord
→ OuterEnvironmentReference
→ 외부 EnvironmentRecord
→ ...
→ Global Environment
가장 먼저 발견한 식별자를 사용하므로, 동일한 이름의 식별자가 여러 스코프에 있으면 가장 가까운 스코프의 식별자를 사용한다.
→ 이를 변수 쉐도잉 (Variable Shadowing)이라고 한다.
코드로 확인하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
console.log(a); // ① undefined
var a = 1;
console.log(a); // ② 1
function bar() {
console.log(a); // ③ undefined ← 헷갈리기 쉬운 부분
var a = 2;
console.log(a); // ④ 2
}
bar();
console.log(a); // ⑤ 1
}
foo();
실행 컨텍스트 관점
foo호출 → Creation Phase에서var a가undefined로 초기화 → ①undefined- Execution Phase에서
a = 1할당 → ②1 bar호출 → bar의 Creation Phase에서 bar 내부의var a가undefined로 초기화 → ③undefined- bar의 Execution Phase에서
a = 2할당 → ④2 - bar 종료 후 foo의 실행 컨텍스트로 복귀 → foo의
a는 여전히1→ ⑤1
스코프 체인 관점
- ③에서 착각하기 쉬운 포인트: bar 안에
var a가 있으므로 스코프 체인을 타기 전에 bar의 EnvironmentRecord에서 먼저a를 발견함 - 현재 EnvironmentRecord(bar) → OuterEnvironmentReference(foo) → Global 이지만, 현재에서 이미 찾았으므로 outer로 올라가지 않음
- 이 때문에 foo의
a = 1에 접근하지 않고 변수 쉐도잉 발생
블록 스코프 (Block Scope)
let, const, class는 블록 단위 스코프를 가진다.
블록 내부에 이 선언들이 존재하면 새로운 LexicalEnvironment가 생성되어 해당 블록 내부의 식별자를 관리한다.
블록이 끝나면 해당 LexicalEnvironment에 접근할 수 없다.
반면 var는 함수 또는 전역 스코프에 속하기 때문에 블록 스코프의 영향을 받지 않는다.
전역 변수와 지역 변수
전역 변수 (Global Variable)
전역 스코프에서 선언된 변수를 말한다.
즉, Global EnvironmentRecord에 저장되어 프로그램 전체에서 접근 가능하다.
지역 변수 (Local Variable)
함수 또는 블록 내부에서 선언된 변수를 말한다.
해당 LexicalEnvironment의 EnvironmentRecord에 저장되며 외부 스코프에서는 접근할 수 없다.
함수 선언문 vs 함수 표현식
함수 선언문 (Function Declaration)
Creation Phase에서 함수 객체가 생성되어 EnvironmentRecord에 바로 등록된다.
1
2
3
foo(); // OK
function foo() {}
함수 표현식 (Function Expression)
변수 선언만 먼저 등록되고, 함수 할당은 Execution Phase에서 이루어진다.
1
2
3
foo(); // TypeError: foo is not a function
var foo = function () {};
💼 실무 연결 포인트
var의 함수 스코프 때문에 for문 + 비동기 조합에서 버그가 자주 발생한다.
1
2
3
4
5
6
7
8
9
// 버그: 모두 3 출력
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 해결: let 사용
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
코드 작성 원칙
var대신let/const를 기본으로 사용- 변수 선언 위치를 의도적으로 관리 (선언은 사용 직전에)
- 호이스팅에 의존하는 코드를 작성하지 않음
🗣️ 면접 대비 Q&A
Q1. 호이스팅이란 무엇인가? var / let / const의 차이는?
호이스팅이란 변수나 함수 선언이 코드 상단으로 끌어올려진 것처럼 동작하는 현상입니다. 실행 컨텍스트에서 사용하는 식별자를 바인딩하는 Creation Phase와 실제 코드를 실행하는 Execution Phase로 나누어져 있어 발생하는 현상으로, Creation Phase에서 식별자가 먼저 EnvironmentRecord에 등록되기 때문입니다. var는 undefined로 초기화되지만 let과 const는 초기화가 되지 않아서 TDZ 구간이 생깁니다.
Q2. TDZ란 무엇이며 왜 존재하는가?
TDZ(Temporal Dead Zone)란 let, const, class 선언이 실행 컨텍스트 생성 시 EnvironmentRecord에 바인딩은 되었지만 값이 초기화되지 않은 상태입니다. 이때 변수에 접근하면 ReferenceError가 발생하여 선언 이전에 변수를 사용하는 것을 방지합니다.
Q3. 스코프 체인이란 무엇인가? 탐색 순서는?
스코프 체인이란 변수를 찾을 때 현재 실행 컨텍스트의 EnvironmentRecord부터 시작하여 OuterEnvironmentReference를 통해 외부 실행 컨텍스트의 LexicalEnvironment를 참조하는 과정을 반복하는 구조입니다. 전역 실행 컨텍스트까지 탐색하며, 가장 먼저 찾은 변수를 사용하게 되며 이로 인해 더 넓은 스코프의 변수 대신 가장 가까운 변수를 사용하는 것을 변수 쉐도잉이라고 합니다.
Q4. Lexical Scope란 무엇인가?
Lexical Scope란 함수를 실행할 때 호출된 위치의 환경이 아니라, 선언한 시점의 환경을 참조하는 것입니다.
💡 최종 인사이트
JavaScript 코드는 “선언을 먼저 처리하고, 이후 실행한다”는 흐름으로 동작한다.
이 과정에서 변수와 함수는 실행 전에 미리 등록되기 때문에 호이스팅이 발생하고, let/const는 초기화 시점 차이로 TDZ가 생긴다.
또한 변수 탐색은 현재 스코프에서 시작해 외부로 확장되며, 함수는 선언된 위치 기준으로 스코프가 결정된다.
결국 실행 컨텍스트를 이해하면 “이 코드가 왜 이 값이 나오는지”를 순서대로 추적할 수 있다.