- Published on
호이스팅은 정말 선언을 위로 끌어올리는 걸까?
- Authors

- Name
- GW
개요
JavaScript를 공부하다 보면 호이스팅을 보통 이렇게 설명합니다.
변수와 함수의 선언이 코드 위로 끌어올려지는 현상
실제 실행 결과를 보면 정말 코드가 위로 이동한 것처럼 보이기도 합니다.
console.log(message);
var message = '안녕하세요';
선언보다 먼저 message에 접근했지만 선언되지 않았다는 오류 대신 undefined가 출력됩니다.
그렇다면 JavaScript 엔진이 아래에 있는 var message를 찾아 실제로 코드 위로 옮긴 걸까요?
결론부터 말하면 코드가 물리적으로 위로 이동하는 것은 아닙니다.
JavaScript 엔진이 코드를 실행하기 전에 선언을 먼저 확인하고, 식별자를 사용할 수 있도록 환경을 준비하기 때문에 위로 끌어올려진 것처럼 보이는 것입니다.
이번 글에서는 호이스팅이 무엇인지부터 시작해 다음 내용을 살펴보겠습니다.
- 선언이 코드 실행 전에 어떻게 처리되는지
var,let,const의 실행 결과가 다른 이유- 함수 선언문과 함수 표현식의 차이
- 호이스팅을 코드를 위로 옮기는 현상으로만 이해하면 안 되는 이유
호이스팅이란 무엇일까?
호이스팅은 변수와 함수의 선언이 코드 실행 전에 먼저 처리되어, 선언문보다 앞에서도 해당 식별자가 존재하는 것처럼 동작하는 현상입니다.
여기서 중요한 단어는 선언입니다.
JavaScript 엔진은 코드를 위에서 아래로 한 줄씩 무작정 실행하지 않습니다.
본격적으로 코드를 실행하기 전에 현재 스코프에 어떤 변수와 함수가 선언되어 있는지 먼저 확인하고, 이를 관리할 환경을 준비합니다.
그 결과 선언문보다 앞에서 식별자에 접근했을 때도 JavaScript는 해당 식별자의 존재를 알고 있을 수 있습니다.
여기서 말하는 식별자(Identifier) 는 변수, 함수, 클래스 등을 구분하기 위해 사용하는 이름을 의미합니다.
console.log(message);
var message = '안녕하세요';
실행 결과는 다음과 같습니다.
undefined
만약 JavaScript가 첫 번째 줄을 실행할 때까지 message의 존재를 전혀 몰랐다면 아래의 오류가 발생해야 합니다.
ReferenceError: message is not defined
하지만 JavaScript 엔진은 코드를 실행하기 전에 var message 선언을 이미 확인했습니다.
따라서 message라는 식별자는 존재하지만, 아직 할당문이 실행되지 않아 값은 undefined인 상태입니다.
선언과 할당은 다르게 처리된다
호이스팅을 이해하려면 선언과 할당을 구분해야 합니다.
var message = '안녕하세요';
한 줄로 작성되어 있지만 내부적으로는 다음 두 작업을 포함합니다.
var message;
message = '안녕하세요';
변수 선언은 코드 실행을 위한 환경을 준비하는 과정에서 먼저 처리됩니다.
반면 값을 넣는 할당은 실제 코드가 해당 줄에 도달했을 때 실행됩니다.
따라서 다음 코드에서,
console.log(message);
var message = '안녕하세요';
console.log(message);
첫 번째 출력 시점에는 선언만 처리되어 있습니다.
undefined
안녕하세요
실행 흐름을 단순화하면 다음과 같습니다.
코드 실행 전
message 식별자 생성
초기값: undefined
코드 실행
console.log(message) → undefined
message = '안녕하세요'
console.log(message) → 안녕하세요
설명을 위해 다음처럼 표현하기도 합니다.
var message;
console.log(message);
message = '안녕하세요';
console.log(message);
하지만 이것은 실행 결과를 쉽게 이해하기 위한 표현일 뿐, JavaScript 엔진이 원본 코드를 실제로 다시 작성해 선언문을 파일 위쪽으로 옮기는 것은 아닙니다.
위로 올라가는 것은 코드가 아니라, 선언에 대한 처리 시점입니다.
호이스팅은 어떻게 동작할까?
JavaScript가 코드를 실행할 때는 실행에 필요한 환경이 만들어집니다. 이 과정에서 현재 스코프에 선언된 식별자들이 먼저 등록됩니다.
그다음 실제 코드가 위에서 아래로 실행됩니다.
이를 크게 나누면 다음과 같이 이해할 수 있습니다.
1. 코드 실행에 필요한 환경 준비
변수와 함수 선언 확인
2. 코드 실행
할당문과 함수 호출 등을 위에서 아래로 실행
예를 들어 다음 코드가 있다고 해보겠습니다.
console.log(count);
var count = 10;
코드 실행 전에는 count 선언이 먼저 확인되고 undefined로 초기화됩니다.
count: undefined
그다음 첫 번째 줄이 실행되므로 undefined가 출력됩니다.
이후 var count = 10이 있는 줄에 도달하면 할당이 실행됩니다.
count: undefined → 10
호이스팅은 이러한 준비 과정 때문에 나타나는 결과입니다.
다만 모든 선언이 똑같은 상태로 준비되는 것은 아니고, var, let, const, 함수 선언문은 각각 다른 방식으로 처리됩니다.
var는 undefined로 초기화된다
var로 선언한 변수는 선언문에 도달하기 전에도 접근할 수 있습니다.
console.log(count);
var count = 10;
undefined
코드 실행 전에 count 식별자가 생성될 뿐 아니라 undefined로 초기화되기 때문입니다.
따라서 var 변수는 다음과 같은 상태로 코드 실행을 시작합니다.
count: undefined
이러한 동작은 오류를 발생시키지 않는다는 점에서는 편해 보이지만, 예상하지 못한 버그를 만들기도 합니다.
const printPrice = () => {
console.log(price);
if (true) {
var price = 10_000;
}
};
printPrice();
실행 결과는 오류가 아니라 undefined입니다.
undefined
var는 블록 스코프가 아니라 함수 스코프를 따르기 때문에 if 블록 안의 선언도 printPrice 함수 전체에서 존재합니다. 다만 할당 전에 접근했으므로 undefined가 출력됩니다.
개발자가 변수를 선언하지 않았다고 착각하거나 값이 이미 들어 있다고 생각하면 문제의 원인을 찾기 어려워질 수 있습니다.
let과 const도 호이스팅될까?
let과 const를 선언 전에 사용하면 오류가 발생합니다.
console.log(count);
let count = 10;
ReferenceError: Cannot access 'count' before initialization
이 결과만 보면 let과 const는 호이스팅되지 않는 것처럼 보입니다.
하지만 let과 const의 선언도 코드 실행 전에 확인됩니다. 다만 var와 달리 선언문에 도달하기 전까지 값이 초기화되지 않습니다.
즉, 다음과 같은 차이가 있습니다.
var
└─ 식별자 생성
└─ undefined로 초기화
let, const
└─ 식별자 생성
└─ 아직 초기화되지 않음
스코프가 시작된 지점부터 선언문을 만나 초기화되기 전까지의 구간을 TDZ(Temporal Dead Zone), 일시적 사각지대라고 합니다.
const printCount = () => {
// count의 일시적 사각지대 시작
console.log(count);
let count = 10;
// count의 일시적 사각지대 종료
};
이 구간에서 count는 존재하지 않는 변수가 아닙니다.
식별자는 이미 등록되어 있지만 아직 사용할 수 없는 상태입니다.
이 차이는 바깥 스코프에 같은 이름의 변수가 있을 때 더 분명하게 드러납니다.
const count = 1;
const printCount = () => {
console.log(count);
let count = 10;
};
printCount();
만약 내부의 let count가 코드 실행 전에 전혀 인식되지 않았다면 바깥의 count를 찾아 1을 출력해야 합니다.
하지만 실제로는 오류가 발생합니다.
ReferenceError: Cannot access 'count' before initialization
printCount 내부의 count 선언이 이미 현재 스코프에 등록되어 바깥의 count를 가리고 있기 때문입니다.
다만 선언문에 도달하기 전이라 아직 초기화되지 않았으므로 접근할 수 없습니다.
따라서 다음과 같이 이해하는 것이 정확합니다.
let과const도 호이스팅되지만, 선언문에 도달하기 전에는 일시적 사각지대에 있어 접근할 수 없다.
일시적 사각지대는 왜 필요할까?
var처럼 선언 전에 접근했을 때 undefined를 반환하게 만들 수도 있었을 텐데, let과 const는 왜 오류를 발생시킬까요?
그 이유는 변수가 초기화되기 전에 사용되는 실수를 빠르게 발견할 수 있도록 하기 위해서입니다.
const calculatePrice = () => {
console.log(price);
let price = 10_000;
};
여기에서 undefined가 출력되면 코드는 계속 실행될 수 있습니다.
이후 undefined를 이용한 계산까지 이어지면 문제의 원인을 다른 곳에서 찾게 될 수도 있습니다.
반면 let과 const는 변수를 잘못된 시점에 사용하자마자 오류를 발생시킵니다.
ReferenceError
일시적 사각지대는 불편한 제약처럼 보일 수 있지만, 초기화되지 않은 변수를 조용히 사용하는 것을 막아 코드의 예측 가능성을 높여줍니다.
이 때문에 특별한 이유가 없다면 var보다 let과 const를 사용하는 것이 일반적입니다.
함수 선언문은 함수 전체가 준비된다
함수 선언문은 변수와 조금 다르게 동작합니다.
greet();
function greet() {
console.log('안녕하세요');
}
함수 호출이 선언보다 앞에 있지만 정상적으로 실행됩니다.
안녕하세요
함수 선언문은 코드 실행 전에 함수 이름뿐만 아니라 함수 객체까지 함께 초기화됩니다.
따라서 실제 실행이 시작되는 시점에는 이미 greet을 호출할 수 있습니다.
여기에서는 화살표 함수가 아니라 함수 선언문을 사용했습니다.
함수 선언문의 호이스팅 동작 자체를 비교해야 하기 때문입니다.
함수 선언문의 준비 상태를 단순화하면 다음과 같습니다.
코드 실행 전
└─ greet: 실행 가능한 함수
그래서 선언문보다 먼저 호출할 수 있습니다.
함수 표현식은 변수의 규칙을 따른다
함수를 변수에 할당하는 함수 표현식은 함수 선언문과 다릅니다.
greet();
var greet = () => {
console.log('안녕하세요');
};
이 코드는 다음 오류를 발생시킵니다.
TypeError: greet is not a function
var greet 선언은 코드 실행 전에 처리되어 undefined로 초기화됩니다.
하지만 화살표 함수를 할당하는 작업은 해당 줄에 도달했을 때 실행됩니다.
첫 번째 줄이 실행되는 시점의 상태는 다음과 같습니다.
greet: undefined
따라서 존재하지 않는 식별자 오류가 아니라 undefined를 함수처럼 호출했다는 오류가 발생합니다.
const를 사용하면 결과가 달라집니다.
greet();
const greet = () => {
console.log('안녕하세요');
};
ReferenceError: Cannot access 'greet' before initialization
const greet는 코드 실행 전에 등록되지만 선언문에 도달하기 전까지 초기화되지 않기 때문입니다.
정리하면 함수의 작성 방식에 따라 선언 전에 호출할 수 있는지가 달라집니다.
| 작성 방식 | 선언 전 접근 결과 |
|---|---|
| 함수 선언문 | 정상 호출 |
var 함수 표현식 | undefined를 호출하여 TypeError |
let, const 함수 표현식 | 일시적 사각지대로 ReferenceError |
화살표 함수 자체가 호이스팅되지 않는 것이 아닙니다.
화살표 함수는 대부분 변수에 할당해서 사용하므로 해당 변수 선언 방식의 호이스팅 규칙을 따르는 것입니다.
var·let·const·함수 선언문의 차이
각 선언이 코드 실행 전에 어떻게 준비되는지 정리하면 다음과 같습니다.
| 선언 방식 | 코드 실행 전 식별자 등록 | 선언 전 초기화 | 선언 전 접근 |
|---|---|---|---|
var | 등록됨 | undefined | undefined |
let | 등록됨 | 초기화되지 않음 | ReferenceError |
const | 등록됨 | 초기화되지 않음 | ReferenceError |
| 함수 선언문 | 등록됨 | 함수 객체 | 호출 가능 |
이 표에서 중요한 점은 네 방식 모두 코드 실행 전에 선언이 확인된다는 것입니다.
차이는 초기화되는 시점과 초기값에 있습니다.
var는 먼저undefined로 초기화됩니다.let과const는 선언문에 도달할 때까지 초기화되지 않습니다.- 함수 선언문은 실행 가능한 함수 객체로 초기화됩니다.
선언 전에 사용해도 되는 코드를 작성해야 할까?
함수 선언문은 선언보다 먼저 호출할 수 있습니다.
startApplication();
function startApplication() {
console.log('애플리케이션을 시작합니다.');
}
문법적으로는 문제가 없습니다.
함수 선언을 파일 아래쪽에 배치하고 상단에서 전체 실행 흐름을 먼저 보여주는 스타일도 사용할 수 있습니다.
하지만 변수까지 호이스팅에 의존해 선언 전에 사용하는 것은 피하는 것이 좋습니다.
console.log(userName);
var userName = '철수';
이 코드는 오류 없이 실행되지만 첫 번째 결과는 의도한 이름이 아니라 undefined입니다.
호이스팅을 알고 있다는 이유로 이러한 코드를 작성할 필요는 없습니다.
선언과 초기화를 사용 지점보다 먼저 배치하면 실행 순서를 훨씬 쉽게 파악할 수 있습니다.
const userName = '철수';
console.log(userName);
호이스팅은 적극적으로 활용해야 하는 특별한 문법이라기보다, JavaScript 코드가 예상과 다르게 동작할 때 그 원인을 이해하기 위해 알아야 하는 실행 원리에 가깝습니다.
자주 하는 오해
선언과 할당이 모두 호이스팅된다?
아닙니다.
선언은 코드 실행 전에 처리되지만, 할당은 실제 코드가 해당 줄에 도달했을 때 이루어집니다.
console.log(count);
var count = 10;
이 코드에서 먼저 처리되는 것은 var count 선언이지 count = 10이라는 할당이 아닙니다.
화살표 함수는 호이스팅되지 않는다?
화살표 함수는 보통 변수에 할당하므로 해당 변수의 규칙을 따릅니다.
const greet = () => {};
여기에서 greet은 const로 선언되었기 때문에 선언문 이전에는 일시적 사각지대에 있습니다.
함수 선언문처럼 함수 객체 전체가 미리 초기화되는 것은 아닙니다.
정리
호이스팅은 변수와 함수의 선언문이 실제 코드 위쪽으로 이동하는 현상이 아닙니다.
JavaScript 엔진이 코드를 실행하기 전에 현재 스코프의 선언을 먼저 확인하고, 각 선언 방식에 맞는 상태로 식별자를 준비하기 때문에 나타나는 동작입니다.
핵심을 정리하면 다음과 같습니다.
- JavaScript는 실제 코드를 실행하기 전에 변수와 함수 선언을 먼저 확인합니다.
- 선언이 처리되는 시점과 값을 할당하는 시점은 서로 다릅니다.
var는 코드 실행 전에undefined로 초기화됩니다.let과const는 호이스팅되지만 선언문 전까지 일시적 사각지대(TDZ)에 있습니다.- 함수 선언문은 함수 객체까지 미리 초기화되어 선언 전에 호출할 수 있습니다.
- 함수 표현식은 함수를 저장하는 변수의 선언 방식에 따라 동작합니다.
- 호이스팅은 코드가 이동하는 것이 아니라 선언이 먼저 처리되면서 나타나는 결과입니다.
따라서 호이스팅을 이해하는 문장은 다음과 같습니다.
호이스팅은 선언이 위로 이동하는 현상이 아니라, JavaScript가 코드를 실행하기 전에 선언을 먼저 처리하면서 나타나는 동작입니다.