ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JavaScript] 호이스팅 (Hoisting)
    👩🏻‍💻 정리/JavaScript 2021. 8. 22. 17:06

    목차

    1. 호이스팅이란?
    2. 호이스팅의 범위
    3. 변수 호이스팅 (var, let, const)
    4. 함수 호이스팅 (함수 선언문, 함수 표현식)
        4.1 함수 선언문과 함수 표현식
        4.2 함수 선언문의 호이스팅
    5. 참고 링크


    1. 호이스팅이란?

    자바스크립트 및 액션스크립트 코드를 인터프리터가 로드할 때, 변수의 정의가 그 범위에 따라 선언과 할당으로 분리되어 변수의 선언을 항상 컨텍스트 내의 최상위로 끌어올리는 것을 의미한다. 이는 오로지 변수에만 해당되는 것은 아니고 함수도 가능하며, 자바스크립트에서 함수의 호출을 첫 줄에서 하고 마지막 줄에 함수를 정의해도 문제없이 작동되도록 하는 유용한 특성이다. 한마디로, 호이스팅이란 변수가 끌어올려지는 현상을 말한다.

    var 변수 선언과 함수 선언문에서만 호이스팅이 발생한다. var 변수와 함수의 선언만 호이스팅되며, 할당은 끌어 올려지지 않는다. 또한, let, const 변수 선언과 함수 표현식에서는 호이스팅이 발생하지 않는다.

    2. 호이스팅의 범위

    - 전역 범위 (global scope)
      : 전역 범위에서는 스크립트의 단위에서 최상단으로 끌어올려진다.

    - 함수 범위 (function scope)
      : 함수 범위에서는 해당 함수의 최상단으로 끌어올려진다.

    3. 변수 호이스팅 (var, let, const)

    function sayHi() {
      phrase = 'Hello';
    
      console.log(phrase); // Hello
    
      var phrase;
    }
    sayHi();
    function sayHi() {
      var phrase;
    
      phrase = 'Hello';
    
      console.log(phrase); // Hello
    }
    sayHi();

    위의 두 코드는 동일하게 동작한다. var phrase;가 다른 행에 위치해 있어도, 호이스팅으로 인해 둘은 동일하게 동작한다.
    이처럼 선언은 호이스팅된다. 하지만 할당은 호이스팅 되지 않는다! 아래 코드를 살펴보자.

    function sayHi() {
      console.log(phrase);
    
      var phrase = 'Hello';
    }
    
    sayHi();

    var phrase = "Hello"; 행에서는 두 가지 일이 일어난다.

    1. 변수 선언(var)
    2. 변수에 값을 할당(=)

    변수 선언은 함수 실행이 시작될 때 처리되지만(호이스팅), 할당은 호이스팅되지 않기 때문에 할당 관련 코드에서 처리된다.
    따라서 위의 코드는 아래 코드처럼 동작하게 된다.

    function sayHi() {
      var phrase; // 선언은 함수 시작 시 처리됩니다.
    
      console.log(phrase); // undefined
    
      phrase = 'Hello'; // 할당은 실행 흐름이 해당 코드에 도달했을 때 처리됩니다.
    }
    
    sayHi();

    이와 같이 모든 var 변수 선언은 함수 시작 시에 처리가 되므로, var로 선언한 변수는 어디서든 참조할 수 있다. 하지만 변수에 무언가를 할당하기 전까지는 값이 undefined이다.

    반면, ES6에 새로 도입된 let과 const는 호이스팅을 발생시키지 않는다.

    정확하게 말하자면, 호이스팅이 발생하지 않는 게 아니라 let과 const 역시 마찬가지로 Lexical Environment에 변수 정보를 미리 수집하고 있는데, 이 둘은 실행되기 전까지 접근할 수 없고, 그 단계(공간)을 TDZ(Temporal Dead Zone)라고 한다.

    me = "happy"; // Uncaught ReferenceError: Cannot access 'me' before initialization
    let me = "sad";

     

    💡 TDZ(Temporal Dead Zone)란?
    변수 선언 이전에 변수를 참조하는 영역을 말한다. 해당 영역에서 선언 이전에 참조한 변수는 참조 에러(ReferenceError)가 발생한다. 즉, TDZ에 영향을 받는 변수는 선언 이전에 참조하는 것을 금지하고 있다.

    이를 좀 더 자세하게 이해하기 위해서는 변수의 3단계 생성과정에 대해서 생각을 해봐야 한다.

    1. 선언 단계 : 변수를 실행 컨텍스트의 변수 객체에 등록한다.
    2. 초기화 단계 : 실행 컨텍스트에 등록된 변수 객체에 대한 메모리를 할당한다. 이 단계에서 변수는 undefined로 초기화된다.
    3. 할당 단계 : undefined로 초기화된 변수에 값을 할당한다.

    이 생성과정에서 var 키워드로 변수를 만들 경우, 선언 단계와 초기화 단계가 동시에 이뤄진다. 하지만 let, const 키워드는 선언 단계와 초기화 단계가 분리되어 진행된다.

    🔗 코드가 Execution되는 모습을 시각화해주는 사이트

    4. 함수 호이스팅 (함수 선언문, 함수 표현식)

    함수 호이스팅에 대해서 알아보기 전에 함수 선언문과 함수 표현식이 어떤 것인지 먼저 알아보아야 한다.

    4.1 함수 선언문과 함수 표현식

    함수 선언문(Function Declaration)은 아래와 같이 일반 프로그래밍 언어에서의 함수 선언과 비슷한 형식으로 생겼다.

    function 함수명() {
      구현 로직;
    }

    반면에 함수 표현식(Function Expression)은 아래와 같이 변숫값에 함수 표현을 담아 놓은 형식으로 생겼다.

    const untitled = function () {
      // 내부 로직
      return '익명 함수 표현식';
    };
    
    const titled = function title() {
      // 내부 로직
      return '기명 함수 표현식';
    };
    
    // 익명 함수 표현식과 기명 함수 표현식의 차이는 함수에 식별자가 있느냐 없느냐이다.
    💡 표현식이란?
    자바스크립트 인터프리터가 계산하여 값을 구할 수 있는 자바스크립트 구절을 말한다. 이러한 값을 표현하는 것을 리터럴이라고 한다.

    4.1.1 기명 함수 표현식

    함수 표현식은 익명 함수 표현식과 기명 함수 표현식이 존재한다. 기명 함수 표현식은 말 그대로 이름이 있는 함수 표현식을 뜻하는데, 이름을 붙인 기명과 이름을 붙이지 않은 익명의 차이는 무엇일까? 어떤 경우에 "title"이라는 이름을 함수에 붙이는 걸까!?

    우선 이름을 추가한다고 해서 기존에 동작하던 기능이 동작하지 않는 일은 발생하지 않는다. 대신 title과 같이 이름을 붙여 함수를 표현하게 되면 두 가지가 변화가 생긴다.

    1. 이름을 사용해 함수 표현식 내부에서 자기 자신을 참조할 수 있습니다.
    2. 기명 함수 표현식 외부에선 그 이름을 사용할 수 없습니다.

    아래 예시 코드를 살펴보자.

    let movieTitle = function movie(title) {
      if (title) {
        console.log(`영화 제목은 ${title}!`);
      } else {
        movie("없다"); // movie()를 사용해서 자신을 호출!
      }
    };
    
    movieTitle(); // 영화 제목은 없다!
    
    // 하지만 아래와 같이 movie 함수를 호출하는 건 불가능!
    movie(); // Error, movie is not defined (기명 함수 표현식 밖에서는 그 이름에 접근 불가)

    함수 movieTitle은 title에 값이 없는 경우, 인수  "없다"를 받고 자기 자신을 호출한다. 근데 왜 굳이 movieTitle 함수가 아닌 movie 함수를 사용하여 중첩 호출을 하였을까?

    let movieTitle = function(title) {
      if (title) {
        console.log(`영화 제목은 ${title}!`);
      } else {
        movieTitle("없다");
      }
    };

    외부 코드에 의해 movieTitle 함수가 변경될 수 있는 문제가 있기 때문이다. movieTitle 함수를 새로운 변수에 할당하고 기존 변수에 null을 할당하면 에러가 발생한다.

    let movieTitle = function(title) {
      if (title) {
        console.log(`영화 제목은 ${title}!`);
      } else {
        movieTitle("없다"); // TypeError: movieTitle is not a function
      }
    };
    
    const movieInfo = movieTitle;
    movieTitle = null;
    
    movieInfo(); // 중첩 movieTitle 호출은 더 이상 불가능!

    이는 함수가 movieTitle을 자신의 외부 렉시컬 환경에서 가지고 오기 때문에 발생하는 것! 지역(local) 렉시컬 환경엔 movieTitle이 없기 때문에 외부 렉시컬 환경에서 movieTitle을 찾는데, 함수 호출 시점에 외부 렉시컬 환경의 movieTitle엔 null이 저장되어있기 때문에 에러가 발생하는 것이다.

    이때 함수 표현식에 이름을 붙여주면 바로 이런 문제를 해결할 수 있다.

    let movieTitle = function movie(title) {
      if (title) {
        console.log(`영화 제목은 ${title}!`);
      } else {
        movie("없다"); // 원하는 값이 제대로 출력된다.
      }
    };
    
    const movieInfo = movieTitle;
    movieTitle = null;
    
    movieInfo(); // 영화 제목은 없다! (중첩 호출이 제대로 동작함!)

    "movie"라는 이름은 함수 스코프 내에 존재하기 때문에 외부 렉시컬 환경에서 찾을 필요가 없다. 함수 표현식에 붙인 이름은 현재 함수만 참조하도록 명세서에 정의되어있기 때문이다.

    이렇게 기명 함수 표현식을 이용하면 movieTitle이나 movieInfo 같은 외부 변수의 변경과 관계없이 movie라는 '내부 함수 이름’을 사용해 언제든 함수 표현식 내부에서 자기 자신을 호출할 수 있다.

    💡 함수 선언문은 내부 이름을 지정할 수 없다!
    기명 함수 표현식은 함수 표현식에서만 해당되며, 함수 선언문엔 사용할 수 없다!

    4.2 함수 선언문의 호이스팅

    함수 선언문은 코드를 구현한 위치와 관계없이 자바스크립트의 특징인 호이스팅에 따라 브라우저가 자바스크립트를 해석할 때 맨 위로 끌어올려진다. 

    반면 함수 표현식은 함수 선언문과 달리 호이스팅에 영향을 받지 않는다. 

    5. 참고 링크

    🔗 var
    🔗 [나무위키] 호이스팅
    🔗 [JavaScript] 호이스팅(Hoisting)이란
    🔗 let과 const는 호이스팅 될까?
    🔗 TDZ(Temporal Dead Zone)이란?
    🔗 기명 함수 표현식
    🔗 함수 표현식 vs 함수 선언식