JavaScript

hoisting-thumb.png

호이스팅이란?

호이스팅이란 변수 혹은 함수의 선언이 해당 스코프의 최상단으로 끌어 올려진 것 같이 동작하는 자바스크립트의 특징이다. 다음 예시를 보자.

console.log(foo); // undefined

var foo;

자바스크립트 코드는 인터프리터에 의해 위에서부터 아래로 한줄 씩 실행된다. 위 코드에서 변수 foovar 키워드로 선언이 되기 전, console.log 가 이를 참조하고 있다. 따라서, 참조 에러 (Reference Error)가 발생해야 할 것 처럼 보이지만, 에러가 발생하지 않고 undefined 가 출력된다. 또 다른 예시를 보자.

console.log(foo); // Reference Error : foo is not defined

let foo;

똑같은 구조지만 let/const 키워드로 변수를 선언했다는 차이만 있다. 하지만, 이 코드는 참조 에러를 발생시킨다. 왜 이런 것일까? 바로 호이스팅 때문이다.

실제로 var 키워드의 선언문이 코드 상단으로 끌어 올려진 것은 아니다. 호이스팅을 이해하려면 자바스크립트 엔진이 코드를 실행할 때, 실행 컨텍스트를 생성하는 과정에서 스코프에 선언을 등록하는 방식을 알아야한다.

왜 발생하는가?

자바스크립트 엔진은 함수를 실행시키기 전, 해당 함수를 한 번 훑어본 후 함수 안에 존재하는 변수와 함수선언에 대한 정보를 기억하고 있다가 실행시킨다. 이 암묵적인 과정이 마치 선언이 끌어 올려진 듯한 느낌을 주는 것이다.

생명 주기의 이해

변수와 함수 선언의 정보들을 기억 (저장)하는데 컴퓨터는 자원 (보통 컴퓨터의 메모리)를 사용한다. 컴퓨터 시스템의 모든 자원은 유한하기 때문에 자원은 생성되고 사용되며 결국에 소멸되는 생명 주기를 갖는다.

변수 생성 과정 ( 3단계 )

변수의 생명 주기 중 변수 생성 과정은 공통적으로 ( 선언 / 초기화 / 할당 ) 총 3단계를 거친다.

1 ) 선언 단계 ( Declaration Phase)

  • 자바스크립트 엔진이 변수를 실행 컨텍스트의 변수 객체에 등록한다. ( 변수 식별자를 등록할 뿐, 실제 메모리에는 아직 존재하지 않음 → 참조 불가 상태 )
  • 이 변수 객체는 런타임 시, 스코프가 참조하는 대상이 된다.

2 ) 초기화 단계 (Initialization Phase)

  • 변수 객체에 등록된 변수를 위한 공간을 메모리에 확보한다. ( 변수 객체가 메모리를 가리키며 참조가 가능하게 됨)
  • 이 단계에서 변수는 undefined로 초기값이 설정(initialized)된다.

3 ) 할당 단계 ( Assignment Phase)

  • undefined 상태인 변수에 실제 값을 할당한다.

var vs let/const 호이스팅의 차이

맨 처음 예시에서 본 var 키워드로 선언한 변수의 호이스팅은 변수 생성 단계에서 그 원인이 나타난다. 다시 예시와 함께 살펴보자.

var 키워드 변수 생성 단계

var 키워드로 선언된 변수는 런타임 이전에 선언 단계 + 초기화 단계가 같이 실행된다.

// 1) 선언 단계 & 초기화 단계가 이미 완료된 상태
console.log(foo); // undefined

var foo;
console.log(foo); // undefined

// 2) 할당 단계
foo = 1;

console.log(foo); // 1

위 코드를 예시로, 좀 더 자세히 살펴보면 자바스립트 엔진은 다음 과정을 거쳤다.

1 ) 선언 단계에서 코드를 한번 훑어본 후 foo 란 변수의 존재를 변수 객체에 등록해놓았다.

동시에, 초기화 단계에서 hoisting 이라는 변수에 대한 값을 담기 위해 메모리를 확보한 후 그 공간에 undefined 를 초기값으로 설정해놓은 것이다.

2 ) 메모리가 확보되었으니, 할당 단계에서 숫자 값을 할당해주었다.

var-keyword-lifecycle.png

let/const 키워드 변수 생성 단계

let/const 키워드로 선언된 변수는 선언 단계초기화 단계가 분리되어 진행된다.

// 1-1) 선언 단계 ( TDZ 시작 )

// 1-2)
console.log(foo); // ReferenceError : Cannot access 'foo' before initialization

// 2) 초기화 단계 ( TDZ 끝 )
let foo;
console.log(foo); // undefined

// 3) 할당 단계
foo = 1;

console.log(foo); // 1

위 코드를 예시로, 좀 더 자세히 살펴보면 자바스립트 엔진은 다음 과정을 거쳤다.

1 - 1 ) 선언 단계에서 코드를 한번 훑어본 후 foo 란 변수의 존재를 변수 객체에 등록해놓았다.

1 - 2 ) 초기화 단계 이전에 변수를 참조하지만, foo 란 변수를 위한 메모리가 아직 확보되지 않은 상태이기 때문에, 참조 에러를 발생시킨다. 이로써, 선언 단계 ~ 초기화 단계 시작까지 일시적 사각지대 (TDZ)가 형성된다.

일시적 사각지대 (Temperal Dead Zone) : 변수를 위한 메모리 공간이 확보되지 않아 변수를 참조할 수 없는 구간. 스코프의 시작부터 초기화 단계의 시작점까지를 일컫는다.

2 ) 초기화 단계에서 foo 변수를 위한 메모리를 확보한다. 마찬가지로, 초기 값은 undefined 이다.

3 ) 할당 단계에서 숫자 1을 할당해주었다.

let-keyword-lifecycle.png

let / const는 호이스팅이 일어나지 않는 것인가?

호이스팅의 범위를 선언까지만 보는가 아니면 선언과 초기화까지 보는가, 이 관점에 따라 답이 다를 수 있다. 다만, ‘함수의 선언이 해당 스코프의 최상단으로 끌어 올려진 것 같이 동작하는 현상’이라는 처음의 설명에 빗대어 보면 let 과 const 또한 호이스팅 대상이라고 보는 것이 맞다.

함수 호이스팅

함수선언문

일반적인 프로그래밍 언어에서의 함수 선언과 비슷한 형식이다.

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

함수선언문의 호이스팅

함수의 선언과 할당이 동시에 일어나기 때문에, 함수가 위치한 스코프의 최상단으로 끌어올려진다.

아래의 예시를 보자.

printName 함수 스코프 내에서 inner 함수가 선언된 형태다. inner 함수의 선언문이 함수의 호출보다 뒤에 있음에도, 호이스팅에 의해 정상적으로 함수가 동작한다.

function printName(firstname) {
  // 함수선언문
  var result = inner(); // 선언 + 초기화 및 할당
  console.log(typeof inner); // "function"
  console.log("name is " + result); // "name is inner value"

  function inner() {
    // (호이스팅) 함수선언문
    return "inner value";
  }
}

printName();

마찬가지로, 좀 괴상하지만 var 변수와 함수 선언문의 호이스팅으로 인해 정상적으로 동작하는 코드다.

function printName(firstname) {
  result = inner(); // 할당
  console.log(typeof inner); // "function"
  console.log("name is " + result); // "name is inner value"

  var result; // (호이스팅) 변수의 선언 + 초기화

  function inner() {
    // (호이스팅) 함수선언문
    return "inner value";
  }
}

printName();

var를 사용하면 이런 가독성이 구린 코드도 동작하기 때문에, let / const 를 사용하는 것이 좋다.

함수 표현식

변수값에 함수 표현을 담아 놓은 형태로 유연한 자바스크립트 언어의 특징을 활용한 선언 방식이다.

var test1 = function () {
  // (익명) 함수표현식
  return "익명 함수표현식";
};

var test2 = function test2() {
  // 기명 함수표현식
  return "기명 함수표현식";
};

함수표현식의 호이스팅

함수의 선언과 할당이 분리되기 때문에, 선언과 호출 순서에 따라 정상적으로 함수가 실행되지 않을 수도 있다.

아래 3가지 경우를 살펴보자. 3가지 경우 모두 printName 함수 스코프 내에서 inner 함수가 함수표현식 또는 함수선언문으로 선언된 모습이다. 두 선언 방식에 따라 호이스팅이 어떻게 함수의 동작에 영향을 주는지 집중하자.

1. 함수표현식의 선언이 호출보다 위에 있는 경우 → 정상 동작

정석적인 형태의 코드다. 변수와 함수의 선언문이 상단에 있고 아래에서 참조되고 있다.

function printName(firstname) {
  var inner = function () {
    // 함수표현식
    return "inner value";
  };

  var result = inner(); // 함수 호출
  console.log("name is " + result); // "name is inner value"
}

printName();
function printName(firstname) {
  var inner; // (호이스팅) 변수의 선언 + 초기화
  var result; // (호이스팅) 변수의 선언 + 초기화

  inner = function () {
    // 함수표현식 할당
    return "inner value";
  };

  result = inner(); // 함수 호출
  console.log("name is " + result); // "name is inner value"
}

printName();

2. `var` 변수를 사용한 함수표현식의 선언이 호출보다 아래에 있는 경우 → TypeError

앞서 언급한 듯이, var 키워드 변수 생성 단계는 변수의 선언 + 초기화 다음 할당이다. var 키워드로 선언한 inner 는 호출된 시점에 어떠한 값도 할당되지 않았기 때문에, undefined 상태다.

undefined 인 변수의 값을 호출하게 된다면? 당연하게도, 함수가 아니기 때문에 호출할 수 없다.

function printName(firstname) {
  console.log(inner); // "undefined"
  var result = inner(); // ERROR!
  console.log("name is " + result);

  var inner = function () {
    // 함수표현식
    return "inner value";
  };
}

printName(); // TypeError: inner is not a function

아래 코드도 마찬가지다. inner 변수가 최상단에 선언과 동시에 초기화되어 undefined 값을 가지게 되었다. 그러나, 아직 함수가 할당되지 않은 상태에서 호출을 하게 된다면 타입 에러가 발생한다.

function printName(firstname) {
  var inner; // (호이스팅) 변수의 선언 + 초기화

  console.log(inner); // "undefined"
  var result = inner(); // ERROR!
  console.log("name is " + result);

  inner = function () {
    // 함수표현식
    return "inner value";
  };
}

printName(); // > TypeError: inner is not a function

3. `let / const` 변수를 사용한 함수표현식의 선언이 호출보다 아래에 있는 경우 → ReferenceError

let / const선언 단계만 호이스팅이 일어나고 초기화와 할당은 분리되어 일어난다. 따라서, 호이스팅이 일어나지 않는 것처럼 동작한다. 이 사실을 다시 머릿 속에 새긴 후 아래 코드를 보자.

아래 코드에서 inner 는 변수가 초기화 되기도 전에 호출되었다. 이 말은 즉, inner 라는 변수는 메모리 상에 없는 값인데 자바스크립트 엔진이 이 값을 참조하려고 하는 것이다. 따라서, 참조 에러가 발생한다.

function printName(firstname) {
  console.log(inner); // ERROR!
  let result = inner();
  console.log("name is " + result);

  let inner = function () {
    // 함수표현식 (변수의 초기화 -> 할당)
    return "inner value";
  };
}
printName(); // > ReferenceError: inner is not defined

호이스팅 우선순위

같은 이름의 var 변수 선언과 함수 선언의 호이스팅

변수 선언이 함수 선언보다 위로 끌어올려진다.

// 1. (호이스팅) 변수값 선언
var myName;
var yourName;

// 2. (호이스팅) 함수선언문
function myName() {
  console.log("Gwan Woo");
}

function yourName() {
  console.log("Hello World");
}

// 3. 변수값 할당
myName = "hi";
yourName = "bye";

// 변수 선언 (win) vs 함수 선언
console.log(typeof myName); // "string"
console.log(typeof yourName); // "string"
// 1. (호이스팅) 변수값 선언
var myName;
var yourName;

// 2. 변수값 할당
myName = "hi";
yourName = "bye";

// 3. (호이스팅) 함수선언문
function myName() {
  console.log("Gwan Woo");
}

function yourName() {
  console.log("Hello World");
}

// 변수 선언 (win) vs 함수 선언
console.log(typeof myName); // "string"
console.log(typeof yourName); // "string"

undefined 변수와 값이 있는 변수의 호이스팅

var value = "value"; // 값 할당
var noValue; // 값 할당 X

function value() {
  // 같은 이름의 함수 선언
  console.log("value Function");
}

function noValue() {
  // 같은 이름의 함수 선언
  console.log("noValue Function");
}

값이 할당되어 있지 않은 변수의 경우, 함수선언문이 변수를 덮어쓴다.

console.log(typeof noValue); // "function"

값이 할당되어 있는 변수의 경우, 변수가 함수선언문을 덮어쓴다.

console.log(typeof value); // "string"

Reference

Leave a comment