← 글 목록

notes

23장. 실행 컨텍스트

모던 JS Deep Dive — 실행 컨텍스트·렉시컬 환경과 스코프 동작 정리.

참고: 이웅모 《모던 자바스크립트 Deep Dive》 23장 · [MDN Execution model](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop)

코드가 실행될 때마다 실행 컨텍스트가 쌓인다. 스코프가 지도라면, 실행 컨텍스트는 지금 서 있는 방과 그 방의 환경 레코드다. 24 클로저의 직전 보스.

flowchart TB
  EVAL[평가: 실행 준비] --> RUN[실행]
  STACK[콜 스택] --> G[전역 컨텍스트]
  STACK --> F1[함수 컨텍스트]
  STACK --> F2[중첩 함수]
  LE[렉시컬 환경] --> ER[환경 레코드]
  LE --> OUTER[외부 렉시컬 환경 참조]

23.1 소스코드의 타입

전역 코드, 함수 코드, eval 코드, 모듈 코드. 타입이 다르면 컨텍스트 생성 규칙도 조금씩 다르다. 일상은 전역·함수·모듈.

23.2 소스코드의 평가와 실행

엔진은 평가(준비) 후 실행한다. 평가에서 실행 컨텍스트를 만들고 식별자 바인딩을 등록한다(호이스팅·TDZ의 무대). 실행에서 코드를 한 줄씩 돌린다.

23.3 실행 컨텍스트의 역할

  • 무엇이 유효한 식별자인지(스코프)
  • 식별자 검색 경로
  • this 바인딩(22)
  • 코드 실행 순서의 단위

함수를 호출하면 새 컨텍스트가 생기고, return/끝나면 파괴(단, 환경이 클로저로 붙잡히면 레코드는 남음 → 24).

23.4 실행 컨텍스트 스택 (콜 스택)

호출하면 push, 끝나면 pop. 맨 위가 “지금 실행 중”. 재귀가 깊으면 스택 오버플로. 비동기는 스택이 비는 타이밍과 태스크 큐가 이어진다(Event Loop 노트).

function a() { b(); }
function b() { console.log('b'); }
a();
// push 전역 → push a → push b → pop b → pop a

23.5 렉시컬 환경

실행 컨텍스트가 가리키는 환경. 두 조각:

  • 환경 레코드 — 그 스코프의 식별자 바인딩(변수·함수·arguments 등)
  • 외부 렉시컬 환경 참조 — 바깥 스코프로의 링크(스코프 체인)

“작성 위치”가 외부 참조를 정한다 = 렉시컬 스코프.

23.6 생성과 식별자 검색 — 이야기로

23.6.1–3 전역

전역 객체 준비 → 전역 실행 컨텍스트·전역 렉시컬 환경 생성 → 전역 코드 평가(바인딩 등록) → 실행. var/함수 선언은 전역 객체와 엮이는 경로가 있고, let/const는 전역 환경 레코드의 다른 칸(15).

23.6.4–9 함수 호출 시

  • 함수 실행 컨텍스트 생성
  • 함수 렉시컬 환경 생성 — 환경 레코드에 매개변수·지역·arguments 등
  • 외부 참조 = 함수 객체의 [[Environment]]가 가리키던 곳(정의 위치)
  • this 바인딩(호출 방식)
  • 코드 실행, 식별자는 자기 레코드 → 외부 참조로 검색
  • 종료 시 스택 pop
var x = 'global';
function foo() {
  var x = 'foo';
  function bar() {
    console.log(x); // foo — 렉시컬, 호출 위치 아님
  }
  bar();
}
foo();

WHY: bar의 외부 환경은 foo의 환경. 호출 스택의 “누가 불렀나”와 식별자 검색 경로는 다르다.

23.6.10 전역 종료

앱/탭이 끝나는 쪽에 가깝다. 모듈은 파일별 모듈 환경.

23.7 실행 컨텍스트와 블록 레벨 스코프

let/const/블록은 블록 렉시컬 환경을 끼워 넣는다. for 루프의 let이 반복마다 새 바인딩인 이유도 여기(15·24).

{
  let x = 1;
  // 블록 환경에 x
}
// 블록 환경 pop — x 검색 불가

LexicalEnvironment / VariableEnvironment (감각)

옛 모델에서 VariableEnvironment는 var 바인딩, LexicalEnvironment는 let/const·블록 등. 지금은 “환경 레코드 종류가 다르다” 정도로 잡고, TDZ는 “방은 생겼는데 가구 조립 전”.

호이스팅을 컨텍스트로 다시

컨텍스트 생성(평가) 단계에서 바인딩이 먼저 준비된다.

  • var → undefined로 등록
  • 함수 선언 → 함수 객체로 등록
  • let/const → 등록되나 TDZ

예제

function outer() {
  const x = 1;
  function inner() { console.log(x); }
  return inner;
}
const f = outer();
f(); // 1 — outer 컨텍스트는 pop, 환경은 inner가 붙잡음 → 24
function sum(a, b) {
  // 매개변수 a,b가 함수 환경 레코드에
  return a + b;
}

헷갈리던 장면 / 함정

  • 스코프 = 실행 컨텍스트라고 동일시 (지도 vs 지금 방)
  • this를 스코프처럼 찾기
  • 클로저 없이 “함수가 끝나면 무조건 전부 파괴”
  • 블록 스코프와 함수 스코프 혼동

표

질문 → 자답

Q. 전역 컨텍스트는 언제 끝나나? A. 앱/탭과 함께. 모듈은 파일별 모듈 환경이 길게 산다.

Q. 실행 컨텍스트 vs 클로저? A. 컨텍스트는 pop 되어도, 환경 레코드를 참조하는 함수가 있으면 그 환경은 산다 → 24.

웹 보강 메모

스펙 Execution Contexts와 MDN 이벤트 루프를 나란히 두고, 콜 스택을 세로로·LE를 옆 상자로 낙서했다.

실무로 바로

  • 버그 리포트에 “어느 컨텍스트에서 this/식별자가 뭐였는지”
  • 재귀·깊은 콜 체인 주의
  • 클로저 누수 = 환경 레코드를 붙잡는 참조 검토

평가 vs 실행을 한 번 더

평가 단계에서 엔진은 “이 코드가 돌 방”을 만든다. 방 장부(환경 레코드)에 이름을 올려 두고, let/const는 TDZ 칸에, var는 undefined로, 함수 선언은 바로 함수로 채운다. 실행 단계에 들어서야 할당문·호출문이 움직인다.

“호이스팅됐다”는 말은 코드가 물리적으로 위로 이동했다는 뜻이 아니라, 평가가 실행보다 먼저 바인딩을 준비했다는 뜻에 가깝다.

this도 컨텍스트 구성 요소

호출 방식이 컨텍스트 생성 시 this를 정한다(22). 같은 함수 객체라도 호출마다 다른 컨텍스트·다른 this가 가능하다. 스코프 체인(렉시컬)과 this(동적)를 한 장부에 섞어 적지 말 것.

모듈 코드의 방

ES 모듈은 파일마다 모듈 환경이 있다. import한 바인딩은 살아 있는 연결(live binding)에 가깝다. 전역 컨텍스트와는 칸이 다르다 — 14·20에서 말한 “파일이 칸”과 맞물린다.

식별자 검색 연습

const a = 1;
function f() {
  const a = 2;
  function g() {
    console.log(a); // 2
    // console.log(b); // 체인을 타고 전역까지 — 없으면 ReferenceError
  }
  g();
}
f();

검색은 현재 환경 레코드 → 외부 참조 → … → 전역. 프로토타입 체인(객체 키 검색, 19)과 축이 다르다. 식별자 vs 프로퍼티.

오늘 지운 미신

호이스팅 = 코드가 물리적으로 위로 → 생성 단계 바인딩 준비. 함수가 끝나면 그 안의 모든 것이 즉시 증발 → 참조가 있으면 환경은 남는다(24).

전역·함수·블록이 스택에 쌓이는 장면

let g = 0; // 전역 환경
function outer(a) {
  const x = 1; // outer 함수 환경
  if (true) {
    const y = 2; // 블록 환경
    console.log(g, a, x, y);
  }
  // y 검색 실패
}
outer(10);

실행 중 스택 맨 위는 outer(또는 그 안 블록이 끼워진 상태). 검색은 장부를 위에서 밖으로.

비동기와의 경계

setTimeout 콜백이 돌 때 콜 스택에는 그 콜백의 컨텍스트만 새로 올라간다. outer의 컨텍스트는 이미 pop 되었을 수 있다. 그래도 클로저로 환경을 붙잡았다면 식별자는 산다. “스택에 없다 = 값이 없다”가 아니다.

실무 체크리스트

  • 재귀 깊이 / 스택 트레이스 읽기
  • “이 변수는 어느 환경 레코드에 있나?”를 말로 설명
  • this와 식별자를 같은 화살표로 그리지 않기
  • 모듈 경계에서 전역 오염 여부 확인

스펙 용어를 내 말로

Execution Context = 지금 실행 단위. Lexical Environment = 그 단위가 붙잡는 장부+바깥 주소. Environment Record = 실제 이름→값 맵(또는 그 계열). Declarative / Object environment record = let·const·함수 vs 전역 객체에 붙는 바인딩 같은 구분.

면접용 암기가 아니라, 디버거 Scope 창의 레이블을 해석하는 어휘다.

여기서 잠깐 💁‍♂️

다음 장: 함수가 환경을 붙잡는 클로저.