코딩/Javascript

자바스크립트 호이스팅(Hoisting) 완벽 이해

드리프트 2021. 8. 4. 20:24
728x170

목차

 

1. 호이스팅이란

 

2. 함수 범위(function scope) 변수: var

 

   2.1. 호이스팅과 var

 

3. 블록 범위(block scope) 변수: let

 

   3.1. 호이스팅과 let

 

4. const

 

   4.1. const 호이스팅

 

5. 함수 선언(function declarations)

 

6. 클래스 선언

 

 

 

안녕하세요?

 

우리가 자바스크립트나 다른 랭귀지로 코드를 짤 때 제일 중요한 것 중 하나가 바로 변수인데요.

 

변수란 서로 상호 작용하는 작은 데이터 또는 논리 조각이라고 볼 수 있습니다.

 

이러한 변수의 활동으로 프로그램(애플리케이션)이 작동되는 것이죠.


JavaScript 변수의 중요한 측면은 바로 그 변수에 액세스 할 수 있는 시점을 정의하는 호이스팅(Hoisting)이라 할 수 있습니다.

 

이제 본격적으로 자바스크립트 호이스팅에 대해 알아보겠습니다.

 

 

 

호이스팅(Hoisting)

 

호이스팅은 변수나 함수 선언을 함수 유효 범위(또는 함수 외부의 경우 전역 범위)의 맨 위로 이동하는 메커니즘입니다.

 

호이스팅은 다음과 같이 3단계로 구성된 변수의 수명 주기에 영향을 줍니다.

 

  1. 선언 - 변수를 만듭니다. 예) var myValue
  2. 초기화(할당) - 변수에 값을 할당. 예) myValue = 100
  3. 사용 - 그 변수를 참조하여 코딩에 이용. 예) alert(myValue)

 

자바스크립트 호이스팅 프로세스는 일반적으로 다음과 같이 진행됩니다.

 

 먼저 변수를 선언한 다음, 특정 값으로 초기화하고, 마지막으로 그 변수를 사용합니다.

 

예를 들어 보겠습니다.

 

// 선언 Declare
var strNumber;

// 초기화(할당) Initialize
strNumber = '16';

// 사용 Use
parseInt(strNumber); // => 16

 

함수의 경우에는 다음과 같이 호이스팅이 일어납니다.

 

함수를 선언하고 나중에 응용 프로그램에서 사용(또는 호출)할 수 있습니다.

 

두 번째 단계인 초기화는 생략합니다.

 

예를 들어 보겠습니다.

 

// 선언 Declare
function sum(a, b) {
    return a + b;
}

// 사용 Use
sum(5, 6); // => 11

선언 -> 초기화 -> 사용 단계가 연속적일 때 모든 것이 단순하고 자연스럽게 보입니다.

 

 가능하면 JavaScript로 코딩할 때는 이 패턴을 적용해야 합니다.

 

그런데 JavaScript는 이 순서를 엄격하게 따르지는 않으며, 훨씬 더 많은 유연성을 제공합니다.

 

예를 들어, 함수는 선언 전에 사용할 수 있습니다: 사용 -> 선언.


다음 코드 예제는 먼저 double(5) 함수를 호출(사용)하고 나중에 함수 double(num) {...}를 선언하고 있습니다.

 

// 사용 Use
double(5); // => 10

// 선언 Declare
function double(num) {
    return num * 2;
}

왜냐하면 자바스크립트에서는 함수 선언이 스코프의 맨 위로 올라가기 때문에 그렇습니다. (호이스팅)

 

그러나 다음과 같은 세 가지의 경우에 각각 다르게 호이스팅이 일어납니다.

  • 변수 선언: var, let 또는 const 키워드 사용
  • 함수 선언: function <name>() {...} 구문 사용
  • 클래스 선언: class 키워드 사용

이러한 차이점을 더 자세히 살펴보겠습니다.

 

 

 

함수 범위(function scope) 변수: var

 

변수 선언문(variable statement)은 함수 범위(function scope) 내에서 변수를 생성하고 초기화합니다. 

 

기본적으로 선언은 되었지만 초기화되지 않은 변수에는 undefined 값이 적용됩니다.

 

// num 변수 선언
var num;   // 선언만 하고 초기화되지 않아 undefined 값이 할당

console.log(num); // => undefined

// str 변수를 선언하고 바로 할당(초기화)
var str = 'Hello World!';

console.log(str); // => 'Hello World!'

 

호이스팅과 var

 

var로 선언된 변수는 유효 함수 범위(function scope)의 맨 위로 호이스트 됩니다.

 

함수 단위로 생각하면 쉽습니다.

 

선언 전에 해당 변수에 액세스 하면 그 변수는 undefined을 리턴합니다.

 

var로 선언하기 전에 myVariable에 액세스 한다고 가정합시다.

 

이 상황에서 선언은 double() 함수 범위의 맨 위로 이동되고 변수는 undefined로 할당됩니다.

 

function double(num) {
    console.log(myVariable); // => undefined
    var myVariable;
    return num * 2;
}

double(3); // => 6

JavaScript는 선언 var myVariable을 double() 함수 범위의 맨 위로 이동하고 코드를 다음과 같이 해석하게 됩니다.

function double(num) {
    var myVariable;          // 함수 유효범위의 맨 꼭대기로 이동
    console.log(myVariable); // => undefined
    return num * 2;
}

double(3); // => 6

var 구문을 사용하여 변수를 선언하면 선언뿐만 아니라 바로 초기 값을 할당할 수 있습니다.

 

 var str = 'initial value' 처럼 말입니다.

 

변수가 호이스트 되면 선언은 맨 위로 이동하지만 초기 값 할당은 그 자리를 유지합니다.

 

function sum(a, b) {
    console.log(myString); // => undefined
    var myString = 'Hello World';
    console.log(myString); // => 'Hello World'
    return a + b;
}

sum(16, 10); // => 26

var myString은 유효 함수 범위의 맨 위로 호이스트 되지만 초기 값 할당 myString = 'Hello World'는 영향을 받지 않습니다. 

 

위의 코드는 다음과 동일합니다.

 

function sum(a, b) {
    var myString;             // 선언만 맨 위로 이동
    console.log(myString);    // => undefined
    myString = 'Hello World'; // 초기값 할당은 그자리 유지
    console.log(myString);    // => 'Hello World'
    return a + b;
}

sum(16, 10); // => 26

 

 

블록 범위(block scope) 변수: let

 

let 선언문은 블록 범위(block scope) 내에서 변수를 생성하고 초기화합니다.

 

let으로 변수를 선언하고 초기화하지 않은 변수에는 undefined 값이 할당됩니다.

 

ECMAScript 6 때 도입된 let은 코드를 모듈화하고 블록 수준에서 캡슐화할 수 있도록 도와주는 아주 훌륭한 기능입니다.

 

if (true) {
    // let으로 블록 범위 변수 선언
    let month;
    
    console.log(month); // => undefined
    
    // 블록 범위 변수 선언 및 할당
    let year = 1994;
    
    console.log(year); // => 1994
}

// month와 year 변수는 블록 바같쪽에서 접근 불가
console.log(year); // ReferenceError: year is not defined

 

 

호이스팅과 let

 

let 변수는 블록의 맨 위에 등록됩니다.

 

그러나 선언 전에 변수에 액세스 하면 JavaScript는 ReferenceError: <variable> is not defined 오류를 발생시킵니다.

 

해당 변수의 선언문부터 유효 블록 시작 위치까지 해당 변수는 일시적인 사각지대(Temporal Dead Zone)에 있으며, 우리는 그 변수에 액세스 할 수 없습니다.

 

예를 들어 보겠습니다.

 

function isTruthy(value) {
    var myVariable = 'Value 1';
    
    if (value) {
    
        /**
         * myVariable: 일시적 데드존
         */
        // Throws ReferenceError: myVariable is not defined
        console.log(myVariable);
    
        let myVariable = 'Value 2';
    
        // myVariable: 일시적 데드존 끝남
        console.log(myVariable); // => 'Value 2'
        return true;
  }
  
  return false;  
}

isTruthy(1); // => true

myVariable은 let myVariable이 if (value) {...} 블록의 상단까지 정렬되기까지 일시적 사각지대(Temporal Dead Zone)에 있습니다.

 

 이 영역의 변수에 액세스 하려고 하면 JavaScript에서 ReferenceError가 발생합니다.

 

여기서 흥미로운 질문을 할 수 있는데요.

 

실제로 myVariable이 블록의 시작 부분까지 호이스트 된 걸까요?

 

아니면 선언 전에 일시적 사각지대(Temporal Dead Zone)에서는 단순히 정의되지 않은 걸까요?

 

변수가 전혀 정의되지 않은 경우에도 ReferenceError 예외가 발생합니다.

 

함수 블록의 시작 부분을 살펴보면 var myVariable = 'Value 1'은 전체 함수 범위에 대한 변수를 선언하고 있습니다.

 

 if (value) {...} 블록에서 let 변수가 외부 범위 변수를 커버하지 않는다면, 즉, 관여하지 않는다면 일시적 사각지대(Temporal Dead Zone)에서 myVariable은 값 var myVariable = 'Value 1' 선언이 함수 블록의 범위를 가지므로 아마 'Value 1'을 갖게 된다고 생각할 수 있습니다. 그러나 이런 일은 절대 발생하지 않습니다.

 

정확히 설명하자면, 자바스크립트 엔진이 let 문장이 있는 블록을 만나면, 제일 먼저 블록의 맨 위에 변수가 선언됩니다.

 

선언된 상태에서 변수는 초기화되지(할당되지)만 않았을 뿐 여전히 사용할 수 없으며, 동일한 이름의 외부 범위 변수까지 커버합니다.

 

즉, let 으로 선언된 변수의 호이스팅으로 그 블록에서는 그 이름으로 메모리가 할당되어 외부 범위의 같은 이름 변수가 적용이 안됩니다.

 

나중에 프로그램 실행이 let 선언문을 통과하면, 변수가 초기화된 상태가 되어 사용할 수 있는 상태가 됩니다.

 

결국 let 선언의 블록 호이스팅은 외부 범위에 의한 수정으로부터 변수를 보호합니다. 심지어 선전되기 전에도 말이죠.

 

일시적 사각지대(Temporal Dead Zone)에서 let 변수에 액세스 할 때 참조 오류가 발생되는 형태는 더 나은 코딩 습관이 됩니다.

 

 먼저 선언한 다음 사용하라는 말입니다.


이 방식은 캡슐화 및 코드 흐름 측면에서 더 나은 JavaScript를 작성하기 위한 효과적인 접근 방식입니다. 

 

이는 var 호이스팅에서 잘 봐온 선언 전에 변수에 접근하는 것이 오해의 원인이 된다는 교훈에 기인합니다.

 

 

const

 

const 문은 블록 범위 내에서 상수를 생성하고 초기화합니다.

 

다음 예제를 볼까요?

 

const COLOR = 'red';

console.log(COLOR); // => 'red'

const ONE = 1, HALF = 0.5;

console.log(ONE);   // => 1

console.log(HALF);  // => 0.5

const로 상수를 정의하면 그 자리에서 바로 값을 할당해야 합니다.

 

 그리고 당연히 선언 및 초기화 후에는 상수 값을 수정할 수 없습니다.

 

const PI = 3.14;

console.log(PI); // => 3.14

PI = 2.14; // TypeError: Assignment to constant variable

 

const 호이스팅

 

const는 블록의 맨 위에 바로 등록됩니다.


일시적 사각지대(Temporal Dead Zone) 때문에 선언 전에 상수에 접근할 수 없습니다.

 

선언 전에 액세스 하면 JavaScript에서 오류가 발생합니다. ReferenceError: <constant> is not defined.

 

const 호이스팅은 let 문으로 선언된 변수와 동일한 동작을 합니다.

 

다음 예제를 살펴봅시다.

 

function double(number) {

   // TWO 상수에 대한 일시적 사각지대
   console.log(TWO); // ReferenceError: TWO is not defined
   
   const TWO = 2;
   
   // 일시적 사각지대가 끝남.
   return number * TWO;
}

double(5); // => 10

 

선언 전에 TWO를 사용하면 JavaScript에서 ReferenceError: TWO is not defined 오류가 발생합니다. 

 

따라서 상수를 먼저 선언하고 초기화한 다음 나중에 액세스해야 합니다.

 

 

 

함수 선언(function declarations)

 

함수 선언의 호이스팅에 따라 함수 선언 이전에도 같은 범위의 어디에서나 함수를 사용할 수 있습니다. 

 

즉, 현재 범위나 내부 범위의 아무 위치에서도 함수를 호출할 수 있습니다.

 

다음 예를 보시면 함수를 먼저 사용했고, 그다음에 선언한 경우입니다.

 

// 호이스팅된 함수를 먼저 사용
equal(1, '1'); // => false

// 함수 선언
function equal(value1, value2) {
    return value1 === value2;
}

 

equal() 함수가 유효 범위의 맨 위로 호이스트 되기 때문에 그렇습니다.

 

함수 선언 function <name>() {...}과 함수 표현식 var <name> = function() {...}의 차이점에 주목할 필요가 있습니다.

 

둘 다 함수를 생성하는 데 사용되지만 호이스팅 메커니즘이 다릅니다.

 

다음 예제에서 그 차이점을 알아봅시다.

 

// 함수 선언은 호이스팅되어 에러없이 사용 가능
addition(4, 7); // => 11

// 함수 표현식은 함수의 변수 즉, substraction이 호이스팅됨
// 즉, 함수의 이름 변수만 호이스팅되었고,
// 그 변수에 함수가 할당이 되지 않은 상태임
substraction(10, 7); // TypeError: substraction is not a function

// 함수 선언 (Function declaration)
function addition(num1, num2) {
    return num1 + num2;
}

// 함수 표현식 (Function expression)
var substraction = function (num1, num2) {
    return num1 - num2;
};

addition 함수는 완전히 호이스팅되며 선언 전에 호출될 수 있습니다.


그러나 substraction 함수는 변수를 사용하여 선언되어 변수 이름인 substraction 변수만 호이스팅되지만,

 

정작 그 함수가 호출될 때는 undefined 값이 지정되어 함수 실행이 안됩니다.

 

위 코드에서는 substraction에서 TypeError : substraction is not a function. 이 발생합니다.

 

 

 

클래스 선언

 

클래스를 선언하고 객체를 인스턴스화하는 방법을 볼까요?

 

class Point {
   constructor(x, y) {
     this.x = x;
     this.y = y;     
   }
   
   move(dX, dY) {
     this.x += dX;
     this.y += dY;
   }
}

// 클래스 인스턴스 생성
var origin = new Point(0, 0);

// 클래스 메서도 호출
origin.move(50, 100);

 

 

클래스 호이스팅

 

클래스 변수는 블록 범위의 시작 부분에 등록됩니다.

 

그러나 정의 전에 클래스에 액세스 하려고 하면 JavaScript에서 레퍼런스 에러를 뿜어냅니다. ReferenceError: <name> is not defined.

 

따라서 올바른 접근 방식은 먼저 클래스를 선언하고 나중에 이를 사용하여 개체를 인스턴스화하는 것입니다.

 

클래스 선언에서 호이스팅은 let 문으로 선언된 변수와 유사합니다.

 

선언 전에 클래스가 인스턴스화 되면 어떻게 되는지 봅시다.

 

// Company 클래스 사용시 아래 에러 발생
// Throws ReferenceError: Company is not defined
var apple = new Company('Apple');


// 클래서 선언 Class declaration
class Company {
  constructor(name) {
    this.name = name;    
  }
}

// 클래스 선언후 객체 인스턴스화 해서 사용해야 함.
var microsoft = new Company('Microsoft');

예상대로 클래스 정의 전에 new Company('Apple')를 실행하면 ReferenceError가 발생합니다.

 

클래스는 변수 선언문(var, let 또는 const 사용)을 사용하여 클래스 표현식 형태로 생성할 수 있습니다.

 

// Sqaure 클래스 사용
console.log(typeof Square);   // => 'undefined'

//Throws TypeError: Square is not a constructor
var mySquare = new Square(10);

// 변수 선언으로 클래스 선언
var Square = class {
  constructor(sideLength) {
    this.sideLength = sideLength;    
  }
  
  getArea() {
    return Math.pow(this.sideLength, 2);
  }
};

// 올바른 사용 위치
var otherSquare = new Square(5);

 

Square 클래스는 var Square = class {...} 형태의 변수 선언문으로 선언됩니다.

 

 Square 변수는 스코프 상단으로 호이스트 되지만 클래스 선언 라인까지 undefined 값을 가지고 있습니다.

 

따라서 클래스 선언 전에 var mySquare = new Square(10)를 실행하면 undefined를 생성자로 호출하려고 시도하는 형태라 JavaScript는 TypeError: Square is not a constructor 에러를 발생시킵니다.

 

 

 

결론

 

지금까지 알아봤듯이 JavaScript의 호이스팅에는 다양한 형태가 있습니다.

 

각각의 작동 방식을 정확히 알고 있더라도 좋은 코딩 습관을 위해 선언 -> 초기화 -> 사용의 순서로 변수를 사용하는 것이 좋습니다.

 

 

 

그리드형