대상 독자
- 자바스크립트 이벤트 루프에 대한 지식이 있는 분
- 렉시컬 스코프와 실행 컨텍스트에 대한 지식이 있는 분
- 호이스팅에 대한 지식이 있는 분
실행 컨텍스트 (Execution Context)
Lexical Environment에 대해 알아보기에 앞서, 자바스크립트의 실행은 실행 컨텍스트(Execution Context)에 의해서 일어납니다. 실행 컨텍스트란 실행할 코드에 제공할 환경 정보들을 모아놓은 객체입니다. 즉, 코드를 실행하기 위해 필요한 정보들이 들어가 있는 공간입니다. 이 공간들을 콜 스택에 쌓아 올리면서 자바스크립트를 실행합니다.
실행 컨텍스트를 구성할 수 있는 방법으로는 전역 공간, eval, 함수 등이 있습니다. 우리가 흔히 실행 컨텍스트를 구성하는 방법으로 대표적으로는 함수를 실행하는 방법이 있습니다. function 키워드를 통해 함수를 실행하면, 실행 컨텍스트가 만들어지는 것이죠.
실행 컨텍스트는 Lexical Environment와 Variable Environment, this로 이루어집니다. 그중 Lexical Environment에 대해 알아보겠습니다.
Lexical Environment
Lexical Environment는 Variable Environment의 복사본이자, 스코프를 구분하여 식별자를 관리하는 역할을 담당합니다. Lexical Environment은 크게 Environment Record와 Outer Environment Reference로 이루어져 있습니다.
Environment Record
Environment Record는 Lexical Environment에서 만들어진 식별자와 이 식별자에 바인딩된 값이 들어있습니다. 즉, 우리가 let이나 const로 생성한 변수와 함수가 이곳에서 관리되고 있습니다. 자바스크립트 코드가 평가될 때마다, 혹은 함수나 블록, try-catch 문을 발견할 때마다 새로운 식별자가 바인딩되어 이곳에 순서대로 기록됩니다.
Environment Record는 Object Environment Record와 Declarative Environment Record로 또다시 나눠집니다.
Object Environment Record는 Record를 객체 형식으로 저장할 때 사용됩니다. 주로 전역 객체를 선언할 때 사용되며, 전역 객체 이외의 값에 사용되는 경우 객체 특성상 불변성이 깨질 수 있어 전역 객체에만 권장된다고 합니다.
전역 객체는 전역 실행 컨텍스트에서 활용됩니다. 전역 객체의 종류로는 브라우저의 window, Node.js의 global 객체 등이 있습니다.
전역 객체 이외에도 with문에서 사용되는데, with문의 사용은 권장되지 않으므로 넘어가도록 하겠습니다.
Declarative Environment Record는 해당 스코프 내에 있는 식별자들의 바인딩을 관리합니다.
호이스팅
Environment Record에 변수 정보를 모두 수집하더라도 아직 실행 컨텍스트는 실행되기 전의 상태입니다. 코드가 실행되지 않았음에도 자바스크립트 엔진은 이미 변수 정보들을 모두 알고 있는 것이죠. 이때, 호이스팅이 발생됩니다. 호이스팅은 '끌어올려지다'라는 의미로, (실제로 코드가 끌어올려지는 것은 아니고) 식별자가 위로 끌어올려지는 듯한 현상이 발생합니다. 아직 선언되기 전의 위치임에도 불구하고 해당 코드가 끌어올려져 실행될 수 있는 현상입니다.
흔히 변수 중 var로 선언된 변수만 호이스팅 된다고 알고 계실 텐데요, 사실 let과 const로 선언된 변수도 호이스팅이 됩니다. 그전에, Temporal Dead Zone(TDZ)에 대해 알아보겠습니다.
TDZ (Temporal Dead Zone)
TDZ는 일시적인 사각지대라는 뜻입니다. 변수가 만들어지고 초기화가 시작되는 지점까지의 구간을 TDZ라고 합니다.
변수는 총 3단계에 걸쳐 생성됩니다.
- 선언 단계 : 스코프와 변수가 생성되고, 스코프가 변수를 참조합니다. 변수는 uninitialized 상태가 됩니다.
- 초기화 단계 : 변수가 가질 값을 위해 메모리에 공간을 할당합니다. 이때, 초기화되는 값은 undefined입니다.
- 할당 단계 : 변수에 값을 할당합니다.
var 키워드를 사용하여 선언된 변수의 경우, 자바스크립트 엔진의 컴파일 단계에서 var 키워드를 찾았을 때 선언 단계와 초기화 단계가 동시에 이루어집니다. 선언이 되자마자 undefined로 값이 초기화되어 Lexical Environment에 저장되는 것이죠.
console.log(a) // undefined
var a = 'hi'
여기서 주목할 점은, 호이스팅은 선언부와 할당부 중 선언부만 끌어올려지는 현상입니다. console.log(a)의 결과로 hi가 아닌 undefined가 출력되는 것을 보면 선언부만 호이스팅 되는 것을 알 수 있습니다.
반대로 function 키워드를 사용하여 선언된 함수는 TDZ의 영향을 받지 않아 호이스팅이 될 때 함수 전체를 끌어올립니다. 따라서 함수는 선언 이전에도 사용할 수 있게 됩니다.
반면, let과 const 키워드를 사용하여 선언된 변수의 경우 호이스팅 된 뒤 선언 단계와 초기화 단계가 나눠서 진행됩니다. 상단으로 끌어올려져 선언이 되었지만 초기화되지 않아 uninitialized 상태로 남아있습니다. 따라서 자바스크립트 엔진이 코드를 읽어 내려가다 let과 const 키워드를 만나기 전까지는 변수를 사용할 수 없습니다. 이렇게 변수가 생성된 뒤 초기화되기 전까지 접근할 수 없는 것을 TDZ라고 합니다.
console.log(a) // ReferenceError : a is not defined
let a = 'hi'
console.log(b) // ReferenceError : b is not defined
const b = 'hello'
let a;
console.log(a); // undefined
a = 'hi';
const b;
console.log(b); // SyntaxError: Missing initializer in const declaration
b = 'hello';
Outer Environment Reference
Outer Environment Reference는 해당 Lexical Environment의 외부 환경을 기록해 놓은 공간입니다. 자신보다 바깥에 위치한 외부 환경을 참조할 수 있게 되어, 외부의 변수나 함수 등의 식별자를 해당 Lexical Environment에서 사용할 수 있게 됩니다. 이것을 "스코프 체인"이라고 부릅니다.
스코프 체인
예시로 아래의 코드를 가져왔습니다.
let x = "global x";
function outer() {
let y = "outer y";
function inner() {
let z = "inner z";
console.log(x); // global x
console.log(y); // outer y
console.log(z); // inner z
}
inner();
}
outer();
이 코드에서는 let 키워드를 통해 x, y, z 가 선언, 할당되고 있습니다. 그리고 function 키워드를 통해 outer와 inner 함수가 생성되었습니다. 가장 안쪽에 위치한 inner 함수에서 console.log를 통해 x, y, z를 출력해 봤을 때, Reference error가 출력되는 것이 아니라 앞서 할당해 놓은 값이 잘 출력되는 이유는 바로 스코프 체인 때문입니다. 하위 스코프 inner는 Outer Environment Reference에 상위 스코프인 outer를 기록해 놓고, outer는 Outer Environment Reference에 상위 스코프인 전역 스코프를 기록합니다.
inner의 Environment Record에서 x를 찾고, 값이 없다면 Outer Environment Reference에 따라 outer의 Environment Record에서 x를 찾고, 값이 없으니 다시 outer의 Outer Environment Reference에 따라 전역 스코프의 Environment Record에서 x를 찾습니다. Outer Environment Reference는 두 스코프를 잇는 사다리 같은 역할을 하는 셈입니다.
여기서, 전역 실행 컨텍스트(Global Execution Context)의 Outer Environment Reference는 null입니다. 자바스크립트는 Lexical Scope를 따르기 때문에, 함수가 선언되었을 때의 위치를 기준으로 스코프가 결정됩니다. 따라서 전역 실행 컨텍스트는 가장 바깥쪽, 최상위에 선언되어 있기 때문에 더 이상 상위 스코프가 존재하지 않아 Outer Environment Reference는 null로 결정됩니다.
참고 자료
ECMA-262-5 in detail. Chapter 3.2. Lexical environments: ECMAScript implementation.
[자바스크립트 스터디] 자바스크립트에서의 호이스팅 (Hoisting)
[Javascript] 호이스팅(hoisting)에 대하여
자바스크립트 Deep Dive 13장. 스코프
코어 자바스크립트 02. 실행 컨텍스트
'IT (프론트엔드)' 카테고리의 다른 글
패턴으로 알아보는 전역 상태 라이브러리 (1) | 2024.12.23 |
---|---|
Github Actions로 CI 구축 & 최적화 (CI 시간 단축, Chromatic 자동 배포, Github Bot으로 배포 주소 남기기) (0) | 2024.12.23 |
[React Deep Dive] 5. 상태 관리 (3) | 2024.11.06 |
CSR vs SSR: 웹 성능 최적화와 혼합 렌더링 방식, Hydration의 역할 (3) | 2024.10.13 |
프론트엔드 에러는 왜 추적해야 할까? (Sentry) (0) | 2024.08.18 |