오늘은 JavaScript의 호이스팅에 대해서 조금 더 자세히 다뤄보고자 합니다.
"호이스팅은 변수의 선언을 위로 끌어올려주는것으로
let
과const
에서는 호이스팅이 동작하지 않는다."
호이스팅에 대해 처음 배울 때 이런 글을 많이 봤었고 얼마 전까지만 해도 그렇게 알고 있었습니다.
"Hoist"의 뜻 자체도 올리다라는 뜻을 가지고 있기 때문에 더더욱 그렇게 느껴졌었습니다.
하지만 얼마 전 호이스팅에 대해 자세하게 다룬 글을 보고 제가 잘 못 알고 있었다는것을 알게 되었고
그래서 오늘은 호이스팅의 진짜 개념과 원리에 대해 다뤄보고 TDZ가 발생하는 원인도 함께 다뤄보겠습니다.
먼저 호이스팅이란 뭘까요?
console.log(a); // Undefined
var a = 3;
코드는 위에서부터 아래로 읽어 내려갑니다.
위 코드의 1번 라인에서는 아직 정의되지 않은 변수 a를 호출하고 있습니다.
정의되기전에 호출이 되었으니까 에러가 발생하지 않을까? 라고 생각이 되지만
JS는 Undefined
를 출력해버립니다.
호이스팅이란 위의 예시 코드처럼 변수나 함수의 정의보다 호출이 먼저 오더라도 에러를 발생시키지 않고 마치 정의가 이미 된 코드와 똑같이 동작하는 결과를 보여주는것을 말합니다.
그래서 위의 코드는 정의를 미리 한 아래의 코드와 똑같은 결과값을 출력합니다.
var a;
console.log(a); // Undefined
a = 3;
그래서 호이스팅은 정의를 위로 끌어 올려주는것이라고 오해하기 쉽습니다.
이번에는 다른 변수 선언 키워드인let
과 const
키워드를 이용해 변수를 선언해 똑같이 실행시켜보면 var
과는 다르게 바로 에러를 출력합니다.
console.log(variable); // ReferenceError: Cannot access 'variable' before initialization
console.log(constance); // ReferenceError: Cannot access 'constance' before initialization
let variable = 3;
const constance = 3;
이러한 결과 때문에 let
과 const
는 var
과는 다르게 호이스팅이 발생하지 않는다고 오해하곤 합니다.
그럼 여기서 2가지를 짚고 넘어가보려고 합니다.
- 호이스팅은 정의를 위로 끌어올리는걸까?
만약 끌어올린다면 원리는 뭘까?
let
과const
에서는 정말 호이스팅이 발생하지 않는가?
위 2개에 대한 정답은
1. 호이스팅은 정확히 말하면 정의를 위로 끌어올려 주는 것이 아니다.
2. let
과 const
에서도 호이스팅은 동작한다입니다.
그럼 호이스팅이 동작하는 원리에 대해서 알아보기 전에
먼저 let
과 const
도 호이스팅이 동작한다는 증거에 대해서 먼저 살펴보고
이후 호이스팅의 원리에 대해서 알아보겠습니다.
// 1
console.log(a); // ReferenceError: a is not defined
// 2
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 3;
위의 두 코드의 에러가 다른게 보이시나요?
1번은 변수의 선언조차 하지 않았을 때,
2번은 변수의 호출을 선언보다 먼저 했을 때의 코드입니다.
1번에서는 a가 정의되지 않았다는 에러가 나오고 있는데 2번의 에러는 뭔가 다릅니다.
2번의 에러에서는 "초기화 하기전에는 b에 접근할 수 없다"고 말하고 있습니다.
만약 let
이 호이스팅이 되지 않는다면 1번의 에러처럼 정의되지 않은 변수라고 나와야 하지 않을까요?
변수는 아래 그림과 같이 3개의 단계에 걸쳐서 생성되며 각 단계에서 일어나는 일은 다음과 같습니다.
선언 : 변수가 scope에 등록된다.
초기화 : 변수가 메모리에 적재되며Undefined
값을 가지게 된다.
할당 : 실제 값을 변수에 할당한다.
위 과정을 토대로 에러를 해석해보면
변수 a의 경우 선언자체가 되지 않은 상태이다.
변수 b는 아직 초기화가 되지 않은 상태라고 말하고 있습니다.
TDZ란 일시적으로 특정 변수에 접근할 수 없는 구간을 이야기 합니다.
TDZ는 let
, const
, class
와 같은 ES6문법에서만 발생하는데 아래 코드를 통해 알아보겠습니다.
// TDZ
console.log(temp); // ReferenceError: Cannot access 'temp' before initialization
// TDZ
let temp = 0;
{
// TDZ
console.log(temp); // ReferenceError: Cannot access 'temp' before initialization
// TDZ
let temp = 5;
}
위 코드에서 전역범위와 지역범위에 각각 temp
라는 변수를 선언했습니다.
let
이 호이스팅이 되지 않는다면 위 코드에서 전역범위에 있는 첫번째 console.log(temp)
는
아직 변수가 선언되지 않았으니 그렇다고 쳐도
블록 내부의 위치한 console.log(temp)
의 결과값은 전역변수 temp
의 값인 "0"이 출력되어야 할 것으로 예상되는데 초기화가 되지 않아서 접근하지 못한다는 에러가 발생하고 있습니다.
let
과 const
가 호이스팅이 되지 않는다면 블록 내부의 콘솔로그에서는 지역변수로 선언된 temp
가 아직 정의되지 않은 상태이므로 전역변수 temp
의 값인 0이 호출되어야 할텐데
또 같은 에러가 출력되고 있습니다.
이는 두번째 console.log
가 전역변수 temp
가 아닌 "5"라는 값을 가진 지역변수 temp
에 접근했기 때문에 초기화가 되지 않았다는 에러가 발생하는 것이며
이게 let
과 const
도 호이스팅이 발생한다는 2번째 증거입니다.
자 그럼 호이스팅이 발생하는 이유는 뭘까요?
JavaScript 엔진은 코드를 실행하기전 실행 컨텍스트(Execution Context)라는것이 생성됩니다.
실행 컨텍스트에 대해서 자세히 다루기에는 양이 너무 방대해져 여기에서는 호이스팅이 발생하는 원리와 관련된 부분에 대해서만 간단히 다루겠습니다.
이 실행 컨텍스트는 JS로 작성된 코드가 실행되기 전 코드가 원활하게 실행될 수 있도록 실행환경을 만들어주는 역할을 합니다.
실행 컨텍스트는 생성 단계(Creation Phase)와 실행 단계(Execution Phase)라는 2개의 단계를 거치는데
생성 단계는 코드를 읽기 전, 실행 단계는 실제 코드를 읽는 단계를 말합니다.
그 중 생성 단계에서는 JS 코드를 읽기 전 자신의 스코프내에서 선언된 변수와 함수들을
스코프내에 미리 등록시키는 작업이 진행됩니다.
그래서 JS엔진은 코드를 읽기 전 이미 자신의 스코프내에 위치한 변수와 함수들의 존재에 대해 알고 있습니다.
그래서 선언보다 호출이 먼저 있더라도 정의되지 않았다는 에러가 아니라
var
의 경우 Undefined
, let
과const
는 초기화하기전에 사용할 수 없다는 에러가 출력되는것이며
var
은 이 실행컨텍스트에 등록될 때 초기화과정까지 진행되기 때문에 Undefined
라는 값을 갖게 되는것입니다.
그래서 만약 코드가 위와 같다면 1번 라인을 읽기 직전의 실행 컨텍스트 내부는 아래와 같은 상태라고 할 수 있습니다.
그래서 증거2의 코드에서 let
과 const
로 선언되는 변수들이 TDZ가 발생하는 이유도 함수 구문내에서 호이스팅이 발생하여 전역범위가 아닌 자신의 지역범위에서 선언 예정인 temp
변수에 접근하여 에러가 발생하는것입니다.
요약해보자면 호이스팅이라는 것은 동작하는것만 보면 선언을 위로 끌어올려주는것처럼 보이지만
사실은 JS의 코드가 실행되기 전에 변수들과 함수가 이미 선언되어있는 상태이며
let
과const
도 호이스팅이 똑같이 발생하지만var
,function
과는 다르게
초기화 단계 이전인 선언 단계까지만 진행된 상태이므로
선언문을 만나기 전까지는 변수를 사용할 수 없다고 정리할 수 있습니다.
추가로 함수표현식 (let a = function() {}
)은 함수가 변수의 값으로써 선언되는것이기 때문에
변수로 판단되어 초기화과정이 진행되지 않습니다.