Javascript Core — AI를 잘 다루려면 내가 잘 알아야한다
실행 컨텍스트(Execution Context)는 JS 코드가 실행되는 환경 정보를 담은 객체입니다.
변수, 함수 선언, this, 스코프 체인 등 코드를 실행하는 데 필요한 모든 정보가 여기에 들어갑니다. JS 엔진은 코드를 실행할 때 항상 실행 컨텍스트 안에서 동작합니다.
실행 컨텍스트는 3가지 종류가 있습니다.
| 종류 | 생성 시점 |
|---|---|
| Global Execution Context | 스크립트가 처음 실행될 때 |
| Function Execution Context | 함수가 호출될 때마다 |
| Eval Execution Context | eval() 호출 시 (사용 지양) |
콜 스택(Call Stack) 은 이 실행 컨텍스트들을 쌓아두는 자료구조입니다. LIFO(Last In, First Out) 방식으로 동작하며, 현재 실행 중인 컨텍스트가 항상 스택의 최상단에 위치합니다.
eval() 사용을 지양해야하는 이유
보안 취약점 (Security Risk): 외부로부터 입력받은 문자열을 eval()로 실행할 경우, 악의적인 코드(Injection attack)가 실행되어 시스템이 위험해질 수 있습니다.성능 저하 (Performance Issue): 자바스크립트 엔진은 코드를 컴파일하여 최적화하는데, eval()은 런타임에 코드를 생성하므로 이러한 최적화가 불가능해 실행 속도가 느려집니다.디버깅의 어려움 (Debugging): eval() 내에서 실행되는 코드는 소스 파일에 나타나지 않아 디버깅과 에러 추적이 어렵습니다.스코프 문제 (Scope Issue): eval()은 실행 위치의 지역 변수에 접근하거나 원치 않게 변수 범위를 변경하여 의도치 않은 동작을 유발할 수 있습니다
실행 컨텍스트는 생성 단계(Creation Phase) 와 실행 단계(Execution Phase), 2단계로 나뉩니다.
실행 컨텍스트 라이프사이클
1. 생성 단계 (Creation Phase)
├── LexicalEnvironment 생성
│ ├── 환경 레코드 (Environment Record) ← let, const, 함수 선언
│ └── 외부 환경 참조 (Outer Reference) ← 스코프 체인
├── VariableEnvironment 생성
│ └── 환경 레코드 ← var 선언
└── this 바인딩 결정
2. 실행 단계 (Execution Phase)
└── 코드를 한 줄씩 실행하며 변수에 값을 할당
1. 생성 단계 — 호이스팅이 일어나는 지점
코드를 실행하기 전에, 엔진은 먼저 해당 스코프의 선언들을 훑습니다.
var → undefined로 초기화let, const → 선언만 등록, 초기화하지 않음 (TDZ)function 선언문 → 함수 전체가 메모리에 올라감console.log(a); // undefined
console.log(b); // ReferenceError: Cannot access 'b' before initialization
var a = 1;
let b = 2;
생성 단계에서 선언이 먼저 처리되는 것을 호이스팅(Hoisting)이라고 합니다.
2. 실행 단계 — 값 할당과 코드 실행
생성 단계가 끝나면 코드를 위에서 아래로 실행하면서 변수에 실제 값을 할당합니다.
var x = 10;
// 생성 단계: x → undefined
// 실행 단계: x → 10
콜 스택에 실행 컨텍스트가 쌓이고 빠지는 과정을 코드로 확인해 보겠습니다.
function first() {
console.log("first 시작");
second();
console.log("first 끝");
}
function second() {
console.log("second 시작");
third();
console.log("second 끝");
}
function third() {
console.log("third");
}
first();
함수 선언문과 함수 표현식의 호이스팅 차이도 중요합니다.
// ✅ 함수 선언문 — 생성 단계에서 함수 전체가 올라간다
hello(); // "hello"
function hello() {
console.log("hello");
}
// ❌ 함수 표현식 — 변수만 호이스팅되고 함수는 할당 전이다
bye(); // TypeError: bye is not a function
var bye = function () {
console.log("bye");
};
TDZ(Temporal Dead Zone)를 모르고 let/const를 사용하는 경우
let과 const도 호이스팅됩니다. 다만 초기화 전까지 접근할 수 없는 구간, 즉 TDZ가 존재합니다.
// ❌ TDZ에 의한 에러
const name = "outer";
function greet() {
console.log(name); // ReferenceError
const name = "inner";
}
greet();
greet 함수의 실행 컨텍스트가 생성될 때 내부의 name이 먼저 등록됩니다. 외부 name이 아니라 아직 초기화되지 않은 내부 name을 참조하기 때문에 에러가 발생합니다.
// ✅ 변수 선언을 사용 전에 배치
const name = "outer";
function greet() {
const localName = "inner";
console.log(localName); // "inner"
}
greet();
스택 오버플로우(Stack Overflow)
콜 스택에는 크기 제한이 있습니다. 종료 조건 없는 재귀 호출은 스택을 초과시킵니다.
// ❌ 종료 조건이 없는 재귀
function loop() {
loop();
}
loop(); // RangeError: Maximum call stack size exceeded
// ✅ 종료 조건을 반드시 포함
function countdown(n) {
if (n <= 0) return;
console.log(n);
countdown(n - 1);
}
countdown(5); // 출력: 5 → 4 → 3 → 2 → 1
에러 스택 트레이스 읽기
실무에서 에러가 발생하면 콜 스택 정보가 스택 트레이스로 출력됩니다. 이 트레이스가 바로 에러 발생 시점의 콜 스택 상태입니다.
function a() {
b();
}
function b() {
c();
}
function c() {
throw new Error("문제 발생!");
}
a();
// Error: 문제 발생!
// at c (index.js:8)
// at b (index.js:5)
// at a (index.js:2)
// at index.js:11
스택 트레이스는 아래에서 위로 호출 순서를 보여줍니다. 가장 위에 있는 함수가 에러가 실제로 발생한 지점이고, 아래로 갈수록 호출한 쪽입니다.
NestJS나 Express에서 에러 로그를 볼 때도 같은 원리입니다. 스택 트레이스의 최상단이 원인, 하단이 진입점입니다.
React와 실행 컨텍스트
React 컴포넌트도 결국 함수입니다. 컴포넌트가 렌더링될 때마다 해당 함수의 실행 컨텍스트가 생성됩니다.
function Counter() {
const [count, setCount] = useState(0);
// 이 함수가 호출될 때마다 새로운 실행 컨텍스트가 생성된다
// count는 해당 컨텍스트의 LexicalEnvironment에 저장된다
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
매 렌더링마다 count가 새로운 값을 가지는 이유가 여기에 있습니다. 렌더링할 때마다 새로운 실행 컨텍스트가 생성되고, 그 안에서 count는 해당 시점의 값으로 고정됩니다. 이것이 클로저(Closure)와 연결되는 부분이며, 다음 클로저 파트에서 자세히 확인해보겠습니다.
let/const도 호이스팅되지만 TDZ 때문에 선언 전 접근 시 에러가 발생한다