0강. JavaScript 엔진은 코드를 어떻게 실행하는가
by
gg582 · 2026-07-09 12:57:55 · 48 views
들어가기 전에
C/C++, Python을 이미 다룰 수 있다면 JavaScript의 문법 자체는 어렵지 않습니다.
반대로 가장 어려운 부분은 JavaScript 엔진이 코드를 실행하는 방식입니다.
많은 입문서는 변수 선언과 반복문부터 설명하지만, C나 Python 개발자에게는 이미 익숙한 내용입니다. 오히려 JavaScript를 이해하는 데 필요한 것은 왜 같은 코드가 다른 언어와 다르게 동작하는가를 이해하는 것입니다.
이 강의에서는 문법을 배우지 않습니다.
JavaScript 엔진이 소스코드를 읽고, 실행 환경을 준비하며, 실제로 코드를 수행하는 과정까지를 살펴봅니다.
JavaScript는 어떤 언어인가?
JavaScript는 다음과 같은 특징을 가진 언어입니다.
- 동적 타입(Dynamically Typed)
- 가비지 컬렉션(Garbage Collection)
- 프로토타입 기반 객체 모델(Prototype-based Object System)
- 이벤트 루프(Event Loop)를 기반으로 하는 싱글 스레드 실행 모델
특히 마지막 특징은 JavaScript를 다른 언어와 구분하는 가장 중요한 요소입니다.
브라우저는 사용자 입력, 네트워크 응답, 화면 렌더링 등 수많은 이벤트를 처리해야 합니다. JavaScript는 이러한 환경에서 하나의 스레드가 매우 빠르게 작업을 전환하는 구조를 채택했습니다.
그러나 이벤트 루프를 이해하려면 먼저 JavaScript 엔진이 코드를 어떻게 실행하는지부터 알아야 합니다.
JavaScript 엔진
JavaScript 소스코드는 CPU가 직접 실행할 수 없습니다.
CPU는 기계어만 이해하므로 JavaScript를 처리하는 별도의 프로그램이 필요합니다.
이를 JavaScript Engine이라고 합니다.
대표적인 엔진은 다음과 같습니다.
| 엔진 | 사용 환경 |
|---|---|
| V8 | Chrome, Node.js |
| SpiderMonkey | Firefox |
| JavaScriptCore | Safari |
엔진은 서로 구현 방식은 다르지만 모두 ECMAScript 표준을 구현합니다.
JavaScript 엔진의 실행 과정
JavaScript 엔진은 코드를 읽자마자 한 줄씩 실행하지 않습니다.
먼저 코드를 분석하여 실행 환경을 준비한 뒤, 실제 실행을 시작합니다.
전체 과정은 다음과 같습니다.
Source Code
│
▼
Tokenizer
│
▼
Parser
│
▼
Abstract Syntax Tree (AST)
│
▼
Evaluation
│
▼
Execution
입문 단계에서는 Evaluation과 Execution 두 단계만 이해하면 충분합니다.
실행 컨텍스트(Execution Context)
JavaScript 엔진은 코드를 실행하기 전에 실행 컨텍스트(Execution Context) 를 생성합니다.
실행 컨텍스트는 현재 실행 중인 코드에 필요한 모든 정보를 저장하는 실행 환경입니다.
여기에는 다음과 같은 정보가 포함됩니다.
- 변수
- 함수
this- 외부 스코프 정보
- 현재 실행 중인 코드
프로그램이 시작되면 가장 먼저 Global Execution Context가 생성됩니다.
함수가 호출될 때마다 새로운 실행 컨텍스트가 생성되어 기존 컨텍스트 위에 쌓입니다.
이 실행 컨텍스트들은 Call Stack에서 관리됩니다.
┌──────────────┐
│ foo() │
├──────────────┤
│ Global │
└──────────────┘
함수가 종료되면 해당 실행 컨텍스트는 스택에서 제거됩니다.
이 구조는 C/C++의 함수 호출 스택과 매우 유사합니다.
Evaluation Phase
실행 컨텍스트가 생성되면 엔진은 곧바로 코드를 실행하지 않습니다.
먼저 Evaluation Phase를 수행합니다.
이 단계에서는
- 변수 선언
- 함수 선언
- 클래스 선언
등을 미리 조사하여 실행 컨텍스트 내부의 Lexical Environment에 등록합니다.
예를 들어 다음 코드를 보겠습니다.
let engine = "V8";
function foo() {
}
평가 단계에서는 아직 "V8"이라는 문자열을 대입하지 않습니다.
단지
engine이라는 식별자가 존재한다.foo()라는 함수가 존재한다.
라는 사실만 등록합니다.
이 정보를 저장하는 공간을 Environment Record라고 합니다.
Execution Context
└── Lexical Environment
│
└── Environment Record
│
├── engine
└── foo
이러한 평가 과정 때문에 JavaScript에서는 호이스팅(Hoisting) 이라는 현상이 발생합니다.
호이스팅(Hoisting)
호이스팅은 흔히
"선언문이 위로 끌어올려진다."
라고 설명되지만 실제로 코드가 이동하는 것은 아닙니다.
평가 단계에서 엔진이 선언을 먼저 등록하기 때문에 마치 선언문이 위에 있는 것처럼 보이는 현상입니다.
대표적인 예제를 살펴보겠습니다.
console.log(v8);
var v8 = "Engine";
실행 결과는
undefined
입니다.
많은 초보자는 "왜 에러가 발생하지 않을까?"라는 의문을 갖게 됩니다.
그 이유는 var가 평가 단계에서 이미 Environment Record에 등록되고 동시에 undefined로 초기화되기 때문입니다.
엔진은 개념적으로 다음과 같은 순서로 처리합니다.
Evaluation
Environment Record
v8 → undefined
↓
Execution
console.log(v8)
↓
v8 = "Engine"
따라서 선언 이전에도 변수는 존재하지만 아직 값이 대입되지 않았으므로 undefined가 출력됩니다.
let과 const
이번에는 같은 예제를 let으로 바꾸어 보겠습니다.
console.log(spiderMonkey);
let spiderMonkey = "Engine";
이번에는
ReferenceError:
Cannot access 'spiderMonkey' before initialization
이 발생합니다.
let과 const도 평가 단계에서 Environment Record에 등록됩니다.
그러나 var와 달리 초기화는 수행하지 않습니다.
평가 단계에서는
Environment Record
spiderMonkey
initialized = false
상태만 만들어집니다.
이후 실행 단계에서 선언문을 만났을 때
Initialize Binding
↓
initialized = true
↓
값 저장
이 수행됩니다.
선언문에 도달하기 전까지는 변수에 접근할 수 없습니다.
이 구간을 Temporal Dead Zone(TDZ) 라고 합니다.
따라서 let과 const 역시 호이스팅되지만, 초기화되지 않은 상태이므로 접근이 차단됩니다.
즉,
| 구분 | var |
let / const |
|---|---|---|
| 평가 단계에서 식별자 등록 | O | O |
| 평가 단계에서 초기화 | undefined |
X |
| TDZ 존재 | X | O |
| 선언 이전 접근 | 가능 | ReferenceError |
호이스팅은 JavaScript의 별도 기능이 아니라, Evaluation Phase에서 선언을 먼저 처리하는 과정의 자연스러운 결과입니다.
Execution Phase
평가가 모두 끝난 뒤 비로소 실행 단계가 시작됩니다.
이 단계에서는 코드가 위에서 아래로 순차적으로 실행됩니다.
let engine = "V8";
은 실행 단계에서
engine = "V8"
이라는 실제 값의 대입이 이루어집니다.
즉,
- Evaluation은 실행 환경을 준비하는 과정
- Execution은 실제 프로그램을 수행하는 과정
이라고 이해하면 됩니다.
식별자는 어떻게 검색되는가?
다음 코드를 보겠습니다.
let message = "Global";
function foo() {
let message = "Local";
console.log(message);
}
foo();
출력은
Local
입니다.
console.log()는 현재 실행 중인 실행 컨텍스트의 Environment Record부터 검색합니다.
현재 컨텍스트에 식별자가 없으면 자신을 둘러싼 외부 Lexical Environment로 이동하여 검색합니다.
foo()
↓
Global
이 연결 구조를 Scope Chain이라고 합니다.
JavaScript는 함수를 호출한 위치가 아니라 정의한 위치를 기준으로 스코프를 결정합니다.
이를 Lexical Scope라고 합니다.
메모리 구조
JavaScript 엔진의 메모리는 크게 두 영역으로 생각할 수 있습니다.
- Call Stack
- Heap
Call Stack에는 실행 컨텍스트와 함수 호출 정보가 저장됩니다.
Heap에는
- 객체
- 배열
- 함수 객체
- Map
- Set
등 참조 타입 데이터가 저장됩니다.
예를 들어
let array = [1, 2, 3];
은 개념적으로 다음과 같습니다.
Call Stack
array
│
▼
Heap
[1, 2, 3]
JavaScript는 힙 메모리를 직접 해제하지 않습니다.
더 이상 어떤 실행 컨텍스트에서도 참조되지 않는 객체는 가비지 컬렉터가 자동으로 회수합니다.
현대 엔진(V8 등)은 Mark-and-Sweep를 기반으로 세대별 가비지 컬렉션, 증분 수집, 병행 수집 등의 기법을 함께 사용하여 메모리를 관리합니다.
이번 강의 요약
JavaScript 엔진은 코드를 읽은 즉시 실행하지 않습니다.
먼저 실행 컨텍스트를 생성하고 Evaluation Phase를 수행하여 변수와 함수를 Environment Record에 등록합니다.
이 과정에서 선언이 먼저 처리되기 때문에 호이스팅이 발생합니다. var는 undefined까지 초기화되지만, let과 const는 초기화되지 않아 TDZ가 형성됩니다.
Evaluation이 끝난 뒤 Execution Phase가 시작되며, 이때 실제 코드가 순차적으로 실행됩니다.
실행 컨텍스트는 Call Stack에서 관리되며, 객체와 배열 같은 참조 타입 데이터는 Heap에 저장됩니다.
이러한 실행 구조를 이해하면 이후 배우게 될 스코프, 클로저, 객체, 이벤트 루프 등 JavaScript의 핵심 개념을 모두 같은 관점에서 이해할 수 있습니다.
다음 강의
다음 강의에서는 JavaScript의 데이터 모델을 살펴봅니다.
- 원시 타입과 참조 타입
- 객체의 가변성(Mutability)
- 값 전달과 참조 전달
- 얕은 복사와 깊은 복사
- 객체 공유로 인해 발생하는 대표적인 버그
이 강의를 통해 JavaScript의 객체가 왜 예상과 다르게 동작하는지 엔진의 메모리 구조 관점에서 이해하게 됩니다.