예제로 만나보는 자바스크립트 호이스팅(hoisting)

코드를 변경하지 않는 호이스팅

호이스팅은 ECMAScript 2015 및 그 이전 표준 명세에서 사용된 적이 없는 용어이다. hoist라는 영단어 뜻이 끌어올린다라는 의미에서 유추해볼 수 있듯 hoisting은 변수와 함수를 최상단으로 끌어올린다고 개괄적으로 정의할 수 있다. 이에 호이스팅은 변수 및 함수 선언이 물리적으로 작성한 코드의 상단으로 옮겨지는 것으로 알려져 있지만, 실제로는 컴파일 단계에서 메모리에 저장될 뿐, 코드 안에서는 그대로 유지된다.

호이스팅이 개념이 나오게 된 배경은 자바스크립트가 함수를 실행하기 전에 반드시 선언되어야 한다는 여타 언어들과 달리 이 순서를 지키지 않아도 무방하도록 설계한 데서 유래한다. 정의되지 않은 함수를 어떻게 호출할 수 있는 것인지, 자바스크립트에는 마법이라도 있는 것일까?

실행 컨텍스트와 절친인 호이스팅

변수나 함수를 뒤늦게 정의해도 사용이 가능한 이유는 실행 컨텍스트에서 현재 컨텍스트 내의 식별자들에 대한 정보 및 외부 환경 정보를 스캔하기 때문이다. 실행 컨텍스트는 다음 포스팅에서 자세히 알아볼 예정이고, 지금은 실행할 코드에 제공할 환경 정보들을 모아놓는 객체로 기억해두고, 이 환경 정보들을 어떻게 수집하는지 간단히 살펴봄으로써 호이스팅을 좀 더 이해할 수 있다.

  • 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됨
    • 매개변수의 이름 : 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자
    • 함수 선언 : 선안한 함수가 있는 경우 함수 그 자체
    • 변수명 : var로 선언된 변수의 식별자
  • 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집
    • 수집을 마쳤다하더라도 아직 코드들은 실행되기 전 상태이지만
    • 이미 해당 환경에 속한 코드의 변수명들을 모두 알게 되는 셈

다시 정리해본 호이스팅

  • 엔진의 실제 동작 방식 대신에 자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행한다라고 생각한다면
    • 변수 정보를 수집하는 과정을 더욱 이해하기 쉬운 방법으로 대체한 가상의 개념
  • 자바스크립트 엔진이 실제로 끌어올리지는 않지만 편의상 끌어올린 것으로 간주하자
    • 변수 혹은 함수 선언부만 끌어올려지며
    • 할당부는 그 자리에 그대로 유지됨
  • 함수도 호이스팅 됨
    • 자바스크립트의 창시자인 브랜든 아이크가 자바스크립트를 유연하고 배우기 쉬운 언어로 만들고자 해서 탄생한 개념
    • 덕분에 함수를 선언한 위치와 무관한게 그 함수를 실행할 수 있게 됐지만 오히려 이로 인해 더 많은 혼란을 야기하기도 함

예제를 통해 만나본 호이스팅

기본 코드를 호이스팅으로 변환해보기

코드를 직접 실행해보기 전에 머릿속으로 시뮬레이션 해보자.

<예제1 - 호이스팅이 발생할 것 같은 샘플 코드>

function hello() {
	console.log(1, world); // 예상하건데 world?
	var world = "world";
	console.log(2, world); // 예상하건데 world?
	function world() {
		console.log("hello world");
	}
	console.log(3, world); // 예상하건데 f world()?
}
hello();

예제1을 실행하면 코드내 주석처럼 예상해볼 수 있을 것 같지만, 실행 결과는 완전히 다르다!

<결과1>

1 ƒ world() { window.runnerWindow.proxyConsole.log("hello world");}
2 world
3 world

위 예제1을 실제로 코드가 변하지는 않지만 호이스팅된 것으로 예제2처럼 재표현하여 이해를 도울 수 있다.

<예제2 - 호이스팅으로 재표현한 예제1>

function hello() {
	var world;
	var world = function world() {
		console.log("hello world");
	}; // 실행 컨텍스트 수집 대상
	console.log(1, world); // 출력 : f world()
	world = "world"; // 변수 할당 부는 그대로 유지
	console.log(2, world); // 출력 : world
	console.log(3, world); // 출력 : world
}
hello();

함수를 선언하는 다양한 방법 - 함수 선언문과 함수 표현식

함수를 선언하는 방법에 따라 호이스팅의 결과도 다를 수 있다. 먼저 다양한 함수 선언 방법을 살펴보자.

<예제3 - 다양한 함수 선언 방법>

// 함수 선언문
function sayHello() {
	console.log("Hello");
}
sayHello();

// 익명 함수 표현식
var sayWorld = function () {
	console.log("World");
};
sayWorld();

// 기명 함수 표현식
var sayWelcome = function welcome() {
	console.log("Welcome");
};
sayWelcome();

예제3에서 보듯 함수를 새롭게 정의할 땐 크게 2가지 방식이 있다.

  • 함수 선언문
    • 함수를 선언하기만 하고 별도로 호출하지 않음
    • 함수명이 반드시 정의되야 함
  • 함수 표현식
    • 정의한 함수를 별도의 변수에 할당하는 것
    • 함수명이 없는 익명 함수 혹은 기명 함수로 정의 가능

함수 선언문과 함수 표현식의 호이스팅 차이

<예제4 - 함수 선언 방법별 호이스팅을 예측해보기>

console.log(sum(1, 2));
console.log(multiply(3, 4));

// 함수 선언문으로 sum() 를 선언
function sum(a, b) {
	return a + b;
}

// 함수 표현식으로 multiply() 를 선언
var multiply = function (a, b) {
	return a * b;
};

함수 선언문과 함수 표현식으로 정의한 예제4를 실행하면 sum()과 multiply()가 잘 실행될까? 결과2를 확인해보자.

<결과2>

VM388:2 Uncaught TypeError: multiply is not a function
    at <anonymous>:2:13

어째서 multiply는 함수가 아니라는 오류가 발생한 것일까? sum()도 동일하게 코드상 호출부분 이후에 정의했는데 말이다. 그건 바로 호이스팅때문이다. 다음 예제5는 예제4를 호이스팅으로 재표현한 코드다.

<예제5 - 함수 선언 방법별 호이스팅으로 재표현한 예제4>

// 함수 선언문은 전체를 호이스팅 한다.
var sum = function sum(a, b) {
	return a + b;
};

// 함수 표현식은 변수만 호이스팅하고,
var multiply;

console.log(sum(1, 2));
console.log(multiply(3, 4));

// 함수 표현식에서 할당부는 제 위치에 유지한다.
multiply = function (a, b) {
	return a * b;
};

호이스팅으로 재표현한 예제5를 보면 함수 역시도 변수와 마찬가지로 선언부는 맨 위로 올리지만, 할당부는 원래 선언한 위치에 유지한다. 그래서 이 console.log(multiply(3, 4)); 부분에서 선언만 하고 함수가 할당되기 전인 변수 multiply를 함수처럼 호출해서 에러가 발생하게 된다. 위 예제5는 함수 선언문과 함수 표현식이 어떤 차이가 있는지 보여준다. 한가지 더 예제6을 살펴보자.

함수 선언문의 호이스팅 사이드 이펙트

같은 함수를 하나의 컨텍스트에서 함수 선언문 형태로 중복 선언 한 경우 어떤 영향이 있을까? 예제6을 통해 살펴보자.

<예제6 - 같은 이름으로 중복 정의한 함수 선언문>

console.log(sum(1, 2)); // return 3?

// 첫번째 sum 함수 선언문
function sum(a, b) {
	return a + b;
}

// ... (중략)

console.log(sum(3, 4)); // return 7?
// 두번째 sum 함수 선언문
function sum(a, b) {
	return `${a} + ${b} = ${a + b}`;
}

예제6의 경우 순서대로 호이스팅되면서 나중에 선언한 두번째 sum()으로 overriding된다. 흔한 케이스는 아니지만 코드가 어떻게 작성되어있는지에 따라 이 경우 오류 없이 지나칠 수 있고 찾기 힘든 버그로 둔갑할 수도 있다.

<예제7 - 호이스팅으로 재표현한 예제6>

var sum = function sum(a, b) {
	return a + b;
};
var sum = function sum(a, b) {
	return `${a} + ${b} = ${a + b}`;
};

console.log(sum(1, 2));
console.log(sum(3, 4));

<결과3>

1 + 2 = 3
3 + 4 = 7

막연히 변수를 위로 끌어올린다고만 알고 있었던 호이스팅을 직접 예제를 통해 정리하니 명쾌해졌다. 호이스팅은 앞서 언급한 대로 혼자서 단독 작동하는 것이 아니라 실행 컨텍스트로 인해 움직인다. 다음에는 이 실행 컨텍스트에 대해 공유할 예정이다 :)

References