함수는 선언 전 사용이 가능한데 클래스는 선언 전 사용하는게 불가능했다. 호이스팅과 관련된 개념이었는데 호이스팅이 정확히 무엇인지 잘 모른다는 것을 깨닫고 호이스팅을 공부해보았다. 또한 호이스팅을 이해하기 위해서 자바스크립트가 코드를 실행하는 과정을 이해할 필요가 있다고 느껴 처음부터 공부해보았다.
자바스크립트 엔진은 코드를 실행을 위해
실행 컨텍스트
라는 환경을 생성한다. 이때 2가지 일이 일어난다.
첫번째로는호이스팅
을 통해 코드에서 선언된 변수와 함수를 찾아 미리 처리한다. 두번째로는스코프 체인
이 형성되어 실행 컨텍스트의 렉시컬 환경을 서로 연결해 변수를 찾을 수 있게 만든다.
2가지 설정이 완료되면 코드가 한 줄씩 실행되고, 변수에 접근할 때마다 스코프 체인을 따라 적절한 값을 찾아 사용한다.
자바스크립트 엔진이 코드를 실행할 때는 크게 두 단계를 거친다. 이 두 단계는 생성 단계와 실행 단계가 있는데 각 단계는 다음과 같다.
실행 컨텍스트
가 생성된다.
실제 구조
ExecutionContext = {
// 변수 환경
VariableEnvironment: {
environmentRecord: {
// var로 선언된 변수
x: undefined,
a: undefined
},
outer: <outer scope reference>
},
// 렉시컬 환경
LexicalEnvironment: {
environmentRecord: {
// let, const로 선언된 변수
y: <uninitialized>,
b: <uninitialized>
},
outer: <outer scope reference>
},
// ThisBinding
ThisBinding: <this value>
}
변수 환경
environmentRecord
: 변수 저장소로 초기 상태를 스냅샷처럼 유지하고, var 변수만 관리outer(outerEnvironmentReference)
: 상위 스코프의 환경을 참조하는 포인터로, 함수 단위의 스코프 체인을 형성렉시컬 환경
environmentRecord
: 변수 저장소로 실시간으로 변경 사항을 반영하고, let, const 변수 관리outer(outerEnvironmentReference)
: 상위 스코프의 환경을 참조하는 포인터로, 블록 단위의 스코프 체인을 형성코드가 한 줄씩 실행되는 단계
실행 가능한 코드를 형상화하고 구분하는 추상적인 개념으로, 실행 가능한 코드가 실행되기 위해 필요한 환경을 의미한다. 즉, 코드를 실행하기 위해 모든 정보를 가지고 있는 환경으로 생각하면 된다.
실행 컨텍스트가 활성화되는 시점에서 다음과 같은 일이 생긴다.
- 호이스팅이 발생해서 선언된 변수를 위로 끌어올리는 것처럼 동작한다.
- 외부 환경 정보를 구성한다.
- this 값을 설정한다.
var a = 1;
function outer () {
function inner () {
console.log(a); // undefined
var a = 3;
console.log(a); // 3
}
inner();
console.log(a); // 1
}
outer();
console.log(a); // 1
위 코드에서 console.log로 출력되는 값은?
inner 함수에서 console.log(a)를 출력하면undefined
가 출력된다. var a = 1로 바깥에서 선언해두었는데 왜 undefined로 출력되는지 이해하기 위해서는 변수의 스코프를 이해해야한다.
inner 함수 내에서는 var a = 3이라는 선언을 해주고 있다. 전역 스코프에서는 var a = 1이 실행되어 전역 변수 a가 1로 초기화된다. 그런데 inner 함수가 실행되면 자바스크립트 엔진은 var a라는 선언을 발견하게 되고, 이때호이스팅
으로 인해 a를 undefined로 초기화하기 때문에 console.log(a)를 하면 undefined가 출력된다.function inner() { // 호이스팅으로 인해 아래와 같이 재구성 var a; console.log(a); // undefined a = 3; console.log(a); // 3 }
- 위와 같이 재구성되어 a는 undefined가 출력된다.
- 그렇다면 만약 inner 함수 내부에 a를 선언해주는 코드를 삭제하게 되면, 이때는 전역 변수 a를 참조하게 되어 전역 변수 a 값이 출력된다.
개발자 도구를 통해 콜 스택 부분과 변수 a를 관찰하면 다음과 같다.
실시간
으로 반영변수 환경과 다르게 렉시컬 환경은 변경 사항을 실시간으로 반영하는 이유는?
변수 환경
은 주로 var 키워드로 선언된 변수를 관리하는데, var 변수는 함수 스코프만 인식하고 호이스팅 시 undefined로 초기화되며 중복 선언을 허용한다. var는 블록 스코프를 무시하고 함수 스코프만 인식하기 때문에, 변수 환경은 복잡한 실시간 업데이트가 필요하지 않다.렉시컬 환경
은 let, const를 위해 도입되었는데, let, const는 블록 스코프를 인식하고 TDZ가 적용되며 중복 선언이 불가하다. 블록 스코프마다 새로운 환경이 필요하고 TDZ를 정확히 관리해야하고 스코프 체인을 통한 변수 검색이 정확해야하기 때문에 실시간으로 변경사항을 반영해야한다.- 위와 같은 이유로 외부 환경을 참조하는 outer는
렉시컬 환경
을 통해 검색이 이루어진다.
📌 실행 컨텍스트의 구성 요소가 어떻게 상호작용하는지 알아보기
let projectName = "움몀곰돔체"; // 렉시컬 환경에서 관리
var teamSize = 4; // 변수 환경에서 관리
const team = {
lead: "채멈",
members: ["드뮴", "뭼주"],
showStatus: function() {
// this binding으로 team 객체 참조
console.log(`${this.lead}이 대장인 ${projectName}`);
// 화살표 함수에서는 외부 스코프의 this 유지
const report = () => {
console.log(`인원: ${this.members.length}`);
};
report();
}
};
team.showStatus();
변수 환경
은 var로 선언된 teamSize를 관리한다.렉시컬 환경
은 let으로 선언된 projectName과 const로 선언된 team을 관리한다.this
는 showStatus 내의 team 객체를 가리키고, 화살표 함수에서의 this는 외부 스코프의 this를 유지한다. 실행 컨텍스트 구조에는 outerEnvironmentReference
가 있었다. 외부 환경을 참조하는 포인터로, 렉시컬 환경을 참조한다. 위에서 간단하게 적었듯이 렉시컬 환경은 실시간으로 변경 사항을 반영하기 때문에 외부 환경을 참조하는 환경은 렉시컬 환경이다.
스코프란 식별자에 대한 유효 범위를 의미한다. 예를 들어 스코프 A 외부에서 정의한 변수는 A의 외부/내부에서 모두 접근이 가능하다. 그러나 A의 내부에서 선언한 변수는 오직 A의 내부에서만 접근이 가능하다.
변수를 어느 위치에 선언하느냐에 따라 변수를 사용할 수 있는 유효 범위인 스코프가 달라진다.
식별자의 유효성을 검사하기 위해 안에서부터 바깥으로 유효 범위를 탐색해나가는 것을 스코프 체인이라 한다. 이를 가능하게 해주는 것이 outerEnvironmentReference
이다.
outerEnvironmentReference
는 호출된 함수가 선언된 당시의 렉시컬 환경을 참조한다.
var a = 1; // 전역 컨텍스트
function outer () { // outer 컨텍스트
function inner () { // inner 컨텍스트
console.log(a);
var a = 3;
console.log(a);
}
inner();
// 1️⃣ outer의 렉시컬 환경을 outerEnvironmentReference로 참조
console.log(a);
}
outer();
// 2️⃣ 전역 컨텍스트의 렉시컬 환경을 outerEnvironmentReference로 참조
console.log(a);
각 컨텍스트는 자신만의 environmentRecord를 가지고 있고, outer 참조를 통해 상위 스코프의 렉시컬 환경과 연결된다. 만약 자신의 environmentRecord에서 검색하는 값을 찾지 못한다면 상위로 이동해서 검색하게 된다.
스코프 체인을 통해 변수를 검색할 때 렉시컬 환경
의 outerEnvironmentReference를 통해 참조를 한다.
그렇다면 변수 환경
의 outerEnvironmentReference는 왜 존재하는걸까?
현대 자바스크립트에서는 변수 환경의 outer는 거의 사용되지 않고 있다. 그러나 이를 완전히 제거하지 않은 이유는 기존 코드와의 호환성을 유지하고 var의 함수 스코프 특성을 보존하고 자바스크립트 엔진의 일관성 있는 구조를 유지하기 위해 여전히 존재한다.
호이스팅은 자바스크립트에서 변수나 함수의 선언이 코드의 최상단으로 끌어올려지는 것처럼 동작하는 특징이다.
자바스크립트에서는 코드를 실행하기 전 식별자를 수집한다. 실제로 끌어올리는건 아니지만 끌어올리는 것으로 이해하는 가상의 개념이다. 식별자만 끌어올리고 할당 과정은 그대로 남겨둔다.
호이스팅을 통해 자바스크립트 엔진은 코드 실행 전에도 변수명들을 모두 알 수 있게 된다.
console.log(name); // undefined
var name = "채멈";
위 코드는 다음과 같이 동작한다.
undefined
를 출력한다.console.log(name); // ReferenceError
let name = "채멈";
위 코드는 다음과 같이 동작한다.
TDZ(Temporal Dead Zone)
가 시작된다.TDZ란?
일시적 사각지대(Temporal Dead Zone, TDZ)
는 변수가 선언된 위치부터 초기화되기 전까지의 구간을 말한다. 이 구간에서는 변수에 접근할 수 없다.- 따라서 TDZ 구간에서 변수를 출력하려하면 ReferenceError가 발생하고 변수를 할당하는 위치에 오게 되면 TDZ가 종료된다.
var
는 선언과 초기화가 동시에 호이스팅되므로, 위로 끌어올려질 때 undefined로 초기화가 일어난다.let
, const
는 호이스팅이 되지만 선언만 호이스팅되고 초기화는 실제 코드 위치에서 이루어지므로 초기화가 이루어지기 전 변수에 접근하면 에러가 발생한다.let, const는 왜 등장했을까?
var
로 선언된 변수는 호이스팅되어 변수를 선언하기 전에 접근할 수 있다. 이렇게 되면 코드의 가독성을 떨어뜨리고 예측할 수 없는 동작을 발생시킨다.let
,const
는 블록 스코프로 선언된 블록 내에서만 유효하므로 변수 충돌을 줄여주고, 초기화되기 전의 구간에서 접근하면 ReferenceError를 발생시키기 때문에 실수로 초기화되지 않은 변수를 사용하는 것을 방지한다.
위 코드를 실행하면 결과는 다음과 같다.
function a(x) {
console.log(x); // 1
var x; // 이미 x가 있어서 무시됨
console.log(x); // 1
var x = 2; // x에 2를 할당
console.log(x); // 2
}
a(1);
호이스팅 단계
function a () {
var x;
var x;
var x;
x = 1;
console.log(x); // 1
console.log(x); // 1
x = 2;
console.log(x); // 2
}
a();
실행 단계
function a(x) { // x = 1
console.log(x); // 1 출력 (매개변수 값)
var x; // 이미 선언된 x가 있으므로 무시
console.log(x); // 1 출력 (매개변수 값)
var x = 2; // x에 새로운 값 2를 할당
console.log(x); // 2 출력 (새로 할당된 값)
}
자바스크립트 엔진이 보는 코드
function a(x) {
// 호이스팅된 상태
// 매개변수 x가 이미 존재하므로 추가 var 선언은 무시
console.log(x); // 매개변수 x의 값 1 출력
// var x; 무시
console.log(x); // 여전히 1
x = 2; // 값만 재할당
console.log(x); // 2
}
a(1);
sayHello(); // "안녕" 출력
function sayHello() {
console.log("안녕");
}
sayHello();
var sayHello = function() {
console.log("안녕");
};
sayHello();
const sayHello = () => {
console.log("안녕");
};
function a() {
console.log(b);
var b = 'bbb';
console.log(b);
var b = function () {};
console.log(b);
}
a();
호이스팅 단계
변수 선언과 함수 선언이 최상단으로 끌어올려진다.
function a() {
// 호이스팅
var b;
function b() {};
console.log(b);
b = 'bbb';
console.log(b);
var b = function () {};
console.log(b);
}
실행 단계
function a() {
...
console.log(b); // undefined
b = 'bbb';
console.log(b); // 'bbb'
var b = function () {};
console.log(b); // [Function: b]
}
const myClass = new MyClass();
class MyClass {
constructor() {
this.name = "MyClass";
}
}
자바스크립트 엔진은 함수와 클래스를 다르게 처리한다.
함수 선언은 완전히 호이스팅 되므로 선언 전에 사용해줘도 되지만, 클래스의 경우는 호이스팅되지만 초기화되지 않는 상태로 남아있다. 따라서 클래스는 초기화되기 전까지 TDZ에 있기 때문에 이 구간에서 사용하게 되면 ReferenceError가 발생한다.
함수를 선언 전에 사용해도 되는 이유는 초기 자바스크립트의 설계 철학과 관련되어 있다.
function init() {
setup();
loadData();
registerEvents();
}
function setup() { }
function loadData() { }
function registerEvents() { }
코드 실행에 필요한 모든 정보를 담고 있다.
식별자 검색은 현재 스코프에서 시작해 상위 스코프로 연결되는 스코프 체인을 따라 이루어진다. 렉시컬 환경의 outer 참조를 통해 구현된다.
호이스팅은 선언을 코드 최상단으로 끌어올리는 것처럼 동작한다.
호이스팅을 공부할 때 변수와 함수의 호이스팅의 차이는 무엇인가?에 대해서만 공부했었다. 이렇게 공부하니 실제 코드 예제가 주어졌을 때 호이스팅이 발생하고 어떻게 출력되는지는 이해하기가 어려웠다. 이번에 호이스팅을 공부하며 자바스크립트의 동작 원리와 스코프 체인까지 함께 공부해서 제대로 이해할 수 있었다.