본문으로 건너뛰기

실행 컨텍스트와 스코프

자바스크립트는 코드를 실행할 때 **실행 컨텍스트(Execution Context)**를 통해 실행 상태와 환경 정보를 관리합니다.

다음 코드에서 선언문보다 먼저 channel을 읽을 수 있는 이유도, 코드 실행에 앞서 변수 정보가 준비되기 때문입니다.

console.log(channel) // undefined

var channel = 'YouTube'

console.log(channel) // 'YouTube'

다만 선언 방식에 따라 실행 전에 준비되는 상태가 다릅니다.

선언 방식해당 스코프의 본문 실행 전 상태선언문 이전에 접근하면
varundefined로 초기화undefined를 읽음
let, const바인딩은 있지만 초기화되지 않음ReferenceError
함수 선언문함수 객체로 초기화호출 가능
var에 담은 함수 표현식변수만 undefined로 초기화호출하면 TypeError
let, const에 담은 함수 표현식변수의 바인딩이 초기화되지 않음접근하면 ReferenceError

이 글은 하루의 실행 컨텍스트 강의를 바탕으로, 변수가 준비되는 과정과 사용할 변수를 찾는 과정을 정리합니다. 예제는 각각 독립적으로 실행하는 일반 스크립트를 기준으로 합니다.

Record와 Outer

강의에서는 실행에 필요한 환경을 두 요소에 집중해 설명합니다.

  • Record — 환경 레코드(Environment Record): 식별자의 바인딩과 초기화 상태 등을 관리합니다.
  • Outer — 외부 환경 참조: 현재 환경에서 이름을 찾지 못했을 때 탐색할 바깥 환경을 연결합니다.

바인딩은 식별자와 값을 연결하는 관계입니다. 이 두 요소를 기준으로 호이스팅과 스코프 체인을 살펴보겠습니다.


실행 컨텍스트를 따라가 보면​

1. 함수를 호출하면 실행 컨텍스트가 만들어집니다​

전역 코드를 실행할 때는 전역 실행 컨텍스트가, 일반 함수를 호출할 때는 해당 호출을 위한 함수 실행 컨텍스트가 만들어집니다.

이 컨텍스트들은 **콜 스택(Call Stack)**에 쌓이며, 맨 위의 컨텍스트가 현재 실행 중인 코드를 나타냅니다. 함수 안에서 다른 함수를 호출하면 새 컨텍스트가 쌓이고, 그 함수가 반환하면 스택에서 빠집니다. 이후 호출한 쪽의 실행을 이어갑니다. MDN · 실행 컨텍스트와 콜 스택

즉 콜 스택으로 실행 흐름을 관리하고, 실행 중에 필요한 변수 정보는 컨텍스트가 참조하는 환경에서 확인합니다.

실행 컨텍스트는 직접 꺼내 쓰는 객체가 아닙니다

실행 컨텍스트는 명세에서 코드의 실행을 설명하기 위한 장치입니다. 개발자가 자바스크립트 코드로 직접 생성하거나 접근하는 일반 객체는 아닙니다.


2. 생성 단계에서 준비하고, 실행 단계에서 값을 읽습니다​

강의에서는 코드 실행 과정을 **생성 단계(Creation Phase)**와 **실행 단계(Execution Phase)**로 나누어 설명합니다.

구분하는 일
생성 단계실행 환경을 준비하고 해당 스코프의 선언 정보를 처리합니다.
실행 단계코드를 순서대로 실행하며 값을 읽거나 변경합니다.

앞의 var channel 예제에서는 생성 단계에 channel의 바인딩이 만들어지고, undefined로 초기화됩니다.

실행 단계에서는 다음 순서로 동작합니다.

  1. 첫 번째 console.log(channel)에서 undefined를 읽습니다.
  2. var channel = 'YouTube'의 대입을 실행하며 값을 바꿉니다.
  3. 두 번째 로그에서 'YouTube'를 읽습니다.

이처럼 선언문이 위로 올라간 것처럼 보이는 동작을 **호이스팅(Hoisting)**이라고 합니다. 실제 코드 줄을 이동시키는 것이 아니라, 본문 실행 전에 선언 정보를 처리한 결과입니다. MDN · Hoisting


3. 선언과 초기화는 같은 단계가 아닙니다​

var, let, const의 차이를 이해하려면 **선언(Declaration)**과 **초기화(Initialization)**를 구분해야 합니다.

구분의미
선언식별자의 바인딩을 만듭니다.
초기화만들어진 바인딩에 처음 사용할 값을 설정합니다.
할당대입을 통해 값을 설정하거나 변경합니다.

var는 본문 실행 전에 바인딩이 만들어지고 undefined로 초기화됩니다. 반면 let과 const는 바인딩만 만들어지고, 초기화는 해당 선언문이 실행되는 시점에 이루어집니다.

console.log(channel) // ReferenceError

let channel = 'YouTube'

첫 번째 줄에서 channel은 존재하지만 아직 초기화되지 않은 상태입니다. 따라서 값을 읽으려 하면 ReferenceError가 발생합니다.

스코프에 진입한 뒤 초기화가 완료되기 전까지 접근할 수 없는 기간을 **일시적 사각지대(Temporal Dead Zone, TDZ)**라고 합니다. 위 코드는 첫 줄에서 오류가 나므로 다음 줄까지 실행되지 않습니다. MDN · let과 TDZ

초기화되지 않은 상태와 undefined는 다릅니다

undefined는 읽을 수 있는 값입니다. 초기화되지 않은 바인딩은 값을 읽는 것 자체가 금지됩니다.

초기값 없이 let channel로 선언하면, 그 선언문을 실행할 때 undefined로 초기화됩니다.


4. 함수 선언문과 함수 표현식도 준비되는 값이 다릅니다​

**함수 선언문(Function Declaration)**으로 정의하면 선언문 이전에도 호출할 수 있습니다.

play() // '재생 시작'

function play() {
console.log('재생 시작')
}

스크립트 최상위의 함수 선언문은 본문 실행 전에 함수 객체가 만들어지고, 식별자도 그 함수 객체로 초기화됩니다. 따라서 첫 줄에서 play를 읽었을 때 이미 호출할 함수가 있습니다.

반면 **함수 표현식(Function Expression)**을 변수에 담으면 그 변수의 선언 방식에 영향을 받습니다.

play() // TypeError

var play = function () {
console.log('재생 시작')
}

첫 줄에서 play의 값은 undefined입니다. 아직 함수 표현식을 평가하고 그 결과를 변수에 할당하지 않았기 때문입니다. 이름은 읽을 수 있지만 undefined를 호출할 수는 없어서 TypeError가 발생합니다.

여기서 var를 const로 바꾸면 초기화되지 않은 play를 읽는 시점에 ReferenceError가 발생합니다. 화살표 함수로 바꾸어도 이 차이는 같습니다.

기준은 호출하는 시점에 해당 바인딩이 초기화되어 있고, 그 값이 호출 가능한 함수인가입니다. MDN · 함수의 호이스팅


5. 같은 이름이 여러 개라면 스코프 체인을 따라 찾습니다​

이번에는 전역과 함수 안에 같은 이름의 변수를 선언해 보겠습니다.

const light = '꺼짐'

function enterRoom() {
const light = '켜짐'

function checkRoom() {
console.log(light) // '켜짐'
}

checkRoom()
}

enterRoom()

코드에 적힌 식별자가 어떤 바인딩을 가리키는지 결정하는 과정을 **식별자 결정(Identifier Resolution)**이라고 합니다.

checkRoom에서 light를 읽을 때는 다음 순서로 찾습니다.

  1. checkRoom의 환경에서 light를 찾습니다.
  2. 없으므로 Outer를 따라 enterRoom의 환경으로 이동합니다.
  3. light를 찾았으므로 탐색을 멈추고 '켜짐'을 읽습니다.

전역의 light까지 계속 찾지는 않습니다. 안쪽의 같은 이름이 바깥 이름을 가리는 현상을 **변수 가리기(Shadowing)**라고 합니다.

이때 식별자를 참조할 수 있는 범위가 **스코프(Scope)**입니다. 식별자를 찾을 때 따라가는 환경의 연결을 스코프 체인(Scope Chain), 연결을 따라 탐색하는 과정을 **스코프 체이닝(Scope Chaining)**이라고 합니다. MDN · 렉시컬 스코프


6. 바깥 환경은 함수가 정의된 위치를 기준으로 연결됩니다​

앞의 예제에서는 함수를 정의한 위치와 호출한 위치가 같았습니다. checkRoom을 전역으로 옮기면 어떨까요?

const light = '꺼짐'

function checkRoom() {
console.log(light)
}

function enterRoom() {
const light = '켜짐'
checkRoom()
}

enterRoom() // '꺼짐'

여전히 enterRoom에서 호출하지만, 결과는 '꺼짐'입니다. 자바스크립트는 **함수가 정의된 위치를 기준으로 외부 스코프가 결정되는 렉시컬 스코프(Lexical Scope)**를 사용하기 때문입니다.

명세에서는 함수 객체를 만들 때 [[Environment]]에 정의 당시 환경을 저장합니다. 이후 호출 시 새 함수 환경을 만들고, 그 환경의 [[OuterEnv]]를 저장해 둔 환경에 연결합니다. 함수 생성 과정, 함수 환경 생성 과정

구분위 예제에서의 역할
콜 스택checkRoom이 반환한 뒤 enterRoom으로 돌아가 실행을 이어갑니다.
스코프 체인checkRoom에 없는 light를 전역 환경에서 찾습니다.

실행이 돌아갈 위치와 변수를 찾으러 갈 환경은 구분해야 합니다.

바깥에 값이 있어도 TDZ 오류가 발생할 수 있습니다
const light = '꺼짐'

function checkRoom() {
console.log(light) // ReferenceError
let light = '켜짐'
}

checkRoom()

checkRoom의 본문 실행 전에 안쪽 light의 바인딩이 만들어집니다. 이름을 찾았으므로 전역으로 넘어가지 않고, 그 바인딩의 값을 읽으려다 초기화되지 않아 오류가 발생합니다.

즉 이름이 없을 때 바깥 환경을 탐색합니다. 이름은 있지만 초기화되지 않은 경우에는 바깥 값을 대신 사용하지 않습니다. 명세에서도 바인딩을 찾는 과정과 값을 읽는 과정을 구분합니다.


정리​

  • 실행 컨텍스트: 코드 실행에 필요한 상태와 환경 정보를 관리합니다.
  • 생성 단계와 실행 단계: 선언 정보를 먼저 준비하고, 이후 코드를 실행하며 값을 읽거나 변경합니다.
  • 호이스팅과 TDZ: 선언 종류에 따라 초기화 시점이 달라지므로 선언문 이전 접근 결과도 달라집니다.
  • 함수 선언문과 함수 표현식: 호출 시점에 함수 객체가 준비되어 있는지 확인해야 합니다.
  • 식별자 결정과 스코프 체인: 현재 환경에서 이름을 찾고, 없으면 정의 위치에 따라 연결된 바깥 환경으로 이동합니다.

예상 질문​

let과 const는 호이스팅되지 않는 것 아닌가요?​

let과 const도 해당 스코프의 코드가 실행되기 전에 변수 이름이 미리 등록됩니다. 다만 이름이 등록되었다고 해서 바로 값을 읽을 수 있는 것은 아닙니다. var는 미리 undefined라는 값을 넣어 두지만, let과 const는 선언문을 실행할 때까지 값을 읽을 수 없는 상태로 둡니다. 이 구간이 TDZ이며, 여기서 변수에 접근하면 ReferenceError가 발생합니다.

즉, 변수 이름을 미리 등록하는 것과 처음 값을 넣어 사용할 수 있게 하는 것은 별개의 과정입니다. 예를 들어 let count = 1;은 이 문장이 실행되어야 count의 값으로 1을 읽을 수 있고, let count;는 이 문장이 실행되면 값이 undefined가 됩니다.

함수 표현식은 왜 TypeError가 나기도 하고 ReferenceError가 나기도 하나요?​

var에 담은 경우 선언문 이전의 값은 undefined이므로, 그것을 호출하려 하면 TypeError가 발생합니다. let이나 const에 담은 경우에는 초기화되지 않은 변수를 읽으려는 시점에 ReferenceError가 발생합니다.

콜 스택을 거슬러 올라가며 변수를 찾는 건가요?​

변수는 현재 환경에서 시작해 Outer로 연결된 환경을 따라 찾습니다. 콜 스택은 호출 관계를 관리하고, 스코프 체인은 함수가 정의된 위치에 따른 환경의 연결을 나타내므로 두 경로는 다를 수 있습니다.

스코프 체인의 끝까지 가도 이름을 찾지 못하면 어떻게 되나요?​

해당 식별자의 값을 읽으려 하면 ReferenceError가 발생합니다. 값이 undefined인 변수를 찾은 경우와는 다릅니다.


참고 자료​