About→

← Studies

03. JavaScript 실행

JavaScript 코드는 어떻게 실행될까?

앞에서 브라우저가 HTML과 CSS를 받아서 화면을 만드는 과정을 살펴봤다.

text
HTML
 ↓
DOM

CSS
 ↓
CSSOM

DOM + CSSOM
 ↓
Render Tree
 ↓
Layout
 ↓
Paint
 ↓
Composite
 ↓
화면

그렇다면 JavaScript는 어디에서 실행될까?

JavaScript 파일을 브라우저가 읽었다고 해서 코드가 바로 위에서부터 단순하게 실행되는 것은 아니다.

JavaScript가 실행되기 위해서는 먼저 실행 환경이 만들어지고, 그 안에서 코드를 실행한다.

이 과정을 이해하기 위해 알아야 하는 핵심 개념이

text
실행 컨텍스트
↓
생성 단계 / 실행 단계
↓
호이스팅
↓
Call Stack
↓
Heap

이다.

이 개념들을 따로 외우기보다,

"JavaScript 코드가 실행될 때 브라우저와 JavaScript 엔진 내부에서 무슨 일이 일어나는가?"

라는 관점에서 살펴보자.

1. JavaScript 엔진

먼저 JavaScript가 실행되는 장소부터 알아보자.

브라우저에는 JavaScript를 실행할 수 있는 JavaScript 엔진이 들어 있다.

대표적으로 Chrome과 Edge는 V8 엔진을 사용한다.

JavaScript 코드를 작성하면 브라우저는 이 코드를 JavaScript 엔진을 통해 실행한다.

jsx
const name = "Yonghee";

function sayHello() {
  console.log(`Hello ${name}`);
}

sayHello();

JavaScript 엔진은 이 코드를 읽고 실행하면서 변수와 함수 등을 관리해야 한다.

그런데 단순히

text
위에서부터 한 줄씩 실행

만으로는 충분하지 않다.

함수가 호출되면 함수 내부에서 사용할 변수도 관리해야 하고,

함수가 끝나면 다시 원래 실행하던 위치로 돌아와야 한다.

예를 들어

jsx
const name = "Yonghee";

function sayHello() {
  const message = `Hello ${name}`;
  console.log(message);
}

sayHello();

실행 과정은 대략 다음과 같다.

text
전역 코드 실행
    ↓
sayHello 함수 호출
    ↓
sayHello 함수 실행 환경 생성
    ↓
함수 내부 코드 실행
    ↓
함수 종료
    ↓
전역 코드로 돌아감

JavaScript는 이런 실행 환경을 관리하기 위해 Execution Context(실행 컨텍스트)라는 개념을 사용한다.

2. 실행 컨텍스트(Execution Context)

실행 컨텍스트는 쉽게 말하면

JavaScript 코드가 실행되기 위해 필요한 정보를 담아놓은 실행 환경

이라고 생각하면 된다.

예를 들어 함수가 실행되면 JavaScript는 그 함수 안에서

jsx
function sayHello() {
  const message = "Hello";

  console.log(message);
}

어떤 변수를 사용할 수 있는지,

어떤 외부 변수에 접근할 수 있는지,

this는 무엇인지 등을 알아야 한다.

이런 실행에 필요한 정보들을 관리하는 단위가 실행 컨텍스트다.

대표적으로 다음과 같은 실행 컨텍스트가 있다.

text
Global Execution Context
Function Execution Context
Eval Execution Context

실무에서 가장 중요한 것은

text
전역 실행 컨텍스트
함수 실행 컨텍스트

이다.

3. JavaScript를 실행하면 가장 먼저 전역 실행 컨텍스트가 만들어진다

JavaScript 코드가 실행되면 가장 먼저 전역 실행 컨텍스트가 만들어진다.

예를 들어

jsx
const name = "Yonghee";

console.log(name);

를 실행하면 개념적으로 다음과 같은 과정이 시작된다.

text
JavaScript 실행
      ↓
Global Execution Context 생성
      ↓
전역 코드 실행

그런데 실행 컨텍스트는 만들어지자마자 모든 코드를 바로 실행하는 것이 아니다.

크게 두 단계로 나누어 생각할 수 있다.

text
실행 컨텍스트 생성
      ↓
생성 단계
      ↓
실행 단계

이 두 단계를 이해하면 호이스팅도 자연스럽게 이해할 수 있다.

4. 생성 단계와 실행 단계

생성 단계(Creation Phase)

먼저 JavaScript 엔진은 코드를 실제로 실행하기 전에 실행 환경을 준비한다.

개념적으로 보면

text
실행 컨텍스트 생성
        ↓
┌───────────────────┐
│ 생성 단계         │
│                   │
│ 변수/함수 바인딩 준비 │
│ Lexical Environment │
│ this 결정          │
│ 외부 환경 연결      │
└───────────────────┘
        ↓
실행 단계

이때 중요한 것은

코드를 실행하기 전에 실행에 필요한 환경을 먼저 준비한다는 것

이다.

예를 들어

jsx
console.log(name);

const name = "Yonghee";

를 생각해보자.

실행 순서만 보면 name을 선언하기 전에 사용하고 있다.

하지만 JavaScript 엔진은 코드를 실행하기 전에 실행 환경을 준비한다.

여기서 let, const는 선언이 준비되더라도 초기화되기 전까지 접근할 수 없기 때문에 ReferenceError가 발생한다.

즉,

text
선언을 코드 위로 실제 이동시킨다

라고 이해하면 안 된다.

이것이 바로 호이스팅(Hoisting)을 이해할 때 중요한 부분이다.

5. 호이스팅(Hoisting)

호이스팅은 흔히

"변수와 함수 선언이 코드의 위로 끌어올려진다."

라고 설명한다.

하지만 실제로 코드가 물리적으로 위로 이동하는 것은 아니다.

호이스팅은

JavaScript 엔진이 코드를 실행하기 전에 선언에 대한 정보를 준비하는 현상

으로 이해하는 것이 더 정확하다.

var의 경우

jsx
console.log(name);

var name = "Yonghee";

실행하면

text
undefined

가 출력된다.

개념적으로 보면 var는 선언과 동시에 초기화가 준비되기 때문이다.

jsx
var name;
console.log(name);

name = "Yonghee";

와 비슷한 결과가 나타난다.

중요한 것은 실제 코드가 이렇게 변경되는 것은 아니라는 점이다.

let과 const

이번에는

jsx
console.log(name);

let name = "Yonghee";

를 실행해보자.

이번에는

text
ReferenceError

가 발생한다.

let과 const도 선언 자체는 실행 전에 처리되지만 초기화되기 전에는 접근할 수 없다.

이 구간을 TDZ(Temporal Dead Zone)라고 한다.

개념적으로 보면

text
let name 선언
      ↓
TDZ
      ↓
name 초기화
      ↓
사용 가능

이다.

그래서

jsx
console.log(name); // ReferenceError

let name = "Yonghee";

가 된다.

6. 함수 선언식도 호이스팅된다

함수 선언식은 조금 다르다.

jsx
sayHello();

function sayHello() {
  console.log("Hello");
}

이 코드는 정상적으로 실행된다.

왜냐하면 함수 선언식은 실행 전에 함수에 대한 정보를 준비하기 때문이다.

개념적으로 보면

text
실행 컨텍스트 생성
        ↓
sayHello 함수 등록
        ↓
코드 실행
        ↓
sayHello() 호출

과 같은 흐름으로 이해할 수 있다.

반면 함수 표현식은 다르다.

jsx
sayHello();

const sayHello = function () {
  console.log("Hello");
};

const 변수에 함수가 할당되는 형태이기 때문에 sayHello가 초기화되기 전에 접근하게 되어 에러가 발생한다.

즉,

jsx
function sayHello() {}

와

jsx
const sayHello = function () {}

은 호이스팅 관점에서도 다르게 동작한다.

7. 실행 단계(Execution Phase)

생성 단계에서 실행 환경이 준비되면 이제 실제 코드가 실행된다.

jsx
const name = "Yonghee";

console.log(name);

개념적으로 보면

text
[생성 단계]

name이라는 식별자를 위한 환경 준비
        ↓
[실행 단계]

name = "Yonghee"
        ↓
console.log(name)

처럼 생각할 수 있다.

여기서 중요한 것은

생성 단계에서 실행에 필요한 환경을 준비하고, 실행 단계에서 실제 값을 계산하고 코드를 실행한다.

는 것이다.

이제 함수가 호출되면 어떻게 되는지 살펴보자.

8. 함수를 호출하면 새로운 실행 컨텍스트가 만들어진다

다음 코드를 보자.

jsx
const name = "Yonghee";

function sayHello() {
  const message = `Hello ${name}`;

  console.log(message);
}

sayHello();

처음에는 전역 실행 컨텍스트가 존재한다.

text
Global Execution Context

그리고 마지막 줄에서

jsx
sayHello();

가 실행된다.

그러면 sayHello를 실행하기 위한 새로운 실행 컨텍스트가 만들어진다.

text
Global Execution Context
        ↓
sayHello()
        ↓
Function Execution Context

즉,

함수가 호출될 때마다 해당 함수의 실행을 위한 실행 컨텍스트가 만들어진다.

라고 이해하면 된다.

그리고 이 실행 컨텍스트들은 아무렇게나 관리되는 것이 아니라 Call Stack에서 관리된다.

9. Call Stack

Call Stack은 이름 그대로 호출된 함수들의 실행 순서를 관리하는 스택(Stack)이다.

스택은

text
LIFO
Last In, First Out

구조다.

쉽게 말하면

가장 나중에 들어온 것이 가장 먼저 나온다.

예를 들어

jsx
function first() {
  second();
}

function second() {
  third();
}

function third() {
  console.log("third");
}

first();

를 실행하면 다음과 같은 흐름이 만들어진다.

text
first()
  ↓
second()
  ↓
third()

Call Stack에서는

text
┌─────────┐
│ third   │ ← 가장 먼저 종료
├─────────┤
│ second  │
├─────────┤
│ first   │
├─────────┤
│ global  │
└─────────┘

처럼 쌓인다.

third()가 종료되면 빠지고,

text
third 제거

그다음 second()가 종료되면

text
second 제거

마지막으로 first()가 종료된다.

text
third
 ↓
second
 ↓
first
 ↓
global

이렇게 실행 흐름을 관리한다.

10. Call Stack이 중요한 이유

브라우저에서 JavaScript가 실행되는 과정을 이해할 때 Call Stack은 매우 중요하다.

왜냐하면 JavaScript의 일반적인 코드 실행은 동기적으로 하나의 실행 흐름에서 진행되기 때문이다.

예를 들어

jsx
console.log("A");

console.log("B");

console.log("C");

라면

text
A
↓
B
↓
C

순서대로 실행된다.

그런데 하나의 함수가 매우 오래 실행된다면 어떻게 될까?

jsx
function heavyWork() {
  for (let i = 0; i < 10_000_000_000; i++) {
    // 무거운 작업
  }
}

heavyWork();

Call Stack에서

text
heavyWork()

가 끝나지 않는 동안 다음 JavaScript 코드를 실행할 수 없다.

그리고 브라우저의 Main Thread에서 실행되는 작업이라면 사용자 입력이나 화면 업데이트에도 영향을 줄 수 있다.

이 문제는 뒤에서 다룰 Event Loop와 Web Worker와 연결된다.

11. Call Stack은 어떻게 쌓이고 빠지는가?

조금 더 자세히 보면

jsx
function a() {
  b();
}

function b() {
  c();
}

function c() {
  console.log("Hello");
}

a();

실행 과정은 다음과 같다.

① 전역 코드 실행

text
Global

② a() 호출

text
a
Global

③ b() 호출

text
b
a
Global

④ c() 호출

text
c
b
a
Global

⑤ c() 종료

text
b
a
Global

⑥ b() 종료

text
a
Global

⑦ a() 종료

text
Global

⑧ 전역 코드 종료

text
Empty

이 구조를 이해하면 나중에 Stack Overflow가 왜 발생하는지도 이해할 수 있다.

12. Stack Overflow

대표적인 예가 재귀 함수다.

jsx
function recursion() {
  recursion();
}

recursion();

recursion()이 다시 recursion()을 호출한다.

그러면

text
recursion
recursion
recursion
recursion
recursion
...

계속 Call Stack에 쌓이게 된다.

결국 Stack이 감당할 수 있는 범위를 넘어가면

text
Maximum call stack size exceeded

와 같은 오류가 발생한다.

즉,

Call Stack은 무한하게 쌓을 수 있는 공간이 아니다.

13. 그렇다면 Heap은 무엇일까?

JavaScript를 설명할 때 Call Stack과 함께 자주 등장하는 것이 Heap이다.

둘의 역할은 다르다.

간단하게 생각하면

text
Call Stack
→ 실행 흐름 관리

Heap
→ 동적으로 생성되는 객체 등의 메모리 영역

이라고 이해할 수 있다.

예를 들어

jsx
const user = {
  name: "Yonghee",
  age: 30
};

라는 객체가 있다고 하자.

개념적으로 보면 객체와 같은 동적으로 생성되는 값은 Heap 영역에서 관리되고,

변수 user는 그 객체를 가리키는 참조를 가지고 있다고 생각할 수 있다.

text
Call Stack              Heap

user ────────────────→ { 
                         name: "Yonghee",
                         age: 30
                       }

여기서 중요한 것은

text
user = 객체 자체

라고 단순하게 생각하기보다는

text
user
 ↓
객체에 대한 참조
 ↓
Heap에 존재하는 객체

라고 이해하는 것이다.

14. 원시값과 객체

JavaScript의 값을 크게 나누면

text
Primitive Value
Reference Value

로 생각할 수 있다.

원시값에는

jsx
const age = 30;
const name = "Yonghee";
const isDeveloper = true;

처럼 숫자, 문자열, 불리언 등이 있다.

반면 객체는

jsx
const user = {
  name: "Yonghee"
};

처럼 여러 값을 묶어 관리한다.

특히 JavaScript의 객체는 다른 변수나 객체가 참조할 수 있기 때문에 메모리와 관련된 문제를 이해할 때 중요하다.

이 부분은 뒤에서 Heap → Reference → Garbage Collection → Closure → Memory Leak으로 이어진다.

15. 실행 컨텍스트와 Call Stack의 관계

지금까지의 내용을 연결하면 훨씬 이해하기 쉽다.

JavaScript 코드가 실행된다.

text
JavaScript 실행
      ↓
Global Execution Context 생성
      ↓
Call Stack에 들어감

그리고 함수가 호출된다.

text
함수 호출
   ↓
Function Execution Context 생성
   ↓
Call Stack에 추가

함수가 종료된다.

text
Function Execution Context
        ↓
Call Stack에서 제거

전체적으로 보면

text
JavaScript 실행
      ↓
Execution Context 생성
      ↓
Call Stack에 push
      ↓
코드 실행
      ↓
함수 호출
      ↓
새로운 Execution Context
      ↓
Call Stack에 push
      ↓
함수 실행
      ↓
함수 종료
      ↓
Call Stack에서 pop

이렇게 연결된다.

16. 실행 컨텍스트는 무엇을 가지고 있을까?

실행 컨텍스트를 조금 더 깊게 보면 실행에 필요한 여러 정보를 가지고 있다.

대표적으로

text
Lexical Environment
Variable Environment
This Binding
Outer Environment Reference

등의 개념이 연결된다.

특히 중요한 것은 Lexical Environment다.

Lexical Environment는 쉽게 말하면

현재 코드에서 사용할 수 있는 식별자와 그 값을 관리하고, 외부 환경과의 관계를 연결하는 환경

이라고 이해할 수 있다.

예를 들어

jsx
const name = "Yonghee";

function sayHello() {
  const message = `Hello ${name}`;

  console.log(message);
}

sayHello();

name은 sayHello 함수 내부에서 선언되지 않았다.

그런데도 사용할 수 있다.

왜 가능할까?

함수의 실행 환경이 자신의 외부 환경과 연결되어 있기 때문이다.

text
sayHello 실행 환경
       │
       │ 외부 환경 참조
       ↓
Global 실행 환경
       │
       ↓
name

이 개념이 바로 이후에 배울 Scope와 Closure를 이해하는 기반이 된다.

17. "렉시컬(Lexical)"이라는 말은 무엇일까?

처음 보면 Lexical Environment라는 이름이 어렵게 느껴질 수 있다.

Lexical은 쉽게 말하면

코드가 작성된 구조와 위치를 기준으로 한다

는 의미로 이해하면 된다.

예를 들어

jsx
const name = "Yonghee";

function sayHello() {
  console.log(name);
}

name을 어디서 찾을 수 있는지는 함수가 실행되는 위치가 아니라 코드가 작성된 구조와 관련이 있다.

그래서 JavaScript를 Lexical Scope 기반의 언어라고 설명한다.

이것은 뒤에서 Closure를 이해할 때 매우 중요하다.

18. 지금까지의 내용을 하나로 연결하면

이제 JavaScript 실행을 처음부터 다시 보자.

jsx
const name = "Yonghee";

function sayHello() {
  const message = `Hello ${name}`;

  console.log(message);
}

sayHello();

브라우저에서 JavaScript가 실행되면

text
① JavaScript 실행 시작
        ↓
② Global Execution Context 생성
        ↓
③ 생성 단계
        ↓
④ 전역 변수/함수 등에 대한 실행 환경 준비
        ↓
⑤ 실행 단계
        ↓
⑥ sayHello() 호출
        ↓
⑦ Function Execution Context 생성
        ↓
⑧ 생성 단계
        ↓
⑨ 함수 내부 실행 환경 준비
        ↓
⑩ 실행 단계
        ↓
⑪ message 생성
        ↓
⑫ name 조회
        ↓
⑬ console.log 실행
        ↓
⑭ 함수 종료
        ↓
⑮ Function Execution Context 제거
        ↓
⑯ 전역 코드 계속 실행

그리고 이 실행 컨텍스트들의 실행 순서를 관리하는 것이

text
Call Stack

이다.

객체 등의 동적으로 생성되는 값은 메모리에서 관리되며,

text
Heap

이라는 개념과 연결된다.

19. 그런데 여기서 하나의 문제가 생긴다

지금까지의 설명만 보면 JavaScript는 단순하다.

text
코드 실행
 ↓
Call Stack
 ↓
함수 실행
 ↓
종료

그런데 실제 브라우저에서는 이런 코드가 존재한다.

jsx
setTimeout(() => {
  console.log("Hello");
}, 1000);

console.log("World");

우리가 기대하는 결과는

text
World
Hello

다.

그렇다면 setTimeout의 1초를 기다리는 동안 JavaScript는 무엇을 하고 있을까?

만약 Call Stack에서

text
setTimeout

이 1초 동안 계속 기다린다면 다음 코드인

jsx
console.log("World");

도 실행할 수 없을 것이다.

하지만 실제로는 그렇지 않다.

text
setTimeout
   ↓
브라우저에게 작업 요청
   ↓
JavaScript는 다음 코드 실행
   ↓
World 출력
   ↓
시간이 지나면 callback이 실행될 수 있도록 대기

이 지점부터 JavaScript 엔진만 알아서는 부족하다.

브라우저가 제공하는 기능인

text
Web API

와

text
Task Queue
Microtask Queue
Event Loop

를 함께 이해해야 한다.

20. 다음 글에서 Event Loop로 넘어가는 이유

지금까지 우리는 JavaScript가

text
Execution Context
        ↓
Call Stack
        ↓
코드 실행

이라는 구조로 동작한다는 것을 살펴봤다.

그런데 브라우저에서는

jsx
fetch(...)
setTimeout(...)
Promise
addEventListener(...)

처럼 JavaScript 코드만으로 처리하기 어려운 비동기 작업이 존재한다.

이런 작업은 브라우저의 Web API와 연결되고,

작업이 완료되면 callback이나 Promise의 후속 작업이 특정 Queue에 들어간다.

그리고 Event Loop가 Call Stack의 상태를 확인하면서 실행할 수 있는 작업을 연결한다.

전체 흐름을 미리 보면 다음과 같다.

text
JavaScript
    ↓
Call Stack
    ↓
Web API
    ↓
Task Queue / Microtask Queue
    ↓
Event Loop
    ↓
Call Stack

따라서 다음 글에서는

text
Web API
   ↓
Promise
   ↓
Microtask Queue / Task Queue
   ↓
Event Loop

을 하나의 흐름으로 연결해서 살펴볼 것이다.

그리고 이 내용을 이해하면

jsx
console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

Promise.resolve().then(() => {
  console.log("C");
});

console.log("D");

왜

text
A
D
C
B

순서로 출력되는지도 자연스럽게 이해할 수 있다.

마무리

이번 글에서 중요한 것은 각각의 정의를 외우는 것이 아니다.

JavaScript가 실행되는 큰 구조를 잡는 것이 먼저다.

text
JavaScript 코드
      ↓
Execution Context
      ↓
생성 단계
      ↓
실행 단계
      ↓
Call Stack
      ↓
함수 호출
      ↓
새로운 Execution Context

그리고 메모리 관점에서는

text
Call Stack
→ 실행 흐름

Heap
→ 객체 등의 동적 데이터가 관리되는 메모리 영역

으로 생각할 수 있다.

여기까지 이해하면 JavaScript가 단순히

"위에서부터 코드를 실행하는 언어"

가 아니라,

실행 환경을 만들고, 실행 컨텍스트를 구성하고, Call Stack을 통해 실행 흐름을 관리하는 언어

라는 것을 볼 수 있다.

그리고 여기서 자연스럽게 다음 질문이 생긴다.

그런데 setTimeout, fetch, Promise 같은 비동기 작업은 Call Stack 하나만으로 어떻게 처리할까?

이 질문의 답이 다음 글에서 다룰 Event Loop다.