호이스팅이란?
코드에 선언된 변수 및 함수가 코드 상단으로 끌어올려지는 것처럼 동작하는 것을 말합니다. 이는 자바스크립트 파서가 내부적으로 끌어올려서 처리하는 것으로, 실제 메모리에서는 변화가 없습니다.
이러한 호이스팅이 발생하는 이유는 자바스크립트 해석기의 동작 방식 때문입니다. 자바스크립트 코드가 실행되면 파싱을 하게 되는데, 이때 코드를 해석해 전역 컨텍스트에 전역 변수 및 함수를 등록하려하기 때문에 끌어올려지는 듯한 현상이 발생하게 됩니다.
변수 호이스팅
console.log(name); // undefined
var name = 'danmin';
코드 상으로 보기에는 name 변수의 생성과 초기화가 console.log 이후에 이루어졌으니 에러가 발생할 것 같아 보이지만, undefined로 출력되는 것을 볼 수 있습니다.
위 코드는 아래와 같이 해석할 수 있습니다.
var name = undefined;
console.log(name); // undefined
name = 'danmin';
이러한 현상이 호이스팅으로, 해당 변수가 상단으로 끌어올려져 undefined로 초기값이 할당되는 것입니다.
따라서 변수가 선언되기 전에 변수를 사용할 수도 있습니다.
name = 'jeongmin';
var name;
스코프와 함께 예시를 보도록 합시다.
var는 함수 스코프를 가지므로 즉시 실행함수와 함께 코드를 작성해보겠습니다.
var x = 'outer';
(function() {
console.log(x); // undefined
var x = 'inner';
}());
위 코드에서 먼저 초기화된 outer가 출력될 것 같지만, undefined가 출력됩니다.
var x가 해당 함수의 스코프 최상단으로 호이스팅되기 때문입니다.
위 코드는 아래와 같이 해석할 수 있습니다.
var x = 'outer';
(function() {
var x;
console.log(x); // undefined
x = 'inner';
}());
함수 호이스팅
함수의 경우에는 표현식이냐 선언식이냐에 따라 조금 다르게 동작합니다.
var result = sum(1 , 1); // error!
var sum = function(num1, num2){
return console.log(num1 + num2);
}
위와 같은 표현식의 경우 sum에 undefined로 초기값이 할당되기 때문에 "sum is not a function"이라는 에러 문구가 뜨게 됩니다. (undefined는 함수 타입이 아니니까요!)
var result = sum(1, 1); // 2
function sum(num1, num2){
return console.log(num1+num2);
}
위와 같은 선언식의 경우 함수가 그대로 끌어올려지기 때문에 sum 로직이 잘 실행됩니다.
그렇다면 let과 const는?
console.log(name); // reference error!
const name = 'danmin';
위의 코드는 var의 경우와 달리 에러를 발생시킵니다. 이러한 결과는 let/const 선언은 호이스팅을 수행하지 않는 것으로 착각하게끔 하는데, 그렇지 않습니다.
스코프와 함께 예시를 보도록 하겠습니다.
const x = 'outer';
(function() {
console.log(x); // reference error!
const x = 'inner';
}());
만약에 호이스팅이 일어나지 않는 것이라면 최상위 스코프의 `outer`가 출력되어야 하지만, 에러가 발생하게 됩니다. 즉 function 내의 x를 읽으려하다가 참조에러가 발생하게 되는 것입니다. 즉, 참조 에러가 발생하는 것 자체가 호이스팅이 되기 때문으로 볼 수 있습니다.
따라서 초기화 전에 변수에 접근할 경우 var는 undefined를 반환하지만, let/const는 에러가 발생한다고 정리할 수 있습니다.
TDZ
그렇다면 let/const의 경우 왜 이러한 에러가 발생하는 것일까요? 바로 TDZ, 'Temporal Dead Zone'의 영향을 받기 때문입니다.
자바스크립트는 대부분의 언어와 같이 lexical scope를 따르는데, TDZ는 코드가 이 문맥적 환경에 포함될 때 변수가 생성되며, 실행되기 전까지는 접근을 막는 개념으로 볼 수 있습니다.
즉, 새로운 범위에 진입할 때마다 호이스팅에 의해 해당 범위에 속한 모든 let/const 바인딩이 코드가 실행되기 전에 일어나는데, 코드 상으로 바인딩이 일어나기 전까지 접근을 막는 것입니다.
이 때 let의 경우 lexical 바인딩 시 초기화 구문이 없으면 undefined가 할당되며, const는 반드시 선언과 동시에 할당을 해주어야 합니다.
Default Parameters
디폴트 파라미터에서도 TDZ가 적용됩니다.
(function(a, b = a) {
console.log(a); // 1
console.log(b); // 1
}(1, undefined));
디폴트 파라미터는 왼쪽에서 오른쪽으로 실행되기 때문에, b의 초기화 구문이 a를 읽을 수 있기 때문에 정상적으로 작동합니다.
(function(a = b, b) { // ReferenceError
/* ... */
}(undefined, 1));
반면에 위의 경우, a의 초기화 구문이 b를 읽을 때 b가 TDZ에 있으므로 참조에러가 발생하게 됩니다.
Class
클래스에서도 TDZ가 적용됩니다.
class Person {
constructor(age, nickname) {
this.name = 'gildong';
this.age = age;
this.nickname = nickname;
}
}
class Error extends Person {
constructor(age, nickname) {
this.age; // reference error!
}
}
class Success extends Person {
constructor(age, nickname) {
super(age, nickname);
this.name = 'jeongmin';
}
}
const danmin_1 = new Error(23, 'danmin');
const danmin_2 = new Success(23, 'danmin');
상위 생성자를 호출하기 전에 this에 엑세스하려고 하는 하위 클래스의 생성자는 참조에러를 발생시키게 됩니다. 하위 클래스의 constructor 내부에서 super()를 호출해야 this에 접근할 수 있게 됩니다.
따라서 TDZ는 초기화되지 않은 바인딩에 엑세스할 경우 에러를 발생시키기 때문에 변수 관리에 유용합니다. var의 경우 TDZ가 적용되지도 않을 뿐더러 함수레벨 스코프를 가지기 때문에 디버깅과 가독성이 좋지 않습니다. 따라서 let/const로 통일성 있게 코드를 작성하면 좋을 것 같습니다 :)