Skip to content

호이스팅은 왜 일어나는가 ​

책을 읽다가 만난 이 코드부터 확인해보자. 출력이 뭘까?

javascript
console.log(score); // ?

var score = 80;

console.log(score); // ?

처음 이 코드를 봤을 때 나는 첫 줄에서 에러가 날 거라고 생각했다. score는 아직 선언되지 않았으니까. 그런데 실제로 실행하면 에러 없이 undefined와 80이 출력된다. 한술 더 떠서 이런 코드도 동작한다.

javascript
console.log(score); // undefined

score = 80; // 할당이 선언보다 위에 있다
var score;

console.log(score); // 80

"코드는 위에서 아래로 실행된다"는 직관이 여기서 깨진다. 선언문이 제일 아래에 있는데 왜 에러가 안 날까?

문제는 "언제 선언이 처리되는가" ​

이 문제를 틀리는 이유는 자바스크립트 엔진이 코드를 한 번에 처리하지 않는다는 걸 모르기 때문이다. 엔진은 코드를 실행하기 전에 먼저 한 바퀴 훑는다. 이 과정은 두 단계로 나눠 이해하면 된다.

  1. 평가 단계 — 코드를 실행하기 전에 선언문(변수 선언, 함수 선언)만 먼저 찾아서 실행 컨텍스트에 등록한다. var로 선언한 변수는 이때 undefined로 초기화까지 된다.
  2. 실행 단계 — 선언문을 제외한 코드가 위에서 아래로 한 줄씩 실행된다. 할당문 score = 80은 이때 실행된다.

그래서 console.log(score)가 선언문보다 위에 있어도, 실행 단계가 시작되는 시점엔 이미 score가 등록되어 있다. 에러가 아니라 undefined가 나오는 이유다.

① 평가 단계 — 선언문만 먼저 처리console.log(score);var score = 80;실행 컨텍스트score: undefined실행 전에 score가 이미 undefined로 등록된다② 실행 단계 — 위에서 아래로 실행console.log(score);var score = 80;실행 컨텍스트score: undefined → 801행: 등록돼 있던 undefined를 출력2행: 이제서야 80이 할당된다

여기서 오해하기 쉬운 게 하나 있다. 호이스팅(hoisting)이라는 이름 때문에 선언문이 코드 최상단으로 "끌어올려져서 이동한다"고 상상하기 쉬운데, 사실 코드는 어디로도 이동하지 않는다. 선언이 실행보다 먼저 처리되는 순서의 문제일 뿐이고, 그 결과가 마치 끌어올려진 것처럼 보이는 것이다.

그럼 함수도 호이스팅되나? ​

된다. 그런데 어떻게 정의했느냐에 따라 결과가 완전히 다르다.

javascript
console.log(add(2, 5)); // 7
console.log(sub(2, 5)); // TypeError: sub is not a function

// 함수 선언문
function add(x, y) {
  return x + y;
}

// 함수 표현식
var sub = function (x, y) {
  return x - y;
};
  • 함수 선언문은 평가 단계에서 함수 객체까지 통째로 등록된다. 그래서 선언문보다 위에서 호출해도 동작한다.
  • 함수 표현식은 함수가 아니라 변수 선언이다. var sub만 호이스팅되어 undefined로 초기화되고, 함수 객체 할당은 실행 단계에서 그 줄에 도달해야 일어난다. 그래서 undefined를 호출하려다 TypeError가 난다.

에러 종류를 구분하는 것도 중요하다. sub가 아예 없어서 나는 ReferenceError가 아니라, undefined라는 값은 있는데 함수가 아니라서 나는 TypeError다. 이 차이를 설명할 수 있으면 호이스팅을 정확히 이해한 것이다.

왜 이렇게 설계했을까? ​

함수 선언문 호이스팅은 나름의 쓸모가 있다. 함수를 정의 순서와 상관없이 호출할 수 있으니, 핵심 로직을 파일 상단에 두고 세부 구현 함수를 아래로 내리는 식으로 코드를 배치할 수 있다.

반면 var의 호이스팅은 의도된 기능이라기보다 이 2단계 처리 방식의 부작용에 가깝다. 선언 전에 변수를 참조했는데 에러 대신 undefined가 조용히 나오는 건 버그를 숨기는 동작이다. 개인적으로 이건 언어가 "실수를 허용해 주는" 게 아니라 "실수를 늦게 발견하게 만드는" 쪽이라고 생각한다.

그래서 ES6의 let/const는 같은 호이스팅을 겪으면서도 다르게 동작하도록 설계됐다. 선언 전에 참조하면 undefined가 아니라 ReferenceError를 던진다.


호이스팅은 코드가 위로 이동하는 현상이 아니라, 선언이 실행보다 먼저 처리되는 평가 단계의 흔적이다.

그런데 방금 "let도 같은 호이스팅을 겪는다"고 했다. 근데 왜 let은 다르게 동작할까? → var와 let/const는 뭐가 다른가