❓호이스팅이란
hoisting을 번역해보면 '끌어올리다'라는 뜻을 가진다.
기본적으로 코드는 위에서 아래로 순차적으로 실행되지만, 사전적 정의와 마찬가지로 자바스크립트에서 호이스팅은 모든 선언문을 해당 스코프의 최상단으로 끌어올리는 동작을 말한다.
실제로 코드가 끌어올려지는 것은 아니고 자바스크립트 엔진이 런타임 이전에 모든 선언들을 렉시컬 환경이라고 하는 자바스크립트 내부 데이터 구조에 저장하는 것으로 선언문이 끌어 올려진 것처럼 동작하는 것이기 때문에 오해하는 일이 없길 바란다.
변수 호이스팅
변수는 var, let, const 키워드를 사용하여 선언을 진행한다. 이 때 var과 let, const와의 호이스팅에는 차이가 존재하기 때문에 구분해서 알아보도록 하자.
1️⃣ var
var과 let, const와의 가장 큰 차이점은 var은 선언과 동시에 초기화가 이루어진다는 점이다. 따라서 var 키워드를 통해 선언한 변수는 선언문 이전에 참조가 가능하고, 에러가 발생하지 않는다. 선언과 동시에 초기화가 이루어진다해서 할당까지 이루어지는 것은 아니다. 할당문은 예외다. 할당 이후에 변수를 참조해야 할당한 값을 얻을 수 있다.
console.log(foo) // undefined
var foo;
console.log(boo) // undefined
var boo = 1;
console.log(boo) // 1
2️⃣ let, const
var과 달리 let과 const는 호이스팅이 이루어지지 않는 것처럼 보인다. 하지만 모든 선언문은 반드시 호이스팅이 발생한다. 위에서 var 키워드로 선언한 변수는 런타임 이전에 선언과 초기화가 동시에 이루어진다고 했다. 하지만, let과 const 키워드로 선언한 변수는 선언과 초기화가 따로 진행된다. 이 선언과 초기화 사이의 구간을 일시적 사각지대(TMZ)라고 한다.
console.log(foo); // ReferenceError: foo is not defined
let foo;
console.log(foo); // undefined
위의 예시에서 알 수 있듯이 선언만 이루어지고 초기화는 선언문에서 실행되기 때문에 호이스팅이 이루어지지 않는 것처럼 보인다.
let foo = 1;
(function () {
console.log(foo); // Output: ReferenceError
let foo = 1;
})();
실제로 해당 키워드로 선언한 변수가 호이스팅이 이루어지지 않는다면 즉시 실행 함수가 실행되었을 때 전역 변수인 foo의 값인 1이 출력되어야 한다. 하지만 지역 변수인 foo가 선언 단계가 진행되었기 때문에 참조 에러가 발생하는 것을 알 수 있다.
const도 let과 동일하게 호이스팅이 이루어지지만 const 키워드를 통해 선언한 변수는 반드시 선언과 함께 초기화를 해주지 않으면 문법 에러가 발생한다.
함수 호이스팅
런타임 이전에 모든 선언문이 자바스크립트 엔진에 의해 먼저 실행된다고 했던 것처럼 함수도 또한 호이스팅이 일어난다. 이 때 함수 선언문과 함수 표현식에도 차이가 있기 때문에 구분해서 알아보자.
1️⃣ 함수 선언문
함수 선언문으로 함수를 정의한 경우에는 var 키워드를 사용한 변수 선언문과 같이 런타임 이전에 선언과 초기화가 동시에 이루어진다. 이 때 var 키워드는 초기화가 undefined로 이루어지는 반면 함수 선언문은 함수 객체 생성 후 함수 이름과 동일한 식별자가 생성되고 해당 식별자는 함수 객체로 초기화되기 때문에 함수 선언문 이전에 함수의 호출이 가능하다.
console.log(foo()); // 1
function foo() {
return 1;
}
2️⃣ 함수 표현식
함수 표현식은 사실 변수에 함수를 할당하는 것이기 때문에 변수 호이스팅과 동일하게 동작한다.
console.log(boo()); // TypeError: boo is not a function
console.log(boo); // undefined
var boo = function foo() {
return 1;
};
따라서 함수의 할당은 런타임 시 할당문이 실행되는 시점에 일어나기 때문에 boo라는 식별자는 할당문이 실행되는 시점에 함수 객체를 참조한다. 그전에는 변수 boo는 undefined의 값을 갖는다. 만약 해당 함수 표현식의 변수 키워드가 let 또는 const였다면 참조 에러가 발생할 것이다.
클래스 호이스팅
클래스도 함수 호이스팅과 마찬가지로 클래스 선언문과 클래스 표현식으로 나뉜다.
1️⃣ 클래스 선언문
클래스 선언문은 함수 선언문과 같이 런타임 이전에 함수 객체를 생성하게 되는데 클래스는 클래스 정의 이전에 참조할 수 없기때문에 클래스 선언문은 let, const 키워드를 사용한 변수 선언문과 같이 호이스팅이 발생하지 않는 것처럼 보인다.
console.log(Foo); // ReferenceError: Cannot access 'Foo' before initialization
class Foo {}
const Boo = 1 // ReferenceError: Cannot access 'Boo' before initialization
{
console.log(Boo)
class Boo {}
}
하지만 호이스팅이 발생하지 않았다면 위의 코드에서 전역 변수 Boo의 값인 1이 출력되었어야 한다.
2️⃣ 클래스 표현식
클래스 표현식은 일반적으로 사용되는 형식은 아니지만 함수 표현식과 같이 변수 호이스팅과 동일하게 동작한다.
TDZ(Temporal Dead Zone)
위에서 언급했듯이 TDZ는 선언과 초기화 사이의 구간을 말하고, 이 TDZ를 구분하는 중요한 키는 바로 선언이다. 런타임 이전에 모든 선언들이 자바스크립트 내부 데이터 구조인 렉시컬 환경에 저장되면서 다음과 같은 구조를 갖는다고 이해하면 되겠다. (해당 렉시컬 환경은 전역 변수에 한정된 것이다.)
lexicalEnvironment = {
var 키워드를 사용한 변수: undefined,
let 키워드를 사용한 변수: <uninitialized>,
const 키워드를 사용한 변수: <uninitialized>,
함수 선언문: 해당 함수 객체,
함수 표현식: - var 키워드를 사용한 변수: undefined
- let 키워드를 사용한 변수: <uninitialized>,
- const 키워드를 사용한 변수: <uninitialized>,
클래스 선언문: <uninitialized>,
클래스 표현식: - var 키워드를 사용한 변수: undefined
- let 키워드를 사용한 변수: <uninitialized>,
- const 키워드를 사용한 변수: <uninitialized>,
...
}
uninitialized로 선언되어 있는 변수들은 선언 후에 TDZ에 위치하고 초기화가 되어있지 않은 상태를 의미한다. 따라서 uninitialized의 값을 갖는 식별자들은 호이스팅이 되지 않은 것이 아니라 초기화가 되어있지 않아 호이스팅이 되지 않은 것처럼 보이는 것이다. 직접 체감할 수 있는 호이스팅은 var 키워드를 사용한 변수와 함수 선언문이다.
결론
이렇게 이번 포스팅은 호이스팅이 이루어지는 과정과 결과에 대해서 알아보는 시간이었다.
가능한 가독성과 유지보수를 위해서 순차적으로 코드를 작성하도록 하고 var 키워드를 사용한 변수 대신 let과 const를 사용해서 예기치 못하게 변수 선언 이전에 값을 참조하지 않도록 하자.