Hoisting and TDZ

open_h·2020년 11월 21일
1

JavaScript

목록 보기
2/5
post-thumbnail

이전 포스팅에서 var 키워드의 경우 let 이나 const 와 달리 변수를 선언하기 이전에도 참조가 가능해지는 문제가 발생한다고 하였다. 이 문제를 이해하기 위해서 호이스팅이라는 개념을 알고 있어야 한다. var 의 경우 선언 이전에 참조가 가능하기에 호이스팅이 일어나지만 letconst 는 호이스팅이 일어나지 않는다고 착각 할 수도 있다. 하지만 let , const 을 포함하여 모든 선언(초기화나 할당과 구분해야 한다. 선언만이다.)은 호이스팅이 일어난다. 심지어 호이스팅이라는 용어는 ES6 이전 표준 명세에서 사용된 적이 없는 용어이다.

Hoisting호이스팅

호이스팅은 영어 단어의 뜻 그대로 끌어올려진다는 의미를 가진다. 이 용어는 자바스크립트 엔진의 동작 과정 중의 일부분을 쉽게 이해하기 위한 가상의 개념이다. 이걸 다르게 말하면 호이스팅에 대해 제대로 이해하기 위해서는 실행 컨텍스트, 자바스크립트 엔진, 컴파일 과정, 스코프, 자바스크립트의 메모리 관리 등을 모두 이해해야 한다는 것이다.

공부할 것은 넘쳐 나는데 내 머리는 슬슬 한계가 보이기 시작했다.

그래서 일단 여기서는 엔진의 실제 동작 방식이 아니라 쉽게 이해할 수 있는 가상의 개념만을 간단히 정리하고자 한다. 호이스팅이란 실제 자바스크립트 엔진 동작 일부 방식을 '자바스크립트 엔진은 식별자들을 (유효 범위의)최상단으로 끌어올리고 난 후에 다음 코드를 실행한다' 라고 생각하는 것이며, 이렇게 이해하더라도 코드 해석에는 문제가 없다.

자바스크립트 엔진이 실제로 식별자들을 끌어올리는 것은 아니지만 식별자들을(실제 메모리 상으로는 변화가 없지만, 메모리에 저장하는 방식을 고려했을 때) 편의상 끌어올린 것으로 간주하는 것이다. 자바스크립트가 실제로 실행되기 이전에 유효 스코프 안에서 변수값들을 모아서 마치 유효 스코프 최상단에 선언한 것 처럼(실제로 코드가 그렇게 바뀌는 것은 아니다) 해석할 수 있다는 것이다.

여기서 식별자(identifier)의 개념을 한번 더 짚고 넘어가자. 식별자는 코드의 변수, 함수 등을 식별하는 단어로 개발자가 만들어주는 것으로 myArrayVar , myAddFunction 과 같은 변수명, 함수명, 속성명, 메소드명이 그것이다.

예제 코드 1 : ''끌어올린다''는 의미

/* 원본 코드 */
console.log(a); // undefined
var a = 10;
/* 
실제로 코드가 이렇게 변하는 것은 아니지만 컴파일 과정에서
이렇게 호이스팅된 것으로 간주하여 코드를 해석할 수 있다.
*/
var a;
console.log(a);
a = 10;

var 로 선언된 변수 a 는 위 코드처럼 식별자들이 호이스팅되어 함수 선언 이전에 함수에 접근하여도 코드가 작동한다. 단 초기화 혹은 할당과는 별개로 선언만 끌어올린다는 것을 주의하자.

이런 코드는 다른 언어를 공부하고 난 후 javascript를 접한 사람들은 이해할 수 없는 동작일 수 있다. javascript를 첫 언어로 공부하더라도 코드가 위에서부터 차례대로 실행되지 않는 듯하여 충분히 헷갈릴 수 있다. 이런 방식은 코드 작성 방법이 자유롭다는 장점이 있지만 한편으로는 var 로 선언된 변수는 변수명 중복 등 개발자 입장에서 error prone해지며 이는 var 키워드를 사용하지 말아야 할 이유 중 하나라고 볼 수 있다.

예제 코드 2 : 함수 호이스팅과 유효 범위의 최상단

/* 
	원본 코드
	에러가 있어서 실행되진 않지만 에러가 발생하는 줄만 없애주면
	출력되는 값은 주석으로 남겼다.
*/
console.log(a); // undefined
f1(); // undefined
console.log(f2) // undefined
f2(); // TypeError: f2 is not a function
function f1(){
  console.log(b);
  var b = 5;
}
var f2 = function () {
  console.log(c);
  var c = 7;
}
var a = 10;
/* 아래 코드로 간주하여 코드 해석 가능 */
var a;
function f1(){
  var b;
  console.log(b);
  b = 5;
}
var f2;

console.log(a);
f1();
console.log(f2);
f2();
f2 = function f2() {
  var c;
  console.log(c);
  c = 7;
}
a = 10;

유효 범위의 최상단이 뜻하는 바를 위 두 코드를 비교하면 서 확인 할 수 있다. 함수 scope 내에서 가장 최상단으로 var b , var c 라는 선언이 이루어 진 것을 볼 수 있다. 전역에서는 변수 a 가 최상단으로 끌어올려졌다.

함수를 선언하는 방식에서 f1 과 같이 function declarations(함수 선언식)으로 함수를 선언할 경우와 다르게 f2 와 같이 function expressions(함수 표현식)으로 함수를 선언한 경우 f2 라는 식별자가 호이스팅될 뿐, 함수 자체는 끌어올려지지 않는다. 실제로 f2 를 출력했을 때는 undefined 를 확인할 수 있으며, 함수로서 f2() 와 같이 호출했을 때는 TypeError: f2 is not a function 라는 에러가 발생한다.

letconst 도 마찬가지로 var 처럼 변수 선언을 위한 키워드이기에 호이스팅이 일어날 것이다. 그런데 constlet 키워드로 선언된 변수는 위의 예시 코드의 var 로 선언된 변수처럼 선언 전에 참조를 할 수 없다. 최상단으로 변수의 선언을 끌어올렸는데 그 아래에서 참조를 할 수 없다? 그렇다면 letconst 는 호이스팅이 일어나지 않는 것일까? 결론부터 말하자면 그렇지 않다.

Temporal Dead Zone(TDZ)

전의 질문에 짧고 쉽게 답을 하자면 letconst 으로 선언한 변수는 var 처럼 호이스팅이 일어나지만, var 과 다르게 초기화가 되기전까지 TDZ라는 곳에 머물러 초기화(혹은 할당)이 될 때까지 잠시 '죽어있는 상태'이기 때문에 선언 전에 참조가 불가능한 것이다. TDZ를 직역해도 '임시로 죽어 있는 공간'이다. TDZ는 선언 전에 변수를 사용하는 것을 비허용하는 개념상의 공간이다.

var 과 달리 선언 전에 변수를 사용하지 못하는 것인데, 호이스팅이 일어난 것을 직접 확인할 수 있을까? 다시 말해 TDZ의 변수와 선언되지 않은 변수를 구별할 수 있을까? 먼저 typeof 연산자로 쉽게 확인 할 수 있는 방법이 있다.

typeof notDefined; // 'undefined'
typeof variable; // ReferenceError : Cannot access 'a' before initialization
let variable;

즉, 아예 선언이 되지 않은 변수는 'undefined' type이지만, TDZ에 있는 식별자에는 접근하는 것은 그 자체로 에러를 발생시키고 있다.

다른 방법으로 아래 예시 코드를 통해 let 이 호이스팅이 된다는 것을 확인해보자.

const a = 3; // 전역 변수
{
  console.log(a); // 당연하게도 전역 변수 3이 출력된다.
}
const a = 3; // 전역 변수
{
  console.log(a); // ReferenceError: Cannot access 'a' before initialization
  const a = 5;
}

JavaScript에서 { } 를 자유롭게 사용하여 level of scope를 설정할 수 있다. 극단적으로 {{{ let a = 10; }}} 도 문법상 문제가 없으며 의미없이 {} 비어 있는 스코프를 생성해도 문법상 문제는 없다. 함수가 하나의 스코프가 되는 것처럼 위 코드에서는 보기 쉽게 중괄호를 사용해 스코프를 분리해주었다.

만약 let 이 호이스팅이 되지 않은 것이라면 두 번째 예시와 같은 에러는 발생하지 않았을 것이다. 중괄호 스코프 내에서 호이스팅이 일어났고 a 가 스코프 내에서 할당이 일어나기 전에 console.log 에서 참조를 하려 했고, 아직 TDZ에 있는 변수 a 를 참조할 수 없기에 ReferenceError 가 발생하는 것이다.

참고로 const , let 말고도 class 구문과 클래스의 constructor() 내부의 super(), 기본 함수 매개변수도 TDZ 제한이 있다. 반면 var , function 구문은 TDZ에 영향을 받지 않으며 현재 스코프에서 호이스팅이 된다. 이것은 이미 위의 예시 코드에서 undefined 를 얻은 var 로 선언된 변수와 호이스팅된 함수 예시 코드로부터 알 수 있었다.

정리

호이스팅이란 자바스크립트 엔진 동작을 이해하기 쉽게 하기 위해 만들어진 개념으로, 변수 및 함수 선언이 유효 스코프 최상단으로 끌어올려진 후에 코드를 실행하는 것으로 생각하는 것이다.

var , let , const 키워드로 선언된 변수는 선언 부분만 끌어올려진다고 생각할 수 있다. 따라서 var 의 경우에는 변수 선언전에도 참조할 수 있지만 할당을 하지 않았기에 undefined 이다. 그러나 letconst 의 경우 식별자가 호이스팅 후 실제 코드에서 선언되기 전까지 TDZ에 있기 때문에 let , const 선언 코드가 있는 곳 이전에는 해당 변수에 참조할 수 없게 된다.

참고 자료:
https://ponyfoo.com/articles/es6-let-const-and-temporal-dead-zone-in-depth
https://yuddomack.tistory.com/category/Javascript
https://dmitripavlutin.com/javascript-variables-and-temporal-dead-zone/


혹시 잘못된 내용이 있거나 부족한 부분이 있다면 말씀 부탁드립니다. 지적은 진심으로 감사히 받겠습니다!

profile
The only thing that interferes with my learning is my education.

2개의 댓글

comment-user-thumbnail
2020년 11월 22일

많이 배우고 갑니다 : )

1개의 답글