JavaScript 실행 컨텍스트 - 코드가 실행되는 환경의 비밀

한 줄 요약

실행 컨텍스트는 JavaScript 엔진이 코드를 평가하고 실행하기 위해 생성하는 추상적인 환경으로, 변수/함수 선언의 저장, 스코프 체인 형성, this 바인딩을 담당합니다.

“왜 함수를 선언하기 전에 호출할 수 있을까?”, “왜 var는 undefined인데 let은 ReferenceError를 던질까?”, “this는 왜 호출 방식에 따라 달라질까?” — 이 모든 질문의 답은 실행 컨텍스트에 있습니다.

실행 컨텍스트는 JavaScript의 가장 핵심적인 개념으로, 호이스팅, 스코프, 클로저, this 바인딩 등 JavaScript의 독특한 동작들을 설명하는 열쇠입니다. 이 문서에서는 ECMAScript 명세를 기반으로 실행 컨텍스트의 원리를 깊이 있게 탐구합니다.

먼저, 기초부터 이해하기

왜 실행 컨텍스트를 알아야 하나?

실행 컨텍스트를 이해하지 못하면, 다음과 같은 코드의 동작을 설명할 수 없습니다:

// 예제 1: 함수 호이스팅
greet(); // "Hello!" - 왜 에러가 안 날까?

function greet() {
  console.log("Hello!");
}

// 예제 2: var vs let의 차이
console.log(x); // undefined - 왜?
console.log(y); // ReferenceError - 왜?

var x = 10;
let y = 20;

// 예제 3: 클로저
function outer() {
  const message = "Hello";

  return function inner() {
    console.log(message); // 왜 접근 가능할까?
  };
}

const fn = outer();
fn(); // "Hello"

// 예제 4: this 바인딩
const obj = {
  value: 42,
  method: function() {
    console.log(this.value); // 42
  }
};

const detached = obj.method;
detached(); // undefined (또는 에러) - 왜?

이 모든 동작은 실행 컨텍스트가 생성되고 관리되는 방식 때문입니다.

실행 컨텍스트란 무엇인가?

ECMAScript 명세에 따르면:

“An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation.”

번역하면, 실행 컨텍스트는 JavaScript 엔진이 코드의 실행 시점 평가를 추적하기 위해 사용하는 명세 장치입니다.

쉽게 말해, 코드가 실행되는 환경이자 상자입니다:

┌─────────────────────────────────────┐
│    실행 컨텍스트 (Execution Context).  │
│                                     │
│  📦 변수 저장소                        │
│     - var x = 10                    │
│     - let y = 20                    │
│     - function foo() {...}          │
│                                     │
│  🔗 스코프 체인                        │
│     - 외부 환경 참조                   │
│     - 변수 검색 경로                   │
│                                     │
│  👉 this 바인딩                       │ 
│     - this가 가리킬 객체               │
│                                     │
└─────────────────────────────────────┘

실행 컨텍스트의 타입

JavaScript에는 3가지 타입의 실행 컨텍스트가 있습니다.

1. 전역 실행 컨텍스트 (Global Execution Context)

코드가 처음 실행될 때 생성되는 기본 컨텍스트입니다.

특징:

  • 프로그램당 단 하나만 존재합니다
  • 전역 객체(window, global, globalThis)를 생성합니다
  • this를 전역 객체에 바인딩합니다
// 전역 실행 컨텍스트에서 실행
console.log(this === window); // true (브라우저)
console.log(this === global); // true (Node.js)

var globalVar = "I'm global";
let globalLet = "I'm also global";

console.log(window.globalVar); // "I'm global"
console.log(window.globalLet); // undefined (let은 전역 객체의 프로퍼티가 아님)

전역 컨텍스트의 구조:

┌─────────────────────────────────────┐
│   전역 실행 컨텍스트 (GEC)            │
├─────────────────────────────────────┤
│ LexicalEnvironment:                 │
│   - Environment Record:             │
│     · let, const 변수               │
│     · 함수 선언                      │
│   - Outer Reference: null           │
│   - This Binding: globalThis        │
│                                     │
│ VariableEnvironment:                │
│   - Environment Record:             │
│     · var 변수                       │
│   - Outer Reference: null           │
│                                     │
└─────────────────────────────────────┘

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

함수가 호출될 때마다 생성됩니다 (선언 시가 아님!).

특징:

  • 함수 호출마다 새로운 컨텍스트 생성
  • 매개변수와 arguments 객체를 포함
  • this는 함수 호출 방식에 따라 결정됨
function add(a, b) {
  console.log(arguments); // [10, 20]
  console.log(this);      // 호출 방식에 따라 다름

  const sum = a + b;
  return sum;
}

// 호출할 때마다 새로운 함수 실행 컨텍스트 생성
add(10, 20); // 첫 번째 컨텍스트
add(5, 15);  // 두 번째 컨텍스트

함수 컨텍스트의 구조:

┌─────────────────────────────────────┐
│   함수 실행 컨텍스트 (FEC)            │
├─────────────────────────────────────┤
│ LexicalEnvironment:                 │
│   - Environment Record:             │
│     · 매개변수 (a, b)                │
│     · let, const 변수               │
│     · 함수 선언                      │
│     · arguments 객체                │
│   - Outer Reference: 상위 스코프     │
│   - This Binding: 호출 방식에 따름   │
│                                     │
│ VariableEnvironment:                │
│   - Environment Record:             │
│     · var 변수                       │
│   - Outer Reference: 상위 스코프     │
│                                     │
└─────────────────────────────────────┘

3. Eval 실행 컨텍스트

eval() 함수 내부의 코드가 실행될 때 생성됩니다.

특징:

  • eval() 호출마다 생성
  • 보안 및 성능 문제로 사용 권장 안 함
// ❌ 사용하지 마세요
eval('var x = 10');
console.log(x); // 10 (전역 스코프를 오염시킴)

// Eval 컨텍스트는 호출된 컨텍스트의 환경에 접근 가능
function test() {
  const a = 5;
  eval('console.log(a)'); // 5
}

주의: eval은 실행 컨텍스트를 복잡하게 만들고 최적화를 방해합니다. 절대 사용하지 마세요!

실행 컨텍스트의 구성 요소

ECMAScript 명세에 따르면, 실행 컨텍스트는 다음 State Components를 가집니다:

핵심 구성 요소 3가지

실행 컨텍스트
├── LexicalEnvironment      (렉시컬 환경)
├── VariableEnvironment     (변수 환경)
└── ThisBinding             (this 바인딩)

1. LexicalEnvironment (렉시컬 환경)

정의: 식별자와 변수의 매핑을 관리하는 구조체입니다.

구성:

  • Environment Record: 현재 스코프의 식별자 바인딩을 저장
  • Outer Reference: 외부(상위) 렉시컬 환경에 대한 참조
  • This Binding: this
// 렉시컬 환경의 예
function outer() {
  const outerVar = "outer";

  function inner() {
    const innerVar = "inner";
    console.log(outerVar); // Outer Reference를 통해 접근
  }

  inner();
}

outer();

시각화:

outer() 호출 시:

┌─────────────────────────────────────┐
│  inner 함수 실행 컨텍스트            │
│  LexicalEnvironment:                │
│    - Environment Record:            │
│        innerVar: "inner"            │
│    - Outer Reference: ─────┐        │
│    - This Binding: global  │        │
└────────────────────────────┼────────┘
                             ↓
┌────────────────────────────┼────────┐
│  outer 함수 실행 컨텍스트   │        │
│  LexicalEnvironment: ←─────┘        │
│    - Environment Record:            │
│        outerVar: "outer"            │
│        inner: <function>            │
│    - Outer Reference: ─────┐        │
│    - This Binding: global  │        │
└────────────────────────────┼────────┘
                             ↓
┌────────────────────────────┼────────┐
│  전역 실행 컨텍스트         │        │
│  LexicalEnvironment: ←─────┘        │
│    - Environment Record:            │
│        outer: <function>            │
│    - Outer Reference: null          │
│    - This Binding: globalThis       │
└─────────────────────────────────────┘

Outer Reference 체인이 바로 스코프 체인입니다!

2. VariableEnvironment (변수 환경)

정의: var 선언과 함수 선언을 저장하는 렉시컬 환경입니다.

LexicalEnvironment와의 차이:

특성 LexicalEnvironment VariableEnvironment
저장 대상 let, const, 함수 매개변수 var, 함수 선언
값 변경 실행 중 변경 가능 초기 값 유지
with/catch 블록 임시로 변경됨 변경 안 됨

초기 상태:

// 컨텍스트 생성 시
LexicalEnvironment === VariableEnvironment // true

// 하지만 with 문이나 catch 블록에서는...

with 문에서의 차이:

const obj = { x: 10 };
let y = 20;
var z = 30;

with (obj) {
  console.log(x); // 10 (obj.x)
  console.log(y); // 20 (외부 y)
  console.log(z); // 30 (외부 z)

  // 여기서 LexicalEnvironment는 임시로 obj를 포함하지만,
  // VariableEnvironment는 변경되지 않음
}

시각화:

with 블록 진입 전:
┌─────────────────────────────────┐
│ LexicalEnvironment:             │
│   y: 20, z: 30                  │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ VariableEnvironment:            │
│   y: 20, z: 30                  │
└─────────────────────────────────┘

with 블록 안:
┌─────────────────────────────────┐
│ LexicalEnvironment (임시):      │
│   obj: { x: 10 } ───┐           │
│   Outer: ───────────┼─→ 외부    │
└─────────────────────┼───────────┘
                      ↓
┌─────────────────────┼───────────┐
│ VariableEnvironment (불변): ←──┘│
│   y: 20, z: 30                  │
└─────────────────────────────────┘

3. ThisBinding (this 바인딩)

정의: 현재 실행 컨텍스트에서 this 키워드가 참조할 값입니다.

결정 시점: 함수가 호출될 때 결정됩니다 (선언 시가 아님!).

바인딩 규칙:

// 1. 전역 컨텍스트: 전역 객체
console.log(this); // window (브라우저) / global (Node.js)

// 2. 함수 호출: 전역 객체 (비엄격) / undefined (엄격)
function regularFunc() {
  console.log(this);
}
regularFunc(); // window (비엄격) / undefined (엄격 모드)

// 3. 메서드 호출: 메서드를 소유한 객체
const obj = {
  value: 42,
  method() {
    console.log(this.value); // 42
  }
};
obj.method(); // this = obj

// 4. 생성자 호출: 새로 생성된 인스턴스
function Person(name) {
  this.name = name;
}
const person = new Person("John"); // this = 새 객체

// 5. 명시적 바인딩: call/apply/bind로 지정한 값
function greet() {
  console.log(this.name);
}
const user = { name: "Alice" };
greet.call(user); // this = user

// 6. 화살표 함수: 상위 스코프의 this
const arrow = () => {
  console.log(this); // 상위 컨텍스트의 this
};

실행 컨텍스트 스택 (Call Stack)

실행 컨텍스트는 스택(Stack) 구조로 관리됩니다.

Call Stack의 동작 원리

스택 구조 (LIFO: Last In, First Out)
┌─────────┐
│  최상단  │ ← 현재 실행 중인 컨텍스트
├─────────┤
│         │
├─────────┤
│         │
├─────────┤
│  전역   │ ← 항상 최하단
└─────────┘

실전 예제: 실행 순서 추적

function first() {
  console.log("첫 번째 함수 시작");
  second();
  console.log("첫 번째 함수 끝");
}

function second() {
  console.log("두 번째 함수 시작");
  third();
  console.log("두 번째 함수 끝");
}

function third() {
  console.log("세 번째 함수");
}

first();

Call Stack 변화:

1. 프로그램 시작
┌─────────────────┐
│  전역 컨텍스트   │
└─────────────────┘

2. first() 호출
┌─────────────────┐
│  first()        │
├─────────────────┤
│  전역 컨텍스트   │
└─────────────────┘
출력: "첫 번째 함수 시작"

3. second() 호출
┌─────────────────┐
│  second()       │
├─────────────────┤
│  first()        │
├─────────────────┤
│  전역 컨텍스트   │
└─────────────────┘
출력: "두 번째 함수 시작"

4. third() 호출
┌─────────────────┐
│  third()        │
├─────────────────┤
│  second()       │
├─────────────────┤
│  first()        │
├─────────────────┤
│  전역 컨텍스트   │
└─────────────────┘
출력: "세 번째 함수"

5. third() 완료
┌─────────────────┐
│  second()       │
├─────────────────┤
│  first()        │
├─────────────────┤
│  전역 컨텍스트   │
└─────────────────┘
출력: "두 번째 함수 끝"

6. second() 완료
┌─────────────────┐
│  first()        │
├─────────────────┤
│  전역 컨텍스트   │
└─────────────────┘
출력: "첫 번째 함수 끝"

7. first() 완료
┌─────────────────┐
│  전역 컨텍스트   │
└─────────────────┘

최종 출력:

첫 번째 함수 시작
두 번째 함수 시작
세 번째 함수
두 번째 함수 끝
첫 번째 함수 끝

Stack Overflow

스택은 무한정 커질 수 없습니다. 너무 깊은 재귀는 스택 오버플로우를 유발합니다:

// ❌ Stack Overflow 발생
function recursiveFunc() {
  recursiveFunc(); // 끝없는 재귀
}

recursiveFunc(); // Uncaught RangeError: Maximum call stack size exceeded

해결책:

// ✅ 종료 조건 추가
function recursiveFunc(n) {
  if (n <= 0) return; // 종료 조건
  recursiveFunc(n - 1);
}

recursiveFunc(10); // 정상 작동

// ✅ Tail Call Optimization (ES6)
// 엄격 모드에서 꼬리 재귀는 최적화됨
'use strict';
function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return factorial(n - 1, n * acc); // 꼬리 호출
}

실행 컨텍스트의 생성과 실행

실행 컨텍스트는 2단계로 생성되고 실행됩니다.

1단계: 생성 단계 (Creation Phase)

코드 실행 전에 실행 컨텍스트를 설정합니다.

작업 내용:

  1. LexicalEnvironment 생성
    • Environment Record 생성
    • Outer Reference 설정
    • This Binding 결정
  2. VariableEnvironment 생성
    • 초기에는 LexicalEnvironment와 동일
  3. 호이스팅 발생
    • 함수 선언: 전체가 메모리에 저장
    • var 변수: undefined로 초기화
    • let/const: 선언만 되고 초기화 안 됨 (TDZ)
// 원본 코드
console.log(a); // undefined
console.log(b); // ReferenceError

var a = 10;
let b = 20;

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

생성 단계 후 상태:

전역 실행 컨텍스트 (생성 단계):
┌─────────────────────────────────────┐
│ LexicalEnvironment:                 │
│   - Environment Record:             │
│       greet: <function>             │
│       b: <uninitialized>  ← TDZ!    │
│   - Outer: null                     │
│   - This: globalThis                │
│                                     │
│ VariableEnvironment:                │
│   - Environment Record:             │
│       a: undefined                  │
│   - Outer: null                     │
└─────────────────────────────────────┘

2단계: 실행 단계 (Execution Phase)

코드를 한 줄씩 실행하며 변수에 값을 할당합니다.

// 실행 단계
console.log(a); // undefined (생성 단계에서 초기화됨)
console.log(b); // ReferenceError (아직 TDZ)

var a = 10;     // a에 10 할당
let b = 20;     // b 초기화 및 20 할당

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

실행 단계 후 상태:

전역 실행 컨텍스트 (실행 단계):
┌─────────────────────────────────────┐
│ LexicalEnvironment:                 │
│   - Environment Record:             │
│       greet: <function>             │
│       b: 20  ← 이제 초기화됨         │
│   - Outer: null                     │
│   - This: globalThis                │
│                                     │
│ VariableEnvironment:                │
│   - Environment Record:             │
│       a: 10  ← 값 할당됨             │
│   - Outer: null                     │
└─────────────────────────────────────┘

전체 흐름 예제

var x = 10;

function outer(a) {
  var y = 20;

  function inner(b) {
    var z = 30;
    console.log(x + y + z + a + b);
  }

  inner(5);
}

outer(15);

단계별 실행:

1. 전역 컨텍스트 생성 (Creation)
┌─────────────────────────────────────┐
│ Global Execution Context            │
│ LexicalEnvironment:                 │
│   outer: <function>                 │
│ VariableEnvironment:                │
│   x: undefined                      │
└─────────────────────────────────────┘

2. 전역 코드 실행 (Execution)
┌─────────────────────────────────────┐
│ Global EC                           │
│   x: 10                             │
└─────────────────────────────────────┘

3. outer(15) 호출 → outer 컨텍스트 생성
┌─────────────────────────────────────┐
│ outer EC (Creation)                 │
│ LexicalEnvironment:                 │
│   a: 15, inner: <function>          │
│ VariableEnvironment:                │
│   y: undefined                      │
│ Outer Reference: Global EC          │
├─────────────────────────────────────┤
│ Global EC                           │
│   x: 10, outer: <function>          │
└─────────────────────────────────────┘

4. outer 함수 실행
┌─────────────────────────────────────┐
│ outer EC (Execution)                │
│   a: 15, y: 20, inner: <function>   │
│ Outer: Global EC                    │
├─────────────────────────────────────┤
│ Global EC                           │
│   x: 10                             │
└─────────────────────────────────────┘

5. inner(5) 호출 → inner 컨텍스트 생성
┌─────────────────────────────────────┐
│ inner EC (Creation)                 │
│ LexicalEnvironment:                 │
│   b: 5                              │
│ VariableEnvironment:                │
│   z: undefined                      │
│ Outer Reference: outer EC           │
├─────────────────────────────────────┤
│ outer EC                            │
│   a: 15, y: 20                      │
│ Outer: Global EC                    │
├─────────────────────────────────────┤
│ Global EC                           │
│   x: 10                             │
└─────────────────────────────────────┘

6. inner 함수 실행
┌─────────────────────────────────────┐
│ inner EC (Execution)                │
│   b: 5, z: 30                       │
│                                     │
│ 변수 검색 순서:                      │
│ z → inner EC (30)                   │
│ y → outer EC (20)                   │
│ x → Global EC (10)                  │
│ a → outer EC (15)                   │
│ b → inner EC (5)                    │
│                                     │
│ 출력: 10 + 20 + 30 + 15 + 5 = 80    │
├─────────────────────────────────────┤
│ outer EC                            │
├─────────────────────────────────────┤
│ Global EC                           │
└─────────────────────────────────────┘

7. inner 완료 → inner EC 제거
┌─────────────────────────────────────┐
│ outer EC                            │
├─────────────────────────────────────┤
│ Global EC                           │
└─────────────────────────────────────┘

8. outer 완료 → outer EC 제거
┌─────────────────────────────────────┐
│ Global EC                           │
└─────────────────────────────────────┘

실행 컨텍스트로 설명하는 JavaScript의 핵심 개념

실행 컨텍스트를 이해하면 JavaScript의 독특한 동작들을 완벽히 설명할 수 있습니다.

1. 호이스팅 (Hoisting)

원인: 생성 단계에서 함수와 변수 선언이 메모리에 먼저 저장되기 때문입니다.

// 작성한 코드
console.log(foo); // undefined
console.log(bar); // ReferenceError

var foo = "Hello";
let bar = "World";

greet(); // "Hi!"

function greet() {
  console.log("Hi!");
}

생성 단계 후:

LexicalEnvironment:
  greet: <function>  ← 함수 전체가 저장됨
  bar: <uninitialized>  ← 선언만 됨 (TDZ)

VariableEnvironment:
  foo: undefined  ← undefined로 초기화됨

따라서:

  • fooundefined를 출력 (var 호이스팅)
  • barReferenceError (TDZ)
  • greet()는 정상 실행 (함수 호이스팅)

2. 스코프 (Scope)

원인: Outer Reference 체인을 통한 변수 검색입니다.

const global = "전역";

function outer() {
  const outerVar = "외부";

  function inner() {
    const innerVar = "내부";

    console.log(innerVar);  // "내부" (자신의 스코프)
    console.log(outerVar);  // "외부" (Outer Reference)
    console.log(global);    // "전역" (Outer Reference 체인)
    console.log(notExist);  // ReferenceError (체인 끝까지 없음)
  }

  inner();
}

outer();

변수 검색 순서:

1. innerVar 검색:
   inner EC → 발견! ✓

2. outerVar 검색:
   inner EC → 없음
   → outer EC (Outer Reference) → 발견! ✓

3. global 검색:
   inner EC → 없음
   → outer EC → 없음
   → Global EC (Outer Reference) → 발견! ✓

4. notExist 검색:
   inner EC → 없음
   → outer EC → 없음
   → Global EC → 없음
   → ReferenceError ✗

3. 클로저 (Closure)

원인: 함수가 생성될 때 Outer Reference가 결정되고, 함수가 반환된 후에도 외부 환경에 접근할 수 있기 때문입니다.

function makeCounter() {
  let count = 0; // 외부 함수의 변수

  return function() {
    count++;
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

클로저 형성:

1. makeCounter() 호출
┌─────────────────────────────────────┐
│ makeCounter EC                      │
│   count: 0                          │
│   return <function>                 │
└─────────────────────────────────────┘

2. makeCounter() 완료, but...
   반환된 함수는 makeCounter EC에 대한
   Outer Reference를 유지!

3. counter() 호출
┌─────────────────────────────────────┐
│ counter EC                          │
│   (변수 없음)                        │
│   Outer Reference: ───┐             │
└────────────────────────┼────────────┘
                         ↓
┌────────────────────────┼────────────┐
│ makeCounter EC (유지됨!) │           │
│   count: 1 ← 접근 가능!  │           │
└─────────────────────────────────────┘

핵심:

  • 일반적으로 함수 실행이 끝나면 실행 컨텍스트가 제거됩니다
  • 하지만 클로저가 있으면 외부 환경이 메모리에 유지됩니다
  • 이것이 클로저의 “상태 보존” 원리입니다!

4. Temporal Dead Zone (TDZ)

원인: let/const는 생성 단계에서 선언만 되고 초기화되지 않기 때문입니다.

console.log(x); // ReferenceError: Cannot access 'x' before initialization

// ↑ TDZ (Temporal Dead Zone) ↑
// ─────────────────────────────
let x = 10; // ← 초기화 지점
// ─────────────────────────────
// ↓ TDZ 종료 ↓

console.log(x); // 10

생성 단계:

LexicalEnvironment:
  x: <uninitialized>  ← 선언은 됐지만 초기화 안 됨

TDZ 구간:

  • 스코프 시작 ~ 선언문까지
  • 이 구간에서 x에 접근하면 ReferenceError

실행 단계에서 let x = 10 만나면:

LexicalEnvironment:
  x: 10  ← 이제 초기화되고 값 할당됨

5. var vs let/const의 차이

// var: VariableEnvironment에 저장
if (true) {
  var x = 10;
}
console.log(x); // 10 (블록 밖에서도 접근 가능)

// let/const: LexicalEnvironment에 저장 (블록 스코프)
if (true) {
  let y = 20;
}
console.log(y); // ReferenceError

왜 다를까?

if 블록 진입 시:

var의 경우:
┌─────────────────────────────────────┐
│ 전역 EC                             │
│ VariableEnvironment:                │
│   x: 10  ← 전역에 저장됨             │
└─────────────────────────────────────┘

let의 경우:
┌─────────────────────────────────────┐
│ 블록 스코프 (새 LexicalEnvironment)  │
│   y: 20  ← 블록에만 저장됨           │
│   Outer: 전역 EC                    │
├─────────────────────────────────────┤
│ 전역 EC                             │
└─────────────────────────────────────┘

블록 종료 후:
- var x는 여전히 전역 EC에 존재
- let y는 블록 스코프와 함께 제거됨

실전 예제와 함정

예제 1: 클로저의 흔한 실수

// ❌ 잘못된 예: 모두 3을 출력
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 3, 3, 3
  }, 100);
}

// 왜? var는 함수 스코프이므로 전역에 하나의 i만 존재
// setTimeout 콜백들이 모두 같은 i를 참조함

실행 컨텍스트 분석:

for 루프 후:
┌─────────────────────────────────────┐
│ 전역 EC                             │
│   i: 3  ← 하나의 i만 존재            │
└─────────────────────────────────────┘

setTimeout 콜백들:
모두 전역 EC의 i를 참조 → 3, 3, 3

해결책 1: let 사용 (블록 스코프)

// ✅ 올바른 예 1
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 0, 1, 2
  }, 100);
}

// let은 블록 스코프 → 각 반복마다 새로운 i 생성

실행 컨텍스트:

각 반복마다 새로운 블록 스코프:
┌─────────────────────────────────────┐
│ 반복 1 블록 스코프                   │
│   i: 0  ← 첫 번째 콜백이 참조        │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 반복 2 블록 스코프                   │
│   i: 1  ← 두 번째 콜백이 참조        │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 반복 3 블록 스코프                   │
│   i: 2  ← 세 번째 콜백이 참조        │
└─────────────────────────────────────┘

해결책 2: IIFE로 클로저 생성

// ✅ 올바른 예 2: IIFE 사용
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => {
      console.log(j); // 0, 1, 2
    }, 100);
  })(i);
}

// 각 반복마다 새로운 함수 EC 생성
// 각 EC는 자신만의 j를 가짐

예제 2: this 바인딩 함정

const obj = {
  value: 42,
  getValue: function() {
    return this.value;
  }
};

// ✅ 정상 작동
console.log(obj.getValue()); // 42
// this = obj (메서드 호출)

// ❌ 문제 발생
const detached = obj.getValue;
console.log(detached()); // undefined (또는 TypeError)
// this = undefined (엄격 모드) / window (비엄격)

실행 컨텍스트 분석:

obj.getValue() 호출:
┌─────────────────────────────────────┐
│ getValue EC                         │
│ This Binding: obj  ← 메서드 호출     │
│   → this.value = 42                 │
└─────────────────────────────────────┘

detached() 호출:
┌─────────────────────────────────────┐
│ getValue EC                         │
│ This Binding: undefined  ← 일반 호출 │
│   → this.value = undefined          │
└─────────────────────────────────────┘

해결책:

// ✅ 해결책 1: bind 사용
const bound = obj.getValue.bind(obj);
console.log(bound()); // 42

// ✅ 해결책 2: 화살표 함수
const obj2 = {
  value: 42,
  getValue: () => {
    return this.value; // 상위 스코프의 this (전역)
  }
};
// 주의: 화살표 함수는 자체 this가 없음!

// ✅ 해결책 3: 래퍼 함수
const wrapper = () => obj.getValue();
console.log(wrapper()); // 42

예제 3: 중첩 스코프와 변수 섀도잉

let x = "전역";

function outer() {
  let x = "외부";
  console.log(x); // "외부"

  function inner() {
    let x = "내부";
    console.log(x); // "내부"
  }

  inner();
  console.log(x); // "외부"
}

outer();
console.log(x); // "전역"

실행 컨텍스트:

┌─────────────────────────────────────┐
│ inner EC                            │
│   x: "내부"  ← 가장 가까운 x         │
│   Outer: outer EC                   │
├─────────────────────────────────────┤
│ outer EC                            │
│   x: "외부"  ← 외부의 x              │
│   Outer: Global EC                  │
├─────────────────────────────────────┤
│ Global EC                           │
│   x: "전역"  ← 전역의 x              │
└─────────────────────────────────────┘

변수 검색: 가장 가까운 스코프부터 검색
inner의 x → inner EC의 x ("내부")
outer의 x → outer EC의 x ("외부")
global의 x → Global EC의 x ("전역")

예제 4: 비동기와 실행 컨텍스트

console.log("1");

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

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

console.log("4");

// 출력: 1, 4, 3, 2

Call Stack과 실행 컨텍스트:

1. 전역 EC 생성 및 실행
   Call Stack: [전역 EC]
   출력: "1"

2. setTimeout 등록 (Task Queue에 추가)
   Call Stack: [전역 EC]
   Task Queue: [() => console.log("2")]

3. Promise 등록 (Microtask Queue에 추가)
   Call Stack: [전역 EC]
   Microtask Queue: [() => console.log("3")]

4. console.log("4") 실행
   Call Stack: [전역 EC]
   출력: "4"

5. 전역 EC 종료
   Call Stack: []

6. Microtask Queue 처리 (우선순위 높음)
   Call Stack: [Promise 콜백 EC]
   출력: "3"

7. Task Queue 처리
   Call Stack: [setTimeout 콜백 EC]
   출력: "2"

실전 활용 패턴

1. 모듈 패턴 (Module Pattern)

실행 컨텍스트와 클로저를 활용한 캡슐화:

const CounterModule = (function() {
  // Private 변수 (외부에서 접근 불가)
  let count = 0;

  // Private 함수
  function validateValue(val) {
    return typeof val === 'number' && val >= 0;
  }

  // Public API
  return {
    increment() {
      count++;
      return count;
    },

    decrement() {
      count--;
      return count;
    },

    reset() {
      count = 0;
      return count;
    },

    getValue() {
      return count;
    },

    setValue(val) {
      if (validateValue(val)) {
        count = val;
        return true;
      }
      return false;
    }
  };
})();

// 사용
console.log(CounterModule.increment()); // 1
console.log(CounterModule.increment()); // 2
console.log(CounterModule.getValue());  // 2
console.log(CounterModule.reset());     // 0

// ❌ Private 변수/함수에는 접근 불가
console.log(CounterModule.count);        // undefined
console.log(CounterModule.validateValue); // undefined

실행 컨텍스트 분석:

IIFE 실행 후:
┌─────────────────────────────────────┐
│ IIFE EC (클로저로 유지됨)            │
│   count: 0                          │
│   validateValue: <function>         │
│                                     │
│   ↑                                 │
│   │ Outer Reference                 │
│   │                                 │
│ ┌─┴───────────────────────────────┐ │
│ │ 반환된 객체의 메서드들:          │ │
│ │ - increment                     │ │
│ │ - decrement                     │ │
│ │ - reset                         │ │
│ │ - getValue                      │ │
│ │ - setValue                      │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────┘

각 메서드는 IIFE EC에 접근 가능 (클로저)
하지만 외부에서는 count에 직접 접근 불가!

2. 팩토리 함수로 독립적인 인스턴스 생성

function createPerson(name, age) {
  // 각 호출마다 새로운 실행 컨텍스트 생성

  // Private 변수
  let _name = name;
  let _age = age;

  // Public 메서드 (클로저)
  return {
    getName() {
      return _name;
    },

    setName(newName) {
      if (typeof newName === 'string' && newName.length > 0) {
        _name = newName;
        return true;
      }
      return false;
    },

    getAge() {
      return _age;
    },

    haveBirthday() {
      _age++;
      return _age;
    },

    introduce() {
      return `안녕하세요, 저는 ${_name}이고 ${_age}살입니다.`;
    }
  };
}

// 각 인스턴스는 독립적인 실행 컨텍스트를 가짐
const person1 = createPerson("Alice", 25);
const person2 = createPerson("Bob", 30);

console.log(person1.introduce()); // "안녕하세요, 저는 Alice이고 25살입니다."
console.log(person2.introduce()); // "안녕하세요, 저는 Bob이고 30살입니다."

person1.haveBirthday();
console.log(person1.getAge()); // 26
console.log(person2.getAge()); // 30 (독립적!)

실행 컨텍스트:

createPerson("Alice", 25) 호출:
┌─────────────────────────────────────┐
│ createPerson EC #1 (유지됨)         │
│   _name: "Alice"                    │
│   _age: 25                          │
│   ↑                                 │
│   └─ person1 메서드들이 참조         │
└─────────────────────────────────────┘

createPerson("Bob", 30) 호출:
┌─────────────────────────────────────┐
│ createPerson EC #2 (유지됨)         │
│   _name: "Bob"                      │
│   _age: 30                          │
│   ↑                                 │
│   └─ person2 메서드들이 참조         │
└─────────────────────────────────────┘

각 인스턴스는 독립적인 실행 컨텍스트를 가지므로
상태가 서로 영향을 주지 않음!

3. 커링 (Currying)과 부분 적용

// 커링: 여러 인자를 받는 함수를 단일 인자를 받는 함수들의 체인으로 변환
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      // 충분한 인자가 모이면 실행
      return fn.apply(this, args);
    } else {
      // 아직 부족하면 다음 인자를 기다리는 함수 반환
      return function(...nextArgs) {
        return curried.apply(this, args.concat(nextArgs));
      };
    }
  };
}

// 예제: 3개 인자를 받는 함수
function add(a, b, c) {
  return a + b + c;
}

const curriedAdd = curry(add);

// 다양한 방식으로 호출 가능
console.log(curriedAdd(1)(2)(3));       // 6
console.log(curriedAdd(1, 2)(3));       // 6
console.log(curriedAdd(1)(2, 3));       // 6
console.log(curriedAdd(1, 2, 3));       // 6

// 부분 적용으로 재사용 가능한 함수 생성
const add5 = curriedAdd(5);
console.log(add5(10, 15)); // 30
console.log(add5(20, 25)); // 50

const add5and10 = curriedAdd(5)(10);
console.log(add5and10(15)); // 30
console.log(add5and10(20)); // 35

실행 컨텍스트 (curriedAdd(1)(2)(3) 호출 시):

1. curriedAdd(1) 호출
┌─────────────────────────────────────┐
│ curried EC #1                       │
│   args: [1]                         │
│   return <function>  ───────┐       │
└──────────────────────────────┼──────┘
                               ↓
2. 반환된 함수(2) 호출         ↓
┌──────────────────────────────┼──────┐
│ nextArgs wrapper EC          │      │
│   nextArgs: [2]              │      │
│   curried.apply(..., [1, 2]) │      │
│                              │      │
│ ┌──────────────────────────┐ │      │
│ │ curried EC #2            │ │      │
│ │   args: [1, 2]           │ │      │
│ │   return <function> ─────┼─┼──┐   │
│ └──────────────────────────┘ │  │   │
└──────────────────────────────┘  │   │
                                  ↓   │
3. 반환된 함수(3) 호출             ↓   │
┌──────────────────────────────────┼──┐
│ nextArgs wrapper EC              │  │
│   nextArgs: [3]                  │  │
│   curried.apply(..., [1, 2, 3])  │  │
│                                  │  │
│ ┌──────────────────────────────┐ │  │
│ │ curried EC #3                │ │  │
│ │   args: [1, 2, 3]            │ │  │
│ │   args.length >= fn.length   │ │  │
│ │   → fn.apply(this, [1,2,3])  │ │  │
│ │                              │ │  │
│ │ ┌────────────────────────┐   │ │  │
│ │ │ add EC                │   │ │  │
│ │ │   a: 1, b: 2, c: 3   │   │ │  │
│ │ │   return 6           │   │ │  │
│ │ └────────────────────────┘   │ │  │
│ └──────────────────────────────┘ │  │
└──────────────────────────────────┘  │

각 단계의 EC가 클로저를 형성하여
이전 인자들을 유지함!

4. 메모이제이션 (Memoization)

실행 컨텍스트를 활용한 결과 캐싱:

function memoize(fn) {
  // 캐시를 저장할 객체 (클로저로 유지됨)
  const cache = new Map();

  return function(...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      console.log(`캐시에서 반환: ${key}`);
      return cache.get(key);
    }

    console.log(`계산 중: ${key}`);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// 예제: 피보나치 수열
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const memoizedFib = memoize(fibonacci);

console.log(memoizedFib(10)); // 계산 중
console.log(memoizedFib(10)); // 캐시에서 반환 (빠름!)
console.log(memoizedFib(15)); // 계산 중

실행 컨텍스트:

memoize(fibonacci) 호출:
┌─────────────────────────────────────┐
│ memoize EC (유지됨)                 │
│   cache: Map {}                     │
│   ↑                                 │
│   └─ 반환된 함수가 참조              │
└─────────────────────────────────────┘

첫 번째 memoizedFib(10) 호출:
┌─────────────────────────────────────┐
│ 반환된 함수 EC                       │
│   args: [10]                        │
│   key: "[10]"                       │
│   cache.has("[10]") → false         │
│   → fibonacci(10) 실행               │
│   → cache.set("[10]", 55)           │
│   Outer: memoize EC                 │
│     cache: Map { "[10]" => 55 }     │
└─────────────────────────────────────┘

두 번째 memoizedFib(10) 호출:
┌─────────────────────────────────────┐
│ 반환된 함수 EC                       │
│   args: [10]                        │
│   key: "[10]"                       │
│   cache.has("[10]") → true ✓        │
│   → cache.get("[10]") → 55          │
│   (fibonacci 실행 안 함!)            │
│   Outer: memoize EC                 │
│     cache: Map { "[10]" => 55 }     │
└─────────────────────────────────────┘

cache는 memoize EC에 저장되어
모든 호출에서 공유됨!

디버깅과 도구

1. 브라우저 개발자 도구에서 확인하기

Chrome DevTools에서 실행 컨텍스트를 시각적으로 확인할 수 있습니다:

function outer() {
  const outerVar = "외부";

  function inner() {
    debugger; // 여기서 중단점
    const innerVar = "내부";
    console.log(outerVar);
  }

  inner();
}

outer();

DevTools에서 볼 수 있는 것:

  1. Call Stack: 현재 실행 컨텍스트 스택
  2. Scope: 각 스코프의 변수들
    • Local (현재 함수)
    • Closure (클로저 변수)
    • Global (전역 변수)
  3. Watch: 변수 값 추적

2. console.trace()로 호출 스택 확인

function first() {
  second();
}

function second() {
  third();
}

function third() {
  console.trace("호출 스택 추적");
}

first();

// 출력:
// console.trace 호출 스택 추적
//   third @ example.js:10
//   second @ example.js:6
//   first @ example.js:2
//   (anonymous) @ example.js:13

3. 스코프 체인 시각화 도구

function visualizeScope() {
  const global = "전역";

  function level1() {
    const var1 = "레벨1";

    function level2() {
      const var2 = "레벨2";

      function level3() {
        const var3 = "레벨3";

        // 모든 스코프에 접근 가능
        console.log("Scope Chain:");
        console.log("- Level 3:", var3);
        console.log("- Level 2:", var2);
        console.log("- Level 1:", var1);
        console.log("- Global:", global);
      }

      level3();
    }

    level2();
  }

  level1();
}

visualizeScope();

주의사항과 베스트 프랙티스

1. 메모리 누수 방지

// ❌ 메모리 누수: 전역 변수에 클로저 할당
let globalClosure;

function createLeak() {
  const hugeArray = new Array(1000000).fill("메모리 낭비");

  globalClosure = function() {
    // hugeArray를 참조하므로 GC가 수거 불가
    console.log(hugeArray.length);
  };
}

createLeak();
// hugeArray는 globalClosure가 살아있는 한 메모리에 남음

// ✅ 해결: 명시적으로 참조 제거
globalClosure = null; // 이제 GC가 수거 가능

2. eval() 사용 금지

// ❌ eval은 실행 컨텍스트를 예측 불가능하게 만듦
function bad() {
  const x = 10;
  eval('var x = 20'); // 스코프 오염!
  console.log(x); // 20 (예상치 못한 변경)
}

// ✅ 대안: Function 생성자 또는 다른 방법
function good() {
  const x = 10;
  const fn = new Function('return x + 5');
  console.log(fn()); // ReferenceError (독립적인 스코프)
}

3. with 문 사용 금지

// ❌ with는 LexicalEnvironment를 임시로 변경해 성능 저하
with (Math) {
  console.log(sin(PI / 2)); // 1
}

// ✅ 대안: 구조 분해 할당
const { sin, PI } = Math;
console.log(sin(PI / 2)); // 1

4. strict mode 사용

'use strict';

// strict mode에서는:
// 1. this가 자동으로 전역 객체로 바인딩되지 않음
function test() {
  console.log(this); // undefined (비엄격: window)
}

// 2. 선언 없는 변수 할당 금지
// x = 10; // ReferenceError

// 3. eval이 별도 스코프 생성
eval('var y = 20');
// console.log(y); // ReferenceError

5. 화살표 함수의 this 이해

// ❌ 화살표 함수는 자체 this가 없음
const obj = {
  value: 42,
  method: () => {
    console.log(this.value); // undefined
    // this는 상위 스코프(전역)의 this
  }
};

// ✅ 일반 함수 사용
const obj2 = {
  value: 42,
  method: function() {
    console.log(this.value); // 42

    // 내부에서 화살표 함수는 OK
    setTimeout(() => {
      console.log(this.value); // 42 (method의 this 상속)
    }, 100);
  }
};

참고 자료

공식 명세 및 문서

심화 학습

관련 문서

이 저장소의 다른 JavaScript 문서들:

마치며

실행 컨텍스트는 JavaScript의 가장 핵심적인 개념입니다. 이를 이해하면:

호이스팅의 원리를 설명할 수 있습니다 ✅ 스코프와 클로저가 어떻게 작동하는지 알 수 있습니다 ✅ this 바인딩이 결정되는 시점을 이해합니다 ✅ var, let, const의 차이를 명확히 구분할 수 있습니다 ✅ 메모리 관리와 성능 최적화를 할 수 있습니다

실행 컨텍스트는 눈에 보이지 않는 추상적인 개념이지만, JavaScript의 모든 동작을 설명하는 열쇠입니다. 이 문서를 통해 JavaScript가 코드를 실행하는 방식을 깊이 이해하셨기를 바랍니다!

댓글