티스토리 뷰

기억도 나지않는 어느 날엔가, 누군가 내게 제목과 같은 질문을 했다.

"Javascript의 Class도 호이스팅이 되나요?"

 

그리고 내가 답했다.

"네, Class도 당연히 호이스팅 됩니다."

 

그러자 질문자가 되물었다.

"하지만 아래와 같은 코드는 에러를 뱉는걸요?"

var foo = new Foo(1, 2); //ReferenceError

class Foo {
   constructor(x, y) {
      this.x = x;
      this.y = y;
   }
}

 

"네, 에러를 뱉겠죠. Foo class의 선언보다 사용이 앞섰으니까요."

실제 new Foo가 뱉는 에러


0. 누구에게나 처음은 있다.

위의 일화가 본 포스팅을 작성하게 된 이유다.

아마도 Javascript를 시작하는 길목에서 처음 호이스팅이라는 개념을 이해할 때면,

우리는 아래와 같은 코드를 보게 될 것이고

console.log(str); // undefined

var str = "hello";

결론적으로 str이라는 변수를 정의하지 않은 시점에 콘솔로 값을 출력했을 때, 에러가 아닌 undefined라는 값이 출력되는 것은 'str'이라는 변수가 '호이스팅'되서 그렇다,라고 배웠을 것이다. 그리고 혹자는 호이스팅의 개념이 Javascript 내부에 사용되는 변수나 함수를 소스코드 위로 끌어올려주는 것, 이라고 정의를 내리곤한다.

 

내가 본 몇몇 책에도 이렇게만 설명되어있었다. 사실 이 말은 반은 맞고 반은 틀린 말이다.

 

물론, 입문자의 이해도를 높이려는 관점에서보자면 위와 같은 설명보다 적절한 것은 없는 것 같다.

하지만 하루이틀 Javascript를 사용할 것도 아니고, 업으로 계속 써야하는 입장에서보자면 한 발자국 더 나아가서 호이스팅에 대한 명확한 개념적 이해를 가져야한다.


1. 내로남불? ㄴㄴ 쌤쌤.

전설의 베컴형의 '난 둘 다'

Class로 정의한 Foo는 선언보다 앞서서 사용되었을 때 에러였고

var로 정의한 str은 선언보다 앞서서 사용했음에도 에러가 발생하지 않았다.

 

하지만 이미 결론을 밝혔듯이 Class도 var도 호이스팅 된다.

근데 왜 누구는 에러고 누구는 에러가 아닐까?

 

이를 이해하기 위해선 호이스팅에 대한 정확한 개념을 알아야한다.


2. Lexical Environment

기술 관련 포스팅을 쓰려면 우선, 나부터가 그 개념에 대한 명확한 이해를 가져야한다.

그래서 늘 책을 한 번 더 살펴보거나 다른 분들이 정의한 내용들을 리서치하고 참고 할 만한 자료가 있으면 기억해두곤하는데...보통 이 과정에서 '아, 이 개념에 대해선 이 사람보다 잘 정리 할 순 없을 거 같다'라는 생각이 들게 만드는 분들이 있다.

 

이번에 호이스팅과 Lexical Environment에 대해 리서치하면서 발견한,

아래 블로그가 딱 그에 해당했다.

 

https://dkje.github.io/2020/08/30/ExecutionContext/

 

위 링크의 핵심주제는 사실, Execution Context(실행 컨텍스트)와 Call Stack(콜스택)에 대한 것이다.

하지만 그 과정에서 실행 컨텍스트가 동작하면서 발생되는 필수적인 것들...

그러니까 호이스팅과 Lexical Environment에 대한 설명이 그림을 바탕으로 정말 잘 설명이 되어있다.

 

다만, 위 링크를 가만히 보다보면...

let도 const도 호이스팅됩니다.

이와 같은 문구를 볼 수 있는데, 이는 틀린부분이다.

(댓글로도 어떤 분이 저 부분에 대한 정정 요청을 하셨는데, 글 쓰신 분이 아직 확인을 못 하셨나보다)

2-1. Lexical Environment

위에서 공유해 준 링크는 어쩌면, 누군가에겐 너무 복잡하게 느껴질 수도 있을 것 같아서 첨언하듯 직접 정리를 해보자면 Javascript가 실행되면 Execution Context라는 것이 구성된다.

An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation. 

출처 : ECMAScript(262) 9.4 Execution Contexts

 

이는 현재 실행하려는 Javascript 코드가 동작 할 수 있는 환경이 구성되는 것으로, 우리가 흔히 Javascript 코드에서 사용하는 변수나 함수, this, arguments, scope등이 정상적으로 동작할 수 있도록 해주는 환경 같은 것이다. 

 

그리고 이때, 코드에서 활용되는 변수나 함수의 선언부들이 스캔되어 Lexical Environment라는 자료구조에 저장되게 된다. 이는 변수나 함수를 선언 이전에 사용할 수 있게 해주기 위함이며, 이러한 연유로 인해 호이스팅을 '변수와 함수들이 코드 위쪽으로 끌어올려 지는 것'이라고도 말하는 것이다.

(여기서 한 번 더....변수나 함수를 선언 이전에 사용할 수 있게 해주기 위함인데 왜 class Foo는 에러...? 라는 의문에 대해서는 아래의 3. Temporal Dead Zone 에서 설명하겠다)

 

Lexical Environment는 식별자(identifier)와 값(variable)이 매핑된 자료구조 형태로써,

개념적으론 아래와 같은 형태를 띄고있다.

LexicalEnvironment = {
  Identifier:  <value>,
  Identifier:  <function object>
}

그리고 실제로 아래와 같은 코드를 우리가 작성하였다면

helloWorld();  // prints 'Hello World!' to the console

function helloWorld(){
  console.log('Hello World!');
}

아래와 같은 형태로 LexicalEnvironment가 정의되며

lexicalEnvironment = {
  helloWorld: < func >
}

Javascript내에서 helloWorld()라는 코드를 통해 해당 function의 호출되면,

LexicalEnvironment를 확인하여 해당 function을 정상적으로 호출하는 것이다.


3. Temporal Dead Zone

자, 이제 그럼 다시 처음으로 돌아가서...아래와 같은 코드에서 왜 누구는 undefined고 누구는 ReferenceError인지를 알아보도록 하자.

var foo = new Foo(1, 2); //ReferenceError

class Foo {
   constructor(x, y) {
      this.x = x;
      this.y = y;
   }
}

console.log(str); // undefined

var str = "hello";

그러기 위해선 Temporal Dead Zone 이라는 개념을 알아야하는데, 이는 우리가 사용하려는 변수 혹은 함수의 선언과 초기화 사이의 죽은 시간이라고 이해를 하면된다. 호이스팅은 Javascript 코드에서 선언된 변수 및 함수가, Lexical Environment라는 자료구조에 할당되는 것을 의미한다고 말했다. 여기서 포인트는 바로 선언이라는 키워드다.

 

코드에서 선언이 되었다는 건, 해당 변수/함수를 사용하겠다는 것이기 때문에 Lexical Environment에 적재된다. 근데 그렇게 사용 될 예정인 변수/함수의 초기화는 언제하는가? 

 

위 코드를 보자.

Foo라는 class의 초기화는, 분명 3번째 줄에 했다.

str이라는 변수의 초기화는, "hello"라는 값을 할당하는 시점이다.

 

즉, 실제로 사용하고자하는 변수/함수가 선언되었기때문에 Lexical Environment에 등재되지만, 그 것들이 참조해야하는 실질적인 '값'의 초기화는 이뤄지지 않은 것이다(값이라기보단 참조해야 할 메모리의 위치라는 게 좀 더 정확지않을까 싶다)

 

변수/함수의 선언과 초기화의 사잇시간.

그 사잇시간이 바로 Temporal Dead Zone이라고 불리며, 만약 이 사잇시간에서 우리가 초기화되지 않은 변수/함수를 사용하려고하면, 당연하게도 해당 변수/함수가 참조해야 할 값(메모리)을 모르기 때문에 Reference Error가 발생하는 것이다.

 

어, 근데 var로 선언한 str은 왜 Reference Error가 아니고 undefined인가요? 라는 의문이 드는게 당연해야한다.

Lexical Environment에 대한 설명을 다시 떠올려보자. Lexical Environment는 식별자(identifier)와 값(variable)이 매핑된 자료구조라고 설명을 했었다. 여기서 식별자는 당연하게도 코드에서 사용하고자 선언된 것들(var, let, const, class, 함수선언식)이다.

 

그렇다면 Lexical Environment에 등재될 때의 값(variable)의 영역은?

 

여기서 var의 특이점을 알 수 있는데 var로 선언된 대상은 undefined로 초기화가 이루어지게 된다.

반면에 let이나 const, class와 같은 여타 호이스팅 대상들은 undefined가 아니라 uninitialized로 초기화가 된다. 즉, 아래와 같은 형태로 말이다.

lexicalEnvironment = {
  Foo: <uninitialized>,
  str: undefined
}

그리고 undefined는 Javascript의 Primitive Type중에 하나이고, 실질적인 '값'으로써 인식하기 때문에 에러가 발생하지 않는다. 반면에, <uninitialized>로 선언된 변수/함수는 초기화되지 않았다는 에러를 발생하며 이와같이 <uninitialized>로 Lexical Environment의 값(variable)이 정의된 때를 Temporal Dead Zone이라고 한다.

(여기서 Zone이라고 부르는 이유는, 개인적인 추측이지만 결국 변수/함수가 가리키는 메모리의 영역(Zone)이 할당되지 않았기 때문이 아닐까...즉, 죽은 영역을 가리킨다는 의미가 아닐까 생각해본다)


4. 함수 표현식(Function Expression)은 호이스팅되지 않아요.

helloWorld(); // TypeError: helloWorld is not a function

var helloWorld = function() {
  console.log('Hello World');
}

위와같은 꼴을, 우리는 함수 표현식(Function Expression)이라고 부르며 함수 표현식은 호이스팅 되지 않는다. 좀 더 정확하게 말하자면, var로 선언된 helloWorld라는 변수 자체는 undefined로 호이스팅이 되지만 거기에 할당된 함수 자체는 호이스팅 대상이 아니기 때문에 helloWorld라는 함수를 정의하기도전에 helloWorld();의 형태로 함수를 호출하듯이 사용하면 에러가 발생하는 것이다.

 

코드상으로 helloWorld()를 호출하는 시점에 helloWorld의 값은 undefined기 때문이다.

마찬가지로 아래와 같은 클래스 표현식(Class Expression)도 호이스팅 되지않는다.

let peter = new Person('Peter', 25); // ReferenceError: Person is  
                                     // not defined
console.log(peter);
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

만약 위와같은 코드가 정상적으로 동작하게 하려면 아래와 같이 정의해주어야한다.

let Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
let peter = new Person('Peter', 25); 
console.log(peter); // Person { name: 'Peter', age: 25 }

 

5. 결론

선언을 목적으로 사용된 것은 호이스팅된다.

let도 const도 var도 class도 함수 선언식도, 다 호이스팅된다.

 

하지만 표현식은 안 된다.

함수 표현식, 클래스 표현식은 호이스팅 되지 않는다.


참고사이트

1. [JS]Execution Context와 Call Stack

2. Hoisting in Modern JavaScript — let, const, and var

댓글
최근에 올라온 글
Total
Today
Yesterday