호이스팅(Hositing)은 코드를 실행하기 전에 변수/함수 선언을 해당 스코프의 최상단으로 끌어올리는 것 같은 자바스크립트의 성질을 뜻하는 단어이다. 정확히 말하자면 실제로 변수/함수 선언을 최상단으로 끌어올리는 것은 아니고, 실제 코드를 수행하기 전 자바스크립트는 전체적인 코드 수행을 위한 memory 공간을 미리 설정하는데, 이때 선언된 변수 값들을 위한 memory 공간이 미리 할당되면서 선언문보다 참조/호출이 먼저 나와도 Error
없이 코드가 수행되게 되는 것이다. (Java, C++, 등 타 언어들의 경우 선언문보다 참조/호출이 먼저 나오는 경우 바로 Error
)
이러한 호이스팅 문제를 해결하기 위해 ES6 버전에서 let
이라는 새로운 변수 선언문이 추가되었다. (덕분에 모든 자바스크립트 입문자에게 let과 var의 차이점에 대해 헷갈리게 해주는 현상도 같이 발생) var
대신 let
을 통해 변수를 선언해도 호이스팅 현상은 똑같이 발생하게 된다. 다만, 호이스팅 현상이 발생하지 않는 것처럼 보이게끔 동작한다고 이해하는 것이 적절하다.
ES6 버전 전까지 사용되던 var
는 여러 문제점을 가지고 있으며, 호이스팅 현상을 방지하기 위해 var
를 사용하지 않는 것이 권장된다. 아래 예시들을 살펴보자.
//code 1
console.log(a); //output: undefined
var a = 1;
//code 2
var a;
console.log(a); //output: undefined
a = 1;
code 1에서 console.log(a)
가 선언문보다 먼저 나오지만 에러없이 undefined
가 출력된다. 앞서 언급했듯이 타 개발언어에서 선언문보다 참조/호출이 먼저 나오면 바로 에러처리될 확률이 높다.
var a
라는 선언문이 Hoisting 되었기 때문이다. 이때 1이 할당되는 부분은 호이스팅되지 않기 때문에 자바스크립트는 a = undefined
로 이해하고 console.log을 수행하게 된다.
Hoisting을 감안하면 code 1은 code 2처럼 동작한다고 보면 된다.
for(var i = 1; i < 5; i++) {
console.log(i);
}
console.log(i);
/* output:
1
2
3
4
5
*/
i
값은 for문 안에서 선언된 지역변수임에도 불구하고 for문 밖에서 console.log(i)
를 통해 5
까지 출력되는 것을 확인할 수 있다.
var
는 function-scope이므로 함수 외의 블록에서 선언된 변수는 전역변수로 호이스팅 된다.
//code 1
var a = 1;
console.log(a); //output: 1
var a = 2;
console.log(a); //output: 2
//code 2
var a = 10;
console.log(a); //output: 10;
{
var a = 20;
console.log(a); //output: 20
}
console.log(a); //output: 20
변수는 주민등록번호와 같으므로 변수가 겹치는 것은 불가능해야 정상이지만var
의 경우 변수 재선언이 동일 블록에서 가능하다. code 1을 보면 var a = 1
를 통해 a 변수를 선언하고 1이라는 값을 이미 할당했음에도 아래 var a = 2
를 통해 a 라는 변수를 다시 선언할 수 있다.
앞서 언급했듯이 var
는 함수 외의 블록에서 선언된 경우 전역 변수로 호이스팅된다. 때문에 code 2 를 통해 확인할 수 있는 것처럼 a 변수를 새로운 블록 안에서 재선언했음에도, 블록 밖에서 다시 a를 호출하면 a는 새로운 값이 할당되어 있다.
let
은 이러한 var
의 단점을 보완한 상위호환 개념이라고 생각해도 무방하다.
let | var |
---|---|
블록 레벨 스코프 (block-scope) | 함수 레벨 스코프 (function-scope) |
변수 재선언 불가 | 변수 재선언 가능 |
Hoisting 발생하지 않는 것처럼 동작 | Hoisting 현상 발생 |
1. let
은 block-scope이므로 함수 외의 블록에서 선언되어도 지역 변수로 남는다.
for (let i = 1; i < 4; i++) {
console.log(i); //output: 1, 2, 3, 4
}
console.log(i); //output: ReferenceError: i is not defined
i
는 블록 밖에서 접근 불가능하다. (ReferenceError)2. let
은 동일 블록 내에서 변수 재선언이 안된다.
//code 1
let a = 1;
console.log(a);
let a = 2;
console.log(a);
// SyntaxError: Identifier 'a' has already been declared
//code 2
let a = 10;
console.log(a); //output: 10
{
let a = 20;
console.log(a); //output: 20
}
console.log(a); //output: 10
code 1 → let
을 통해 동일 블록에서 a
변수를 재선언하는 것은 불가능하다. (SyntaxError)
code 2 → 서로 다른 블록에서 let
을 통해 선언된 동일 변수는 서로에게 영향을 주지 않는다.
3. let
, 혹은 const
를 사용한다고 JS Engine의 동작원리가 바뀌는 것이 아니라 Hoisting 현상이 일어나지 않는 것처럼 동작되게 만들어진 것이다.
let
이나 const
를 사용해도 JS Engine은 코드 실행 전 선언된 변수들을 인식하고, 이를 코드 실행을 위한 memory
에 넣어두게 된다. 다만 이때 var
를 통해 선언된 변수는 undefined 값으로 저장되고, let
과 const
를 통해 선언된 변수들은 <uninitialized>
로 저장된다.왜 var
만 변수 선언과 함께 undefined 값이 저장되는지 알기 위해서는 변수 생성 과정에 대한 이해가 필요하다. 변수는 3단계에 걸쳐 생성된다.
1. 선언 단계 (Declaration)
2. 초기화 단계 (Initialization)
3. 할당 단계 (Assignment)
var
를 통해 변수를 선언하면 선언/초기화 단계가 동시에 진행된다. 때문에 선언문보다 호출이 먼저 나와도 이미 undefined로 초기화 되어 있는 변수를 호출하게 되는 것이다.
let
으로 선언된 변수는 두 단계가 따로 진행된다. 즉 호이스팅을 통해 JS Engine이 스코프에 맞게 변수를 미리 선언하는 것은 동일하지만, 선언문과 할당문에 도달해야만 다음 단계들이 진행된다. 선언은 되었지만 초기화 되기 전까지의 단계, 즉 스코프의 시작점부터 선언문 코드까지의 공간을 TDZ (Temporal Dead Zone)이라고 부른다.