JavaScript의 Hoisting이란 무엇이고 왜 그렇게 동작할까

Frontend

JavaScript를 공부하다 보면 한 번쯤은 이런 코드를 보게 됩니다.

console.log(message);
 
var message = 'hello';

그리고 "왜 에러가 안 나지?"라는 의문이 생깁니다.

조금 더 가면 이런 코드도 만나게 됩니다.

console.log(message);
 
let message = 'hello';

이번에는 반대로 에러가 납니다.

여기서 자연스럽게 질문이 따라옵니다.

  • var는 되고 let은 안 되는지
  • 왜 함수 선언문은 선언 전에 호출할 수 있는지
  • const는 선언 전에 접근하면 에러가 나는지
  • 자바스크립트 엔진이 코드를 위로 끌어올리는 건지

이 질문들의 중심에는 hoisting이 있습니다.

겉보기에는 "선언이 위로 끌어올려진다" 정도로 배우기 쉽습니다. 물론 빠르게 이해하는 데는 도움이 됩니다. 하지만 실무에서는 그것만으로는 부족합니다. 중요한 것은 자바스크립트가 코드를 실행하기 전에 어떤 식별자를 먼저 등록하고, 무엇을 즉시 초기화하고, 무엇은 나중까지 접근을 막는지를 이해하는 것입니다.

이 글에서는 JavaScripthoisting을 아래 흐름으로 정리해보겠습니다.

  1. hoisting은 무엇인지
  2. var, let, const는 왜 다르게 보이는지
  3. 함수 선언문은 왜 먼저 호출될 수 있는지
  4. Temporal Dead Zone은 무엇인지
  5. 실무에서 자주 하는 실수는 무엇인지

한눈에 보면

먼저 짧게 정리하면 이렇습니다.

  • hoisting은 식별자 선언이 실행 전에 미리 처리되는 것처럼 보이는 현상을 말합니다.
  • var는 선언이 먼저 등록되고 undefined로 초기화되어 선언 전 접근이 가능합니다.
  • let, const도 스코프에 등록되지만, 초기화 전에는 접근할 수 없습니다. 이 구간이 TDZ입니다.
  • 함수 선언문은 본문까지 함께 준비되어 선언 전에 호출할 수 있습니다.
  • 핵심은 "코드가 실제로 위로 이동한다"보다 실행 전에 식별자 처리 규칙이 다르다는 점입니다.

즉, hoisting은 문법 트릭이라기보다 실행 전에 식별자가 준비되는 방식의 차이를 설명하는 개념입니다.

이걸 더 정확히 이해하려면 보통 자바스크립트 실행을 두 감각으로 나눠보는 것이 도움이 됩니다.

  • 실행 전에 선언과 바인딩을 준비하는 단계
  • 실제 코드가 한 줄씩 실행되는 단계

실무에서는 이 둘을 엄밀한 엔진 내부 구현으로 외울 필요까지는 없지만, 적어도 "hoisting은 실행 전에 뭔가 준비되기 때문에 생기는 현상"이라는 감각은 잡아두는 편이 좋습니다.

hoisting은 정확히 무엇일까?

가장 단순하게 말하면 hoisting선언이 실제 작성 위치보다 먼저 처리되는 것처럼 보이는 현상입니다.

예를 들어:

console.log(value);
 
var value = 10;

이 코드는 에러 대신 undefined를 출력합니다.

많이 알려진 설명은 이런 식입니다.

var value;
console.log(value);
value = 10;

즉, 선언이 위로 끌어올려진 것처럼 보인다는 뜻입니다.

하지만 더 정확히 말하면 자바스크립트 엔진이 실행 전에 선언 정보를 먼저 수집하고 준비한다고 보는 편이 맞습니다.

중요한 것은 "줄 위치가 이동했다"가 아니라:

  • 어떤 선언이 먼저 등록되는지
  • 등록과 초기화가 같은 타이밍인지
  • 접근 가능 시점이 언제인지

가 서로 다르다는 점입니다.

즉, hoisting을 깊게 이해할수록 "선언이 올라간다"는 표현은 설명용 비유에 가깝고, 실제 핵심은 등록과 초기화와 실행이 서로 다른 타이밍을 가질 수 있다는 데 있다는 점이 더 중요해집니다.

실행 전에 무엇이 먼저 준비될까?

이 질문을 이해하면 var, let, const, 함수 선언문이 왜 다르게 보이는지 훨씬 쉬워집니다.

자바스크립트는 코드를 실행하기 전에 대체로 아래를 먼저 준비합니다.

  • 어떤 이름이 이 스코프 안에 있는지
  • 어떤 이름이 함수인지
  • 어떤 이름이 아직 초기화되지 않은 블록 스코프 바인딩인지

즉, 엔진은 코드를 읽으면서 "이 스코프에서 사용할 식별자 목록"을 먼저 잡아둡니다.

그 다음 실제 실행 단계에서:

  • 값이 대입되고
  • 표현식이 평가되고
  • 함수가 호출됩니다

이 관점을 가지고 보면:

  • varundefined가 보이는지
  • let, constTDZ에 걸리는지
  • 왜 함수 선언문은 선언 전에 호출할 수 있는지

가 전부 하나의 흐름으로 연결됩니다.

var는 선언 전에 읽히고 let, const는 안 될까?

이 부분이 hoisting에서 가장 많이 헷갈리는 지점입니다.

var의 hoisting

var는 선언이 먼저 처리되고, 동시에 undefined로 초기화됩니다.

console.log(count); // undefined
 
var count = 3;

즉, 실행 흐름을 감각적으로 보면:

var count;
console.log(count);
count = 3;

처럼 느껴집니다.

그래서 var는 선언 전에 접근해도 ReferenceError가 아니라 undefined가 나옵니다.

이 특성 때문에 var는 편해 보일 수 있지만, 실제로는 버그를 숨기기 쉽습니다.

  • 아직 값을 넣지 않았는데도 접근이 되고
  • 선언 위치가 아래에 있어도 코드가 일단 돌아가고
  • 의도하지 않은 undefined 흐름이 만들어질 수 있기 때문입니다

즉, var의 hoisting은 "유연함"처럼 보이지만, 실무에서는 예측 가능성을 떨어뜨리는 원인이 되기도 합니다.

조금 더 깊게 보면 var의 핵심 문제는 단순히 함수 스코프라는 점만이 아닙니다. 더 큰 문제는:

  • 선언과 초기화가 너무 느슨하게 느껴지고
  • 선언 전 접근이 조용히 허용되며
  • 버그가 즉시 드러나지 않을 수 있다는 점

입니다.

즉, var는 문법이 오래돼서 덜 선호되는 것이 아니라, 초기 실행 상태를 흐리게 만들기 쉬운 선언 방식이기 때문에 더 조심해서 보게 됩니다.

letconst의 hoisting

많은 사람이 let, const는 hoisting이 없다고 생각하지만, 더 정확히는 등록은 되지만 초기화 전에 접근할 수 없다고 보는 편이 맞습니다.

console.log(count);
 
let count = 3;

이 코드는 에러가 납니다.

const도 마찬가지입니다.

console.log(message);
 
const message = 'hello';

즉, letconst도 스코프에 이름이 준비되긴 하지만, 선언문이 실행되기 전까지는 접근이 막혀 있습니다.

이 구간이 바로 Temporal Dead Zone, 줄여서 TDZ입니다.

이 지점이 중요합니다. 많은 사람이 let, const는 "선언 자체가 늦게 처리된다"고 느끼지만, 사실은 오히려 반대에 가깝습니다. 이름은 이미 스코프에 들어와 있습니다. 다만 초기화 전 접근을 허용하지 않는 것입니다.

즉, let, const는 hoisting이 없어서 다르게 보이는 것이 아니라, hoisting 이후 접근 규칙이 더 엄격해서 다르게 보이는 것이라고 보는 편이 더 정확합니다.

TDZ는 무엇일까?

Temporal Dead Zone스코프에는 식별자가 존재하지만, 아직 초기화되지 않아 접근할 수 없는 구간입니다.

예를 들어:

{
  // TDZ 시작
  console.log(value); // error
  let value = 1;
  // TDZ 끝
}

여기서 value는 블록에 이미 속해 있지만, let value = 1이 실행되기 전까지는 접근할 수 없습니다.

즉, TDZ를 이해할 때 중요한 포인트는:

  • "변수가 아예 없는 것"이 아니라
  • "있지만 아직 사용할 수 없는 상태"

라는 점입니다.

이 차이는 기본기에서 정말 중요합니다. 왜냐하면:

  • "없다"라고 이해하면 이름 해석 자체를 잘못 보게 되고
  • "있지만 못 쓴다"라고 이해해야 실행 준비 상태를 더 정확히 볼 수 있기 때문입니다

즉, TDZ는 단순 예외 규칙이 아니라 let, const가 더 안전한 이유를 설명하는 핵심 개념입니다.

이 개념은 const에서도 똑같이 중요합니다.

{
  console.log(name); // error
  const name = 'Marco';
}

이 에러는 단순히 "아래에 있으니까"가 아니라, 초기화 전 접근 금지 규칙 때문에 생깁니다.

let, const는 이런 규칙을 가질까?

실무 감각으로 보면 이유는 꽤 명확합니다.

이 규칙이 없으면:

  • 선언 전에 접근하는 실수를 조기에 잡기 어렵고
  • 값이 아직 준비되지 않았는데도 코드가 돌아가 버릴 수 있고
  • var처럼 undefined가 숨어들어 버그를 늦게 드러낼 수 있습니다

즉, TDZ는 불편함을 주려는 규칙이 아니라 실수를 더 빨리 드러내기 위한 안전장치에 가깝습니다.

실무에서 이 차이는 꽤 큽니다. var였다면 undefined로 흘러가며 한참 뒤 다른 곳에서 버그가 터질 수 있는 코드가, let이나 const에서는 훨씬 앞에서 실패합니다.

즉, TDZ는 문법이 까다로운 것이 아니라 버그를 앞당겨 드러내는 비용에 더 가깝습니다.

함수 선언문은 왜 먼저 호출할 수 있을까?

이 부분도 자주 헷갈립니다.

printMessage();
 
function printMessage() {
  console.log('hello');
}

이 코드는 정상 동작합니다.

왜냐하면 함수 선언문은 실행 전에:

  • 이름이 등록되고
  • 함수 본문도 함께 준비되기 때문입니다

즉, 함수 선언문은 var처럼 undefined만 준비되는 것이 아니라, 호출 가능한 함수 자체가 미리 준비된다고 보는 편이 맞습니다.

그래서 선언 전에 호출할 수 있습니다.

이 차이가 중요한 이유는 함수 선언문이 단순 "변수에 함수 넣기"와는 다른 취급을 받기 때문입니다. 즉, 함수 선언문은 런타임에서 값이 대입되기를 기다리는 것이 아니라, 스코프 준비 단계에서부터 함수로 인식되는 선언에 가깝습니다.

함수 표현식은 왜 다르게 보일까?

예를 들어:

printMessage();
 
var printMessage = function () {
  console.log('hello');
};

이 코드는 에러가 납니다.

이유는 printMessage가 함수 선언문이 아니라 var 변수로 처리되기 때문입니다.

즉, 실행 전에는:

  • printMessage라는 이름은 등록되지만
  • 값은 undefined이고
  • 함수는 대입 시점에 들어갑니다

그래서 선언 전 호출 시점에는 함수가 아니라 undefined를 호출하려는 상태가 됩니다.

즉:

  • 함수 선언문: 함수 자체가 먼저 준비됨
  • 함수 표현식: 변수만 먼저 준비되고 함수 값은 나중에 대입됨

이라는 차이가 있습니다.

let이나 const로 만든 함수 표현식은 어떻게 될까?

예를 들어:

printMessage();
 
const printMessage = function () {
  console.log('hello');
};

이 코드는 TDZ 때문에 더 이른 시점에 에러가 납니다.

즉, 함수 표현식이라 해도 선언 방식이 무엇이냐에 따라 보이는 증상이 달라집니다.

  • var 함수 표현식: undefined 관련 에러 가능
  • let/const 함수 표현식: TDZ로 인한 ReferenceError 가능

즉, hoisting은 함수냐 변수냐만이 아니라 어떤 선언 방식이냐까지 같이 봐야 합니다.

여기서 실무적으로 얻는 포인트도 분명합니다.

  • 함수 선언문은 파일 상단 설계가 조금 느슨해도 먼저 호출될 수 있고
  • 함수 표현식은 선언 순서를 더 분명하게 강제하며
  • const 함수 표현식은 재할당 금지와 TDZ까지 함께 가져옵니다

즉, 선언 방식 선택은 단순 취향보다 초기 실행 규칙을 어떻게 가져갈 것인가의 선택이기도 합니다.

블록 안의 선언은 어떻게 보일까?

블록 스코프에서도 let, constTDZ는 그대로 적용됩니다.

if (true) {
  console.log(status); // error
  let status = 'ready';
}

즉, 블록 시작부터 선언문 전까지는 접근이 불가능합니다.

이 특성은 반복문에서도 중요합니다.

for (let i = 0; i < 3; i += 1) {
  console.log(i);
}

이런 코드는 블록 단위로 값이 더 안전하게 관리되기 때문에, var보다 의도한 동작을 얻기 쉽습니다.

hoisting을 이해할 때 자주 하는 오해

1. 코드가 진짜로 위로 이동한다고 생각한다

설명용 비유로는 괜찮지만, 너무 문자 그대로 받아들이면 오해가 생깁니다.

핵심은 코드 이동이 아니라 실행 전 선언 처리 규칙입니다.

2. let, const는 hoisting이 없다고 생각한다

정확히는 없다고 보기보다, 등록은 되지만 초기화 전 접근이 막힌다고 보는 편이 맞습니다.

3. 함수는 전부 선언 전에 호출할 수 있다고 생각한다

함수 선언문과 함수 표현식은 다르게 동작합니다.

즉, function foo() {}const foo = function () {} 는 hoisting 관점에서 같지 않습니다.

4. undefined가 나오면 정상이라고 생각한다

실제로는 많은 경우 버그 신호일 수 있습니다.

var hoisting은 문제를 "안 터지게" 만드는 것이 아니라, 늦게 드러나게 만들 수도 있습니다.

실무에서는 어디서 특히 중요할까?

1. 선언 전에 값 읽는 버그

var를 쓰면 조용히 undefined가 나오면서 버그가 숨어들 수 있습니다.

2. 함수 선언문과 표현식 혼동

특히 유틸 함수를 선언문으로 둘지, 함수 표현식으로 둘지에 따라 초기 접근 가능성이 달라집니다.

3. 블록 안 초기화 순서

if, for, switch 안에서 let, const를 쓸 때 선언 이전 접근은 바로 에러가 됩니다.

4. 리팩터링 중 선언 방식 변경

예전에 함수 선언문이던 코드를 const fn = () => {} 같은 방식으로 바꾸면, hoisting 동작도 같이 바뀝니다.

즉, 단순 문법 교체처럼 보여도 실행 시점 규칙이 달라질 수 있습니다.

자주 하는 실수

정리하면 아래 실수가 정말 자주 나옵니다.

  • varundefined를 정상 흐름처럼 받아들인다
  • let, const는 hoisting이 전혀 없다고 이해한다
  • 함수 선언문과 함수 표현식을 같은 규칙으로 본다
  • 선언 전에 접근하는 코드를 허용한 채 리팩터링한다
  • TDZ를 단순 문법 에러 정도로만 생각한다

즉, hoisting은 시험용 개념이 아니라 실행 시점의 초기 상태를 읽는 감각과 연결된 기본기입니다.

조금 더 깊게 보면 hoisting은 단독 개념이라기보다:

  • scope
  • 실행 컨텍스트
  • 선언과 초기화의 분리

와 함께 이해할 때 가장 자연스럽습니다.

즉, hoisting은 "특이한 문법 현상"이 아니라 자바스크립트가 실행 전에 코드를 준비하는 방식이 표면으로 드러난 결과라고 보는 편이 더 정확합니다.

실무 체크리스트

실제로 코드를 볼 때는 아래 질문으로 빠르게 점검하면 도움이 됩니다.

  1. 이 식별자는 선언 전에 접근되고 있지 않은가?
  2. 함수 선언문과 함수 표현식을 혼동하고 있지는 않은가?
  3. var 때문에 undefined가 조용히 흘러들어오고 있지는 않은가?
  4. let, const 선언 이전 접근이 TDZ를 만들고 있지는 않은가?
  5. 리팩터링으로 선언 방식이 바뀌면서 초기 실행 순서도 바뀌지 않았는가?

이 기준으로 보면 hoisting은 단순 암기 대상이 아니라, 코드가 실행되기 직전 어떤 준비 상태에 놓이는지 이해하는 도구가 됩니다.

결론

JavaScripthoisting은 선언이 위로 이동하는 마법이라기보다, 실행 전에 식별자가 어떤 상태로 준비되는지 설명하는 개념입니다.

짧게 정리하면:

  • var는 선언과 초기화가 먼저 보여 undefined가 나올 수 있고
  • let, constTDZ 때문에 초기화 전 접근이 막히며
  • 함수 선언문은 함수 자체가 먼저 준비되고
  • 함수 표현식은 선언 방식에 따라 전혀 다르게 보일 수 있고
  • 결국 핵심은 선언, 초기화, 실행이 같은 타이밍이 아니라는 점입니다

결국 hoisting을 이해하면 선언 순서, 초기값, 함수 호출 가능 시점, TDZ 에러를 훨씬 더 자연스럽게 설명할 수 있게 됩니다.