JavaScript 호이스팅 완벽 가이드 - 변수와 함수는 어떻게 끌어올려질까?
함수를 선언하기 전에 호출했는데도 작동하는 것을 본 적 있나요? 아니면 var로 선언한 변수가 undefined를 반환하는데, let으로 선언한 변수는 ReferenceError를 던지는 이유가 궁금하신가요?
저도 처음 JavaScript를 배울 때 이런 경험을 했습니다. 코드를 위에서 아래로 읽는데, 선언도 하기 전에 함수를 호출하면 당연히 에러가 날 것 같았죠. 그런데 신기하게도 작동했습니다. 심지어 때로는 undefined가 나오고, 때로는 ReferenceError가 나오는 이 불가사의한 동작의 비밀이 바로 호이스팅(Hoisting)입니다.
이 가이드에서는 호이스팅의 정체부터 실전에서 주의해야 할 함정까지, MDN 공식 문서를 기반으로 깊이 있게 설명합니다.
왜 호이스팅을 알아야 할까요?
1. 예상치 못한 버그 방지
호이스팅을 모르면 변수가 undefined인 이유를 찾느라 시간을 낭비하게 됩니다.
2. 코드 실행 순서 이해
JavaScript 엔진이 코드를 어떻게 해석하는지 이해하면, 더 나은 코드를 작성할 수 있습니다.
3. let과 const의 동작 방식 파악
왜 let과 const는 var와 다르게 동작하는지 이해하게 됩니다.
4. 면접 질문 대비
“호이스팅이 무엇인가요?”는 JavaScript 면접에서 단골 질문입니다.
호이스팅이란?
개념
호이스팅(Hoisting)은 JavaScript 인터프리터가 코드를 실행하기 전에, 변수와 함수의 선언을 해당 스코프의 최상단으로 “끌어올리는 것처럼 보이는” 동작을 말합니다.
시각적으로 표현하면:
┌─────────────────────────────────────┐
│ 우리가 작성한 코드 │
├─────────────────────────────────────┤
│ console.log(x); // undefined │
│ var x = 5; │
└─────────────────────────────────────┘
↓ JavaScript 엔진이 해석
┌─────────────────────────────────────┐
│ 엔진이 실제로 실행하는 방식 │
├─────────────────────────────────────┤
│ var x; // 선언이 위로! │
│ console.log(x); // undefined │
│ x = 5; // 할당은 제자리 │
└─────────────────────────────────────┘
주의: 호이스팅은 ECMAScript 명세서에 공식적으로 정의된 용어가 아닙니다. 하지만 코드의 실행 결과를 설명하는 데 매우 유용한 개념입니다.
호이스팅의 4가지 타입
MDN에 따르면, 호이스팅은 크게 4가지 타입으로 나뉩니다.
Type 1: 값 호이스팅 (Value Hoisting)
선언보다 먼저 변수의 값을 사용할 수 있습니다.
적용 대상: function, function*, async function, async function*
// ✅ 작동함
greet(); // "Hello!"
function greet() {
console.log("Hello!");
}
Type 2: 선언 호이스팅 (Declaration Hoisting)
선언보다 먼저 참조할 수 있지만, 값은 undefined입니다.
적용 대상: var
console.log(x); // undefined (에러가 아님!)
var x = 5;
console.log(x); // 5
Type 3: 스코프 동작 (Scope Behavior)
선언이 스코프 전체에 영향을 미치지만, Temporal Dead Zone(TDZ) 때문에 선언 전에 접근하면 ReferenceError가 발생합니다.
적용 대상: let, const, class
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
Type 4: 부수 효과 호이스팅 (Side Effect Hoisting)
선언의 부수 효과가 코드 실행 전에 발생합니다.
적용 대상: import
// import는 모듈 최상단에서 가장 먼저 실행됨
console.log(myFunction); // function
import { myFunction } from './module.js';
선언 타입별 호이스팅 비교
| 선언 타입 | 호이스팅 타입 | 선언 전 접근 시 | 초기값 |
|---|---|---|---|
var |
Type 2 | undefined |
undefined |
let |
Type 3 | ReferenceError |
TDZ |
const |
Type 3 | ReferenceError |
TDZ |
function |
Type 1 | 정상 작동 | 함수 자체 |
class |
Type 3 | ReferenceError |
TDZ |
1. var의 호이스팅
1-1. 기본 동작
console.log(name); // undefined
var name = "John";
console.log(name); // "John"
실제 실행 방식:
var name; // 선언이 맨 위로
console.log(name); // undefined
name = "John"; // 할당은 원래 위치
console.log(name); // "John"
1-2. 함수 스코프
var는 함수 스코프를 가집니다.
function test() {
console.log(x); // undefined
var x = 10;
console.log(x); // 10
}
test();
console.log(x); // ReferenceError: x is not defined
블록 스코프는 무시됨:
if (true) {
var x = 5;
}
console.log(x); // 5 (블록 밖에서도 접근 가능!)
1-3. 흔한 실수 - 반복문
// ❌ 잘못된 예
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 3, 3, 3 (예상과 다름!)
}, 100);
}
// ✅ 올바른 예 (let 사용)
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 0, 1, 2
}, 100);
}
왜 이런 일이 발생할까요?
var는 함수 스코프이므로, 반복문이 끝난 후 i는 3이 됩니다. 모든 setTimeout 콜백은 같은 i를 참조하므로 3을 출력합니다.
2. let과 const의 호이스팅
2-1. Temporal Dead Zone (TDZ)
let과 const도 호이스팅되지만, TDZ 때문에 선언 전에 접근할 수 없습니다.
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
TDZ가 뭔가요?
변수 선언부터 초기화까지의 구간을 Temporal Dead Zone이라고 합니다. 이 구간에서는 변수에 접근할 수 없습니다.
┌─────────────────────────────────┐
│ // TDZ 시작 │
│ console.log(x); // ReferenceError
│ // TDZ 구간 │
│ let x = 5; // TDZ 종료 │
│ console.log(x); // 5 │
└─────────────────────────────────┘
2-2. 블록 스코프
let과 const는 블록 스코프를 가집니다.
if (true) {
let x = 5;
const y = 10;
console.log(x, y); // 5, 10
}
console.log(x); // ReferenceError
console.log(y); // ReferenceError
2-3. 스코프를 오염시키는 호이스팅
const x = 1;
{
console.log(x); // ReferenceError (왜 1이 아닐까?)
const x = 2;
}
왜 ReferenceError가 발생할까요?
블록 내부의 const x = 2 선언이 호이스팅되어 블록 전체 스코프에 영향을 미칩니다. 따라서 바깥의 x = 1에 접근할 수 없고, 내부 x는 아직 초기화 전이므로 ReferenceError가 발생합니다.
┌───────────────────────────────────┐
│ const x = 1; │
│ { │
│ // 이 블록 전체가 내부 x의 스코프 │
│ // TDZ 시작 │
│ console.log(x); // ReferenceError
│ const x = 2; // TDZ 종료 │
│ } │
└───────────────────────────────────┘
3. 함수 호이스팅
3-1. 함수 선언문 (Function Declaration)
완전히 호이스팅됨 - 선언 전에 호출 가능합니다.
greet(); // "Hello!"
function greet() {
console.log("Hello!");
}
실제 실행 방식:
// 함수 전체가 맨 위로
function greet() {
console.log("Hello!");
}
greet(); // "Hello!"
3-2. 함수 표현식 (Function Expression)
변수에 할당된 함수는 변수의 호이스팅 규칙을 따릅니다.
// ❌ var로 선언된 함수 표현식
greet(); // TypeError: greet is not a function
var greet = function() {
console.log("Hello!");
};
왜 TypeError일까요?
var greet는 호이스팅되어 undefined가 됩니다. undefined()를 호출하려고 하면 TypeError가 발생합니다.
// 실제 실행 방식
var greet; // undefined
greet(); // TypeError: greet is not a function
greet = function() {
console.log("Hello!");
};
// ❌ let/const로 선언된 함수 표현식
greet(); // ReferenceError: Cannot access 'greet' before initialization
const greet = function() {
console.log("Hello!");
};
3-3. 비교 정리
// ✅ 함수 선언문 - 작동함
hello();
function hello() {
console.log("Hello!");
}
// ❌ 함수 표현식 (var) - TypeError
hi();
var hi = function() {
console.log("Hi!");
};
// ❌ 함수 표현식 (const) - ReferenceError
hey();
const hey = function() {
console.log("Hey!");
};
// ❌ 화살표 함수 - ReferenceError
greet();
const greet = () => {
console.log("Greet!");
};
4. class 호이스팅
4-1. 클래스도 TDZ의 영향을 받음
const p = new Person(); // ReferenceError: Cannot access 'Person' before initialization
class Person {
constructor(name) {
this.name = name;
}
}
4-2. 클래스 표현식
const p = new Person(); // ReferenceError
const Person = class {
constructor(name) {
this.name = name;
}
};
5. 실전 예제
예제 1: var vs let 비교
// var 사용
function varTest() {
console.log(x); // undefined
if (true) {
var x = 10;
}
console.log(x); // 10
}
// let 사용
function letTest() {
console.log(x); // ReferenceError
if (true) {
let x = 10;
}
console.log(x); // ReferenceError
}
예제 2: 함수와 변수의 호이스팅 순서
console.log(typeof foo); // "function"
var foo = "variable";
function foo() {
return "function";
}
console.log(typeof foo); // "string"
왜 이렇게 동작할까요?
실제 실행 순서:
// 1. 함수 선언이 먼저 호이스팅됨
function foo() {
return "function";
}
// 2. 변수 선언이 호이스팅됨 (하지만 함수가 이미 있으므로 무시됨)
var foo;
// 3. 코드 실행
console.log(typeof foo); // "function"
foo = "variable"; // 할당
console.log(typeof foo); // "string"
핵심: 함수 선언이 변수 선언보다 우선순위가 높습니다.
예제 3: 중첩된 스코프
let x = "outer";
function test() {
console.log(x); // ReferenceError (왜 "outer"가 아닐까?)
let x = "inner";
}
test();
이유: 함수 내부의 let x = "inner" 선언이 함수 전체 스코프에 호이스팅되어, 바깥의 x를 가립니다. 하지만 아직 초기화 전이므로 TDZ에 걸려 ReferenceError가 발생합니다.
예제 4: 실무에서 흔한 실수
// ❌ 잘못된 예: 반복문에서 var 사용
var buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log('Button ' + i + ' clicked'); // 항상 마지막 i 값!
});
}
// ✅ 올바른 예 1: let 사용
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log('Button ' + i + ' clicked'); // 올바른 i 값
});
}
// ✅ 올바른 예 2: 클로저 활용 (var를 써야 한다면)
for (var i = 0; i < buttons.length; i++) {
(function(index) {
buttons[index].addEventListener('click', function() {
console.log('Button ' + index + ' clicked');
});
})(i);
}
6. 흔한 실수와 해결법
실수 1: var를 블록 스코프처럼 사용
// ❌ 잘못된 예
if (true) {
var secret = "password";
}
console.log(secret); // "password" (의도치 않게 노출!)
// ✅ 올바른 예
if (true) {
let secret = "password";
}
console.log(secret); // ReferenceError (의도한 대로 숨겨짐)
실수 2: 함수 표현식을 선언 전에 호출
// ❌ 잘못된 예
doSomething(); // TypeError
var doSomething = function() {
console.log("Doing something");
};
// ✅ 올바른 예 1: 함수 선언문 사용
doSomething(); // "Doing something"
function doSomething() {
console.log("Doing something");
}
// ✅ 올바른 예 2: 순서 바꾸기
const doSomething = function() {
console.log("Doing something");
};
doSomething(); // "Doing something"
실수 3: TDZ를 이해하지 못한 채 let/const 사용
// ❌ 잘못된 예
function calculateTotal(items) {
console.log(total); // ReferenceError
let total = 0;
for (let item of items) {
total += item.price;
}
return total;
}
// ✅ 올바른 예
function calculateTotal(items) {
let total = 0; // 선언을 먼저
console.log(total); // 0
for (let item of items) {
total += item.price;
}
return total;
}
7. 디버깅 팁
팁 1: typeof를 사용한 안전한 체크
// ❌ 위험한 방법
if (myVar) {
console.log(myVar);
}
// ✅ 안전한 방법 (선언되지 않은 변수도 체크 가능)
if (typeof myVar !== 'undefined') {
console.log(myVar);
}
팁 2: strict mode 사용
'use strict';
x = 5; // ReferenceError: x is not defined
strict mode에서는 선언 없이 변수를 사용할 수 없어서, 호이스팅 관련 실수를 방지할 수 있습니다.
팁 3: ESLint 설정
.eslintrc.json:
{
"rules": {
"no-use-before-define": ["error", {
"functions": false,
"classes": true,
"variables": true
}]
}
}
8. 베스트 프랙티스
1. var 대신 let/const 사용
// ❌ 피해야 할 방식
var x = 5;
// ✅ 권장하는 방식
const x = 5; // 재할당이 필요 없으면 const
let y = 10; // 재할당이 필요하면 let
2. 변수는 사용하기 직전에 선언
// ❌ 좋지 않은 방식
function processData(data) {
let result;
let temp;
let index;
// ... 많은 코드 ...
result = doSomething(data);
return result;
}
// ✅ 좋은 방식
function processData(data) {
// ... 다른 코드 ...
const result = doSomething(data);
return result;
}
3. 함수 선언문 사용 (순서 무관하게 사용 가능)
// ✅ 가독성 좋은 방식: 메인 로직을 위에
main();
function main() {
const data = fetchData();
const processed = processData(data);
displayResults(processed);
}
function fetchData() {
// ...
}
function processData(data) {
// ...
}
function displayResults(results) {
// ...
}
4. 클래스는 항상 선언 후 사용
// ✅ 올바른 방식
class Person {
constructor(name) {
this.name = name;
}
}
const john = new Person("John");
9. 면접 질문 대비
Q1: 호이스팅이 무엇인가요?
답변: 호이스팅은 JavaScript 인터프리터가 코드 실행 전에 변수와 함수의 선언을 스코프 최상단으로 끌어올리는 것처럼 보이는 동작입니다. 실제로는 컴파일 단계에서 선언들이 메모리에 먼저 저장되기 때문에 발생합니다.
Q2: var, let, const의 호이스팅 차이는?
답변:
var: 선언이 호이스팅되고undefined로 초기화됩니다.let/const: 선언은 호이스팅되지만 초기화되지 않아 TDZ에 걸립니다.var는 함수 스코프,let/const는 블록 스코프입니다.
Q3: 다음 코드의 출력은?
console.log(a);
var a = 1;
console.log(b);
let b = 2;
답변:
- 첫 번째
console.log(a):undefined - 두 번째
console.log(b):ReferenceError: Cannot access 'b' before initialization
Q4: Temporal Dead Zone이란?
답변: TDZ는 let, const, class 선언에서 스코프 시작부터 실제 선언 라인까지의 구간입니다. 이 구간에서는 변수에 접근할 수 없으며, 접근 시 ReferenceError가 발생합니다.
마치며
축하합니다! 이제 JavaScript 호이스팅의 모든 것을 배웠습니다.
핵심 정리:
- var: 선언 호이스팅 (undefined), 함수 스코프
- let/const: TDZ 적용, 블록 스코프
- function 선언문: 완전 호이스팅 (값 포함)
- function 표현식/class: 변수 호이스팅 규칙 따름
- 함수 > 변수 우선순위
다음 단계:
- 클로저와 호이스팅의 관계 이해하기
- 실행 컨텍스트와 렉시컬 환경 공부하기
- 모듈 시스템에서의 호이스팅 탐구하기
- 코드베이스에서
var를let/const로 리팩토링하기
유용한 리소스:
실습 과제:
var로 작성된 레거시 코드를let/const로 변환하기- TDZ 관련 버그를 직접 재현하고 해결하기
- 함수 선언문과 표현식의 호이스팅 차이 실험하기
- 중첩된 스코프에서 호이스팅 동작 테스트하기
호이스팅을 이해하면 JavaScript의 동작 원리를 더 깊이 알게 되고, 예측 가능한 코드를 작성할 수 있습니다. 이제 자신있게 let과 const를 사용하고, var의 함정을 피하세요!
댓글