호이스팅(Hoisting)

호이스팅(Hoisting)의 정의

호이스팅은 JavaScript에서 변수와 함수의 선언을 현재 스코프의 최상단으로 끌어올리는 특성을 말합니다. 이는 코드 실행 전에 JavaScript 엔진이 변수와 함수의 선언을 메모리에 할당하기 때문에 발생합니다.

호이스팅의 작동 원리

JavaScript의 실행 컨텍스트는 크게 세 단계로 나뉩니다

  1. 생성 단계 (Creation Phase)
    • 변수는 undefined로 초기화됩니다.
    • 함수 선언은 메모리에 저장되며, 해당 스코프의 최상단으로 끌어올려집니다.
  2. 실행 단계 (Execution Phase)
    • 코드가 줄 단위로 실행됩니다.
    • 변수에 값이 할당되고 함수가 호출됩니다.
  3. 종료 단계 (Termination Phase)

호이스팅은 생성 단계에서 발생합니다.

예)

console.log(a); // undefined
var a = 5;
console.log(a); // 5

foo(); // "Hello from foo!"
function foo() {
    console.log("Hello from foo!");
}

이 코드에서는 호이스팅 때문에 첫 번째 console.log(a);에서 에러가 발생하지 않고 undefined가 출력됩니다. 또한, 함수 foo가 선언되기 전에 호출되었지만, 호이스팅 덕분에 문제없이 실행됩니다.

변수 선언과 호이스팅

var로 선언된 변수의 호이스팅

  • var로 선언된 변수는 호이스팅 될 때 초기값이 undefined로 설정됩니다.
  • 변수 선언 및 초기화 이전에 변수를 참조하면 undefined 값을 반환합니다.
console.log(varVariable); // undefined
var varVariable = 5;
console.log(varVariable); // 5

let로 선언된 변수의 호이스팅

  • let로 선언된 변수도 호이스팅되지만, var와는 다르게 초기화 단계에서 undefined로 설정되지 않습니다.
  • 변수 선언 이전에 변수를 참조하려고 시도하면 "ReferenceError"가 발생합니다. 이 시기를 Temporal Dead Zone (TDZ)라고 부릅니다.
    console.log(letVariable); 
    // ReferenceError: Cannot access 'letVariable' before initialization
    let letVariable = 10;
    console.log(letVariable); // 10

const로 선언된 변수의 호이스팅

  • const로 선언된 변수도 호이스팅 되며, let과 마찬가지로 초기화 단계에서 undefined로 설정되지 않습니다.
  • 변수 선언 및 초기화 이전에 변수를 참조하려고 시도하면 "ReferenceError"가 발생합니다. 마찬가지로 이 시기는 Temporal Dead Zone (TDZ)에 속합니다.
  • const는 선언과 동시에 초기화가 반드시 이루어져야 합니다.
console.log(constVariable); 
// ReferenceError: Cannot access 'constVariable' before initialization
const constVariable = 20;
console.log(constVariable); // 20

정리

  • var: 변수가 호이스팅 되며 undefined로 초기화됩니다.
  • let: 변수가 호이스팅되나 초기값이 없으며 TDZ에 속하게 됩니다.
  • const: 변수가 호이스팅되며 초기값이 필요하고, TDZ에 속하게 됩니다.

함수 선언과 호이스팅

함수 선언식(Function Declarations)

  • 함수 선언식은 호이스팅 될 때 해당 스코프의 최상단으로 완전히 끌어올려집니다.
  • 이는 함수의 이름과 함께 함수의 본문도 함께 끌어올려진다는 것을 의미합니다.
  • 그 결과, 함수 선언 전에 함수를 호출할 수 있습니다.
// 호이스팅으로 인해 함수 선언 전에도 호출 가능
helloFunction();
// 안녕하세요!

function helloFunction() {
    console.log("안녕하세요!");
}

함수 표현식 (Function Expressions)

  • 함수 표현식은 변수에 함수를 할당하는 방식으로 정의됩니다.
  • 함수 표현식에 사용된 변수는 호이스팅 될 때 undefined로 초기화됩니다.
  • 그러나 함수 본문은 호이스팅되지 않습니다.
  • 따라서 함수 표현식을 통해 정의된 함수는 변수가 초기화된 후에만 호출할 수 있습니다.
// 호이스팅 때문에 변수는 이미 존재하지만, 함수의 본문은 아직 할당되지 않았으므로 에러 발생
helloExpression(); 
// TypeError: helloExpression is not a function

var helloExpression = function() {
    console.log("안녕하세요!");
};

helloExpression(); 
//안녕하세요!

알겠습니다. 화살표 함수에 대한 호이스팅의 동작을 자세히 알아보겠습니다.

화살표 함수 (Arrow Functions)

  • 화살표 함수는 함수 표현식의 간결한 버전으로 생각할 수 있습니다.
  • 화살표 함수는 변수에 할당되므로, 변수의 호이스팅 규칙이 적용됩니다.
  • 따라서 화살표 함수 자체는 호이스팅되지 않습니다. 하지만, 해당 화살표 함수가 할당된 변수는 호이스팅 될 때 undefined로 초기화됩니다.
  • 그 결과, 변수 할당 이전에 화살표 함수를 호출하려고 하면 오류가 발생합니다.
// 호이스팅 때문에 변수는 이미 존재하지만, 함수의 본문은 아직 할당되지 않았으므로 에러 발생
helloArrow(); 
// TypeError: helloArrow is not a function

var helloArrow = () => {
    console.log("안녕하세요!");
};

helloArrow(); 
// "안녕하세요!

참고: let 또는 const와 함께 선언된 화살표 함수의 경우, 호이스팅이 발생하지만 Temporal Dead Zone (TDZ) 에 있어 호출 시 "ReferenceError"가 발생합니다.

helloArrowWithLet(); 
// ReferenceError: Cannot access 'helloArrowWithLet' before initialization

let helloArrowWithLet = () => {
    console.log("let으로 선언된 화살표 함수입니다.");
};

helloArrowWithLet(); 
// let으로 선언된 화살표 함수입니다.

정리

  • 함수 선언식: 함수의 전체 본문이 호이스팅 되므로 선언 전에 호출할 수 있습니다.
  • 함수 표현식: 변수는 호이스팅되나 함수 본문은 호이스팅 되지 않으므로, 본문 할당 전에는 함수로서 호출할 수 없습니다.
  • 화살표 함수: 변수의 호이스팅 규칙이 적용되므로, 화살표 함수 본문은 호이스팅 되지 않습니다. 변수는 호이스팅 되나 초기화 전에 함수로서 호출하려면 오류가 발생합니다.

호이스팅의 범위

전역 범위 (Global Scope)에서의 호이스팅

  • 전역 범위에서 선언된 변수나 함수는 전역 객체 (브라우저에서는 window 객체)에 바인딩됩니다.
  • 호이스팅이 발생하면, 이러한 변수나 함수 선언은 스크립트의 최상단으로 끌어올려집니다.
  • 이는 앞에서 작성한 일반적인 호이스팅입니다.
console.log(globalVar); // undefined
var globalVar = "전역 변수";
console.log(globalVar); // "전역 변수"

globalFunction(); // "전역 함수 호출됨"

function globalFunction() {
    console.log("전역 함수 호출됨");
}

함수 범위 (Function Scope)에서의 호이스팅

  • 함수 내부에서 선언된 변수나 함수는 해당 함수 범위 내에서만 접근 가능합니다.
  • 함수 범위에서의 호이스팅은 해당 함수의 최상단으로 변수나 함수 선언을 끌어올립니다.
function exampleFunction() {
    console.log(localVar); // undefined
    var localVar = "함수 내부의 변수";
    console.log(localVar); // "함수 내부의 변수"

    localFunction(); // "함수 내부의 함수 호출됨"

    function localFunction() {
        console.log("함수 내부의 함수 호출됨");
    }
}

exampleFunction();
  • 또한, 함수 내부에서 var로 선언된 변수는 해당 함수의 전체 범위에서 사용할 수 있습니다. 이는 함수 범위의 호이스팅 때문입니다.
function hoistingExample() {
    console.log(functionScopedVar); // undefined
    if (true) {
        var functionScopedVar = "변수";
    }
    console.log(functionScopedVar); // "변수"
}

hoistingExample();

위에 예제에서 var functionScopedVar = "변수"if 블록 내부에서 선언되었지만, 함수의 어디에서나 접근할 수 있습니다. 이는 var가 함수 범위를 가지고, 호이스팅이 적용되기 때문입니다.

만약 조건절이 false인 경우는 var로 선언된 변수의 초기화 코드는 실행되지 않습니다. 하지만 변수 자체는 여전히 호이스팅되어 함수의 최상단에 선언되므로, undefined 값으로 초기화 됩니다.

function hoistingExample() {
    console.log(functionScopedVar); // undefined
    if (false) {
        var functionScopedVar = "변수";
    }
    console.log(functionScopedVar); // undefined
}

hoistingExample();

이러한 특성 때문에 var는 함수 내에서 예측하기 어려운 코드가 될 수 있습니다.
반면, letconstvar와 다르게 블록 범위(block scope)를 가집니다.

블록 범위(Block Scope)와 let, const

  • var는 함수 범위를 가지며, letconst는 블록 범위를 가집니다.
  • letconst로 선언된 변수는 호이스팅 되나, 초기화 전에 접근하려고 하면 Temporal Dead Zone (TDZ)에서 오류가 발생합니다.
function blockScopeExample() {
    if (true) {
        console.log(blockScopedVar); 
        // ReferenceError: Cannot access 'blockScopedVar' before initialization
        let blockScopedVar = "블록 범위의 변수";
        console.log(blockScopedVar); // "블록 범위의 변수"
    }
    console.log(blockScopedVar); 
    // ReferenceError: blockScopedVar is not defined
}

blockScopeExample();

TDZ(Temporal Dead Zone)

letconst로 선언된 변수도 호이스팅은 발생합니다. 다만, 이 변수들은 Temporal Dead Zone (TDZ) 시기에 접근할 수 없습니다. 왜냐하면, letconst가 블록 범위(block scope)를 가지며, 선언과 초기화가 분리되어 이루어지기 때문입니다.

Temporal Dead Zone (TDZ)

  • Temporal Dead Zone (TDZ)는 변수가 코드에서 물리적으로 선언된 위치와 초기화(할당)된 위치 사이의 코드 영역을 지칭합니다.
  • 이 시간 동안 해당 변수에 접근하려고 하면 JavaScript는 "ReferenceError"를 던집니다.
// 호이스팅
// TDZ 시작
console.log(myLetVariable); 
// ReferenceError: Cannot access 'myLetVariable' before initialization
// TDZ 끝, 초기화 시작
let myLetVariable = 10;

console.log(myLetVariable); // 10

letconst 선언도 호이스팅 되지만, 호이스팅 되는 시점과 초기화되는 시점 사이에 TDZ가 존재합니다.

  1. 호이스팅: 변수 선언이 스코프의 최상단으로 끌어올려집니다.
  2. TDZ 시작: 변수 선언문을 코드 실행이 만나면 TDZ가 시작됩니다.
  3. TDZ 끝 & 초기화: 변수 초기화(할당)가 실행되면 TDZ가 끝나고 변수가 사용 가능해집니다.

비교: var, let, const

  • var: 함수 범위를 가지며 호이스팅 됩니다. 호이스팅 된 변수는 undefined로 초기화됩니다.
  • let: 블록 범위를 가지며 호이스팅 되나, TDZ에 있을 때에는 접근할 수 없습니다.
  • const: 블록 범위를 가지며 호이스팅되나, TDZ에 있을 때에는 접근할 수 없습니다. 초기화 없이 선언이 불가능합니다.