#hoisting

호이스팅에 대한 오해와 진실

호이스팅에 대한 오해와 진실

호이스팅이란 뭘까? 솔직히 프로그래밍을 공부한 지 얼마 안 되었을 때는 호이스팅과 호스팅이 똑같은 말인 줄 알았다. 그리고 이 개념에 대한 뜻을 몇 번 훑어보고는 ‘아, 변수나 함수를 스코프 상단으로 끌어올리는 기술이구나’라고만 이해했다. 과연 그럴까? 지금부터 호이스팅에 관한 몇가지 오해와 이것을 가능하게 하는 매커니즘에 대해 설명해보려고 한다.

Just Metaphor

현재의 내가 알고 있는 호이스팅의 뜻을 한마디로 정의 하라고 하면 변수, 함수의 선언부가 위치한 인접 스코프의 시작 지점에서 해당 식별자의 관측이 가능한 현상이라고 얘기할 수 있겠다. 단순하게 바라보자. 어떤 스코프에서 선언한 위치에 상관없이 선언 대상을 확인할 수 있다면 맨 처음에는 무슨 생각이 들까?

hoist(); //hoisted!

function hoist() {
  console.log('hoisted!');
}

아마도 알 수 없는 기술이 그것들을 스코프의 최상단 지점으로 옮긴 뒤, 코드에 대한 판독을 시작하는 흐름을 떠올리기 쉬울 것 같다.

function hoist() {
  console.log('hoisted!');
}

hoist(); //hoisted!

이렇게 보았을 때 Hoist(:끌어 올리다) + -ing 을 이어 붙인 호이스팅이란 표현이 썩 어색하게 느껴지지는 않는다. 하지만 이는 그저 개념을 친숙하게 느낄 수 있도록 도와주는 비유적 표현이다. 실제로 JS 엔진은 실행 시점에서 특정한 변수나 함수만 콕 찝어 상단으로 끌어올릴 만한 능력이 없다. 단지 실행 전에 어떠한 과정을 거쳐서 변수와 함수에 대한 전체적인 정보를 미리 알고 있을 뿐이다. 본격적으로 호이스팅에 대한 규칙을 알아보기 전에 방금 얘기한 ‘어떠한 과정’을 먼저 짚고 넘어 가자.

The Answer Is Compiler

단락을 설명하기 앞서 미리 얘기하자면, 컴파일 시점에서 여러 식별자를 수집하는 객체를 일컬을 때 Declarative Environment Records(ECMA 명세 기준)가 좀 더 정확한 표현이다. 하지만 용어가 사용되는 빈도와 호이스팅이란 주제 안에서 글이 쉽게 읽혀야 한다는 점을 고려하여 변수 객체로 표현하였다.

우선 JS는 코드를 실행하기 전에 따로 컴파일 과정을 거친다는 사실을 인지할 필요가 있다. 컴파일이라는 건 사람이 이해하는(고수준) 언어로 이루어진 코드를 컴퓨터가 이해할 수 있도록(저수준) 쪼개고, 체계적인 구조로 재가공하여 전체를 분석하는 일련의 과정이다. (엔진의 컴파일 과정을 자세히 알고 싶다면 여기What's in an Interpretation?부터 읽어볼 것을 추천한다) 이 과정에서 JS 엔진은 모든 스코프(or 실행 컨텍스트)를 탐색하며 각 스코프의 변수 객체에 여러 식별자를 수집한다. 즉 실행 시점으로 넘어가기 전에 선언된 식별자에 대한 정보를 이미 알고 있기 때문에 스코프의 어느 지점이든 관련된 함수/변수를 참조할 수 있는 것이다. 그렇지만 함수/변수에 상관없이 늘 동일한 규칙이 적용되지는 않는다.

호이스팅 규칙

호이스팅은 크게 함수 호이스팅과 변수 호이스팅으로 나뉜다. 그리고 해당 개념에 관한 규칙을 요약하면 다음과 같은 항목으로 이루어진다.

  1. 선언된 함수는 상단에서 참조, 호출이 가능하다.
  2. 선언된 var 는 상단에서 참조, 할당이 가능하다.
  3. 선언된 let , const 는 상단에서 참조, 할당이 불가능하다.

이 3가지 규칙을 찬찬히 읽어보고 밑에서 다루는 각 항목의 상세 설명을 확인하길 바란다.

Function Hoisting

함수 호이스팅은 다른 무엇보다 가장 먼저 이루어진다. 그리고 함수 호이스팅은 선언문에만 해당하는데 이는 굳이 어렵게 받아들일 필요가 없다. 함수 표현식은 결국 어떠한 변수로 함수를 할당하는 모양새여서 이것 또한 변수 호이스팅 사례로 볼 수 있기 때문이다.

var value = 'value';
var func = function () {};

함수의 선언문은 식별자가 변수 객체에 수집될 때 부가적으로 해당 함수 참조에 대한 초기화까지 자동으로 이루어진다. 그래서 선언된 함수는 상단에서 참조, 호출이 가능하다.

Variable Hoisting

변수는 프로그램 내에서 크게 세 가지 단계를 거친다.

  1. 선언 : 파싱 과정에서 변수 객체가 변수에 대한 식별자들을 수집한다.
  2. 초기화 : 식별자에 메모리를 할당하고 undefined 상태를 부여한다.
  3. 할당 : 변수 안에 직접 값을 넘겨 준다.

함수 호이스팅 과정에서 함수의 식별자를 선언하고 참조값을 초기화한 것처럼 변수 또한 마찬가지로 선언과 초기화를 해주어야만 값의 참조 및 할당이 가능하다. 다만 변수를 어떻게 선언했는지에 따라 선언, 초기화 시점이 달라질 수 있다는 점을 유의해야 한다.

  • var 는 호이스팅이 발생하면, 선언과 초기화가 거의 동시에 이루어진다. 실행 시점의 스코프 최상단에서 해당 변수에 대한 메모리가 살아있기 때문에 선언부 위치에 상관 없이 참조, 할당이 가능하다.
  • let , const 는 호이스팅이 발생하면, 선언만 이루어지고 실행 시점에서 실질적인 선언부를 만날 때까지 초기화는 이루어지지 않는다. 이 간극만큼 해당 변수에 대한 메모리는 존재하지 않기 때문에 선언부 상단에서 참조, 할당이 불가능하다.

let, const 가 동작하는 과정에서 스코프의 진입지점과 해당 식별자의 실질적 선언부 사이를 일시적 사각지대, TDZ(Temporal Dead Zone) 라고 한다. 여기서 변수는 존재하지만, 초기화가 되어있지 않다.

{
  /*
   * Temporal Dead Zone of a
   * a는 이 구간에서 참조할 수 없다.
   * console.log(a) // Reference Error
   */
  let a;
}

일각에서는 호이스팅의 범위를 어디까지 보아야 하는지 의견이 분분하다. 호이스팅은 선언만 포함한다는 쪽이 있는 반면, 선언 - 초기화까지 포함해야 한다는 쪽도 있다. 그래서 let, const는 호이스팅의 범주에 들어갈 수 없다는 것이 그들이 주장하는 바이다. 나의 경우에는 전자가 맞다고 생각하는데, 다음의 코드에서 console.log 를 호출하기도 전에 에러를 반환하는 이유는 결국 해당 문의 실행 전에 let 의 식별자를 관측했기 때문이다.

function a() {}

console.log(a);

let a;
// SyntaxError: Identifier 'a' has already been declared

‘호이스팅은 인접 스코프의 상단에서 선언부를 관측할 수 있는 현상’이라는 처음의 설명에 빗대어 보면 letconst 또한 호이스팅 대상이라고 보는 게 맞다.

How To Use?

함수 호이스팅은 추상화 수준으로 나뉜 여러 함수를 어떤 식으로 나열할지 고민해보면 사용법에 대한 답을 내릴 수 있을 것 같다. 위에서부터 읽어 내려나가는 게 편할 수도 있고, 표현식의 일관성을 더 맞추어주고 싶을 수도 있다. 이는 팀으로 운영되는 프로젝트라면 컨벤션적인 부분으로 해결할 수 있겠다.

function handler() {
  util();
}

function util() {}

/* 이렇게 쓸 수도 있다. */

var util = function () {};

var handler = function () {
  util();
};

그에 반해 변수 호이스팅은 엄격히 지양되고 있다. (무조건 var를 배제하는 것도 좋은 자세는 아니라는 시각에 일부 동의하지만, 협업을 하며 발생할 수 있는 소통 비용을 고려해보았을 때는 자제하는 것이 좋다고 생각한다.) 값을 덮어 씌워 얻을 수 있는 이점도 없거니와 코드의 오류 발생 소지를 다분히 높일 수 있기 때문이다. var 의 유연한 특성 때문에 발생할 수 있는 여러 피해를 방지하고자 letconst 가 출현하게 된 건데, TDZ 이슈 때문에 사용하는 것이 망설여진다면 애초에 처음부터 스코프의 상단에 변수를 선언해줌으로 해당 이슈를 방지하면 된다.

{
  const a = 1;
  let b = 2;
  //logic...
}

정리

호이스팅은 엔진의 특별한 능력도 아니고 실행 시점에서 발생하는 현상은 더더욱 아니다. 단지 JS의 특성에 따라 코드 실행 전 컴파일을 거치며 자연스럽게 귀결되는 전처리 과정이다. 호이스팅의 각 사례를 반복해서 읽고 비교해보며 변수와 함수를 더욱 적절히 선언 하도록 하자.

Reference

Javascript ‘Variable Hoisting’