스코프와 실행 컨텍스트

jj·2026년 9월 26일

스코프

1. 스코프란?

변수가 참조될 수 있는 유효범위를 스코프라고 한다.
자바스크립트를 비롯한 대부분의 언어에서 변수는 자신이 정의된 위치에 따라 다른 코드가 자신을 참조할 수 있는 범위가 결정되는데 이를 렉시컬 스코프(Lexical scope)=정적 스코프라고 한다.

var x = 1;

function foo() {
  var x = 10;
  bar();
}

function bar() {
  console.log(x);
}

foo(); // ? foo()는 전역에 정의되었으니 1
bar(); // ? bar()도 전역에 정의되었으니 1

var는 같은 스코프 내에서 같은 변수를 중복 선언할 수 있지만 let, const는 선언할 수 없다.

2. 스코프의 종류

전역(코드의 가장 바깥 영역)에 위치하면 전역 스코프, 지역(함수 내부)에 위치하면 지역 스코프이다.
전역은 어디서든지 참조할 수 있고 지역은 자신의 지역 스코프와 그 하위 스코프에서만 참조할 수 있다.

3. 스코프 체인

스코프가 계층적으로 연결된 것을 스코프 체인이라고 한다.
변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프로 이동하며 선언된 변수를 검색한다. 가장 하위의 스코프가 상위 스코프 변수를 사용할 수 있거나 전역 스코프의 변수는 어디에서나 사용할 수 있는 원리이다.
(사실 변수뿐 아니라 함수도 해당되기 때문에 변수 참조보다는 식별자 참조라고 말하는 편이 맞다.)

4.함수 레벨 스코프

var는 함수의 영역으로 스코프가 정해지는 함수 레벨 스코프, const, let은 {} 괄호로 범위가 정해지는 블록 레벨 스코프이다.

함수 레벨 스코프의 문제점:

var x = 1;

if (true) {
  // var 키워드로 선언된 변수는 함수의 코드 블록(함수 몸체)만을 지역 스코프로 인정한다.
  // 함수 밖에서 var 키워드로 선언된 변수는 코드 블록 내에서 선언되었다 할지라도 모두 전역 변수다.
  // 따라서 x는 전역 변수다. 이미 선언된 전역 변수 x가 있으므로 x 변수는 중복 선언된다.
  // 이는 의도치 않게 변수 값이 변경되는 부작용을 발생시킨다.
  var x = 10;
}

console.log(x); // 10
var i = 10;

// for 문에서 선언한 i는 전역 변수다. 이미 선언된 전역 변수 i가 있으므로 중복 선언된다.
for (var i = 0; i < 5; i++) {
  console.log(i); // 0 1 2 3 4
}

// 의도치 않게 변수의 값이 변경되었다.
console.log(i); // 5

따라서 의도치 않은 동작을 피하기 위해 var는 지양하고 let, const를 사용해야 한다.

실행 컨텍스트

1. 소스코드의 타입

자바스크립트 엔진은 소스코드를 4가지 타입으로 구분해서 각각 다른 방식으로 실행 컨텍스트를 생성한다.

전역 코드: 파일 최상위에 위치한 코드. var로 선언한 변수와 함수는 전역 객체(브라우저는 window)의 프로퍼티가 된다.
함수 코드: 함수 내부의 코드. 독립적인 지역 스코프를 형성하며, 이 지역 스코프는 상위 스코프와 이어진 스코프 체인의 일부가 된다.
eval 코드: eval 함수에 인수로 전달되어 실행되는 코드. strict mode에서 동작하며 실무에서는 거의 사용하지 않는다.
모듈 코드: import/export를 사용하는 모듈 파일 단위의 코드. 독립적인 모듈 스코프를 가진다.

2. 소스코드의 평가와 실행

자바스크립트 엔진은 소스코드를 한 번에 실행하지 않고 평가와 실행 두 단계로 나눠 처리한다.

console.log(x); // undefined
var x = 5;

평가 단계: 실행에 앞서 var 선언, 함수 선언문 등을 미리 등록한다. (호이스팅) var로 선언된 변수는 이 시점에 undefined로 초기화된다.
실행 단계: 코드를 순차적으로 실행하며 변수 값 참조 및 할당이 이루어진다. 필요한 식별자는 평가 단계에서 등록된 것을 참조한다.

3. 실행 컨텍스트의 역할과 구조

실행 컨텍스트 스택 (실행 컨텍스트가 쌓인 스택)
└─ 실행 컨텍스트들을 호출 순서대로 관리
    │
    └─ 실행 컨텍스트 (코드가 실행되는 데에 필요한 환경들을 모아놓은 객체)
        │
        ├─ 구성
        │   └─ 렉시컬 환경 (Lexical Environment)
        │        ↓
        │        ├─ 환경 레코드 (Environment Record)
        │        │   ├─ 변수/함수 등의 바인딩 관리
        │        │   └─ this 바인딩 (this 관련 정보)
        │        │
        │        └─ 외부 렉시컬 환경에 대한 참조 (Outer Lexical Environment Reference)
        │            → 상위 렉시컬 환경
        │
        └─ 역할
            ├─ 코드 실행 상태 관리
            ├─ 식별자 관련 환경 관리
            └─ this 등 실행에 필요한 정보 관리

4. 실행 컨텍스트의 생성과 식별자 검색 과정

var x = 1;
const y = 2;

function foo (a) {
  var x = 3;
  const y = 4;

  function bar (b) {
    const z = 5;
    console.log(a + b + x + y + z);
  }
  bar(10);
}

foo(20); // 42
  1. 전역 객체 생성: 코드 실행 전 console 등 빌트인 포함해서 먼저 만들어짐

  2. 전역 코드 평가: 전역 실행 컨텍스트 push, 전역 렉시컬 환경 생성
    var x → 즉시 undefined
    const y → 등록만, 값은 미할당(TDZ)
    외부 참조 → null

  3. 전역 코드 실행: x=1, y=2 대입 → foo(20) 호출

  4. foo 평가: foo 실행 컨텍스트 push (스택: [전역, foo]). a, x, y, bar 등록. 외부 참조 = 정의된 곳인 전역

  5. foo 실행: a=20, x=3, y=4 → bar(10) 호출
    → bar 평가 — bar 실행 컨텍스트 push (스택: [전역, foo, bar]). b, z 등록. 외부 참조 = 정의된 곳인 foo

  6. bar 실행 → 식별자 검색 (console.log(a+b+x+y+z))
    bar → 없으면 foo → 없으면 전역, 순서로 검색
    → 20+10+3+4+5 = 42

  7. 소멸: bar 종료 → pop (스택: [전역, foo]) → foo 종료 → pop (스택: [전역])
    실행 컨텍스트 내려감 ≠ 렉시컬 환경 내려감. 참조가 남아있으면 실행 컨텍스트는 유지됨 → 클로저의 원리

한줄 요약: 전역 생성 → 전역 평가/실행 → foo 호출 시 평가(외부참조=전역)/실행 → bar 호출 시 평가(외부참조=foo)/실행 → 식별자는 자기 스코프부터 위로 검색 → 종료 시 bar부터 역순 pop

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

var는 함수 단위로만 스코프가 구분되는 함수 레벨 스코프이며 let/const는 if, for, while 등 {} 블록 단위로 구분되는 블록 레벨 스코프다.

let x = 1;

if (true) {
  let x = 10;
  console.log(x); // 10
}

console.log(x); // 1

let/const는 블록 진입 시 해당 블록만을 위한 독립된 렉시컬 환경이 새로 생성되며 부 참조는 기존 렉시컬 환경(전역)을 가리킨다. 블록 종료 시 원래 렉시컬 환경으로 복귀한다.

6. 클로저

자바스크립트는 함수의 상위 스코프를 호출 위치가 아닌 정의 위치로 결정한다(렉시컬 스코프). 함수 정의 시점에 [[Environment]] 슬롯에 상위 스코프 정보가 저장되기 때문에 실행 컨텍스트가 스택에서 제거된 이후에도 해당 렉시컬 환경은 참조가 남아있는 한 유지된다. 이것이 클로저가 성립하는 메커니즘이다.

function foo() {
  var a = 10;
  function bar() {
    console.log(a);
  }
  return bar;
}

const myFunc = foo(); // 여기서 foo,bar는 이미 스택에서 내려간 시점
myFunc(); // 렉시컬 환경이 소멸되지 않은 덕분에 10을 제대로 불러올 수 있다.

bar는 정의되는 시점에 [[Environment]]에 foo의 렉시컬 환경을 저장해둔다. foo의 실행이 종료되어 실행 컨텍스트가 스택에서 제거되어도 bar(myFunc)가 그 렉시컬 환경을 계속 참조하고 있으므로 해당 렉시컬 환경은 소멸되지 않는다. 따라서 이후 myFunc() 호출 시에도 foo의 렉시컬 환경에서 a=10을 정상적으로 찾아낼 수 있다.


추가적인 내용

호이스팅이란?

선언이 코드 실행 전 평가단계에서 먼저 처리되어서, 선언문보다 앞에서도 식별자를 참조할 수 있는 것처럼 보이는 현상이다.

호이스팅은 왜 일어나나? (실행 컨텍스트와의 연관)
실행 컨텍스트가 만들어질 때 엔진은 코드를 바로 실행하지 않고, 먼저 평가 단계에서 그 스코프의 선언들을 스캔해 렉시컬 환경의 환경 레코드에 식별자를 등록한다. 그다음 실행 단계에서 코드를 한 줄씩 실행하며 값을 할당한다. 그래서 실행 단계가 시작될 때는 이미 식별자가 등록되어 있다. 호이스팅은 이 "평가 → 실행" 2단계 구조의 부수 효과이다.

🔍 "let/const는 호이스팅이 안 되는 거 아니야?"
된다. 선언문을 실행하기 전까지는 초기화되지 않은 상태라 접근하면 에러가 나는데 이 구간이 TDZ(Temporal Dead Zone)이다.

let x = 1;
{
  console.log(x); // ReferenceError (바깥 x=1이 아니라, 이 블록의 x가 TDZ 상태)
  let x = 2;
}
profile
frontend

0개의 댓글