이 글은
Sukhjinder Arora
의 Hoisting in Modern JavaScript — let, const, and var을 번역하여 작성된 글입니다. 오역이 있을 수 있으니 원문을 참고해주세요. 잘못된 내용이 있을 수 있으니 이 점 양해 바라며 댓글로 지적하여 주시면 빠르게 수정하도록 하겠습니다.
많은 자바스크립트 개발자들이 호이스팅을 변수와 함수의 선언부가 각자의 현재 스코프 영역 최상단으로 옮겨지는 것
이라고 설명합니다. 함수와 변수의 선언부가 물리적으로 코드의 최상단으로 옮겨지는 것은 사실이 아닙니다.
console.log(a);
var a = 'Hello World!';
사람들은 이 코드를 보고, 아래의 코드대로 호이스팅될 것이라고 말할 것입니다.
var a;
console.log(a);
a = 'Hello World!';
실제로 위의 코드는 에러 없이 잘 동작하기 때문에 선언부의 물리적인 이동이 있는 것처럼 보이지만, 물리적인 이동은 실제로 일어나고 있는 것이 아닙니다. 여러분이 작성한 코드는 어느 곳으로도 이동하지 않습니다. 자바스크립트 엔진은 여러분이 작성한 코드를 물리적으로 이동시키지 않고 작성한 위치 그대로 존재합니다.
컴파일 단계에서, 여러분이 작성한 코드가 실행되기 바로 직전에 변수와 함수의 선언부들이 쭉 스캔 됩니다. 스캔 된 변수와 함수의 선언부들은 Lexical Environment로 불리는 자바스크립트의 자료구조 메모리상에 추가됩니다. 따라서 메모리에 존재하는 변수와 함수는 실제 선언 이전에 사용될 수 있습니다.
Lexical Environment는 identifier-variable 쌍을 매핑하여 저장하는 자료구조입니다. 여기에서 identifier는 변수나 함수의 이름을 나타내고, variable은 타입을 나타냅니다.
추상적으로 아래와 같은 구조를 하고 있다고 볼 수 있습니다.
LexicalEnvironment = {
myVar: <value>,
myFunc: <func>
}
즉, Lexical Environment는 프로그램 실행 중에 변수와 함수들이 들어있는 공간으로 볼 수 있습니다.
만약 여러분이 Lexical Environment에 대해 더 알고싶다면 이전 아티클을 확인해주세요.
지금까지 호이스팅이 실제로 무엇인지 알아보았습니다. 이제 호이스팅이 함수와 변수의 선언부에 대해 어떻게 작동하는지 알아보도록 하겠습니다.
helloWorld(); // 'Hello World' 출력
function helloWorld() {
console.log('Hello World');
}
함수의 선언부가 컴파일 단계에서 메모리에 추가된다는 것은 이제 모두가 알게 되었습니다. 따라서 우리는 실제로 함수가 선언되는 시점 이전에 함수에 접근할 수 있습니다.
위의 코드에 대한 Lexical Environment의 모습을 보면 아래와 같습니다.
LexicalEnvironment = {
helloWorld: <func>
}
자바스크립트 엔진이 helloWorld()
를 실행하려고 할 때, 엔진은 Lexical Environment을 탐색할 것이고, 해당 함수를 찾아 정상적으로 실행할 수 있을 것입니다.
함수 선언은 호이스팅이 되지만 함수 표현식은 호이스팅이 되지 않습니다. 아래의 예시는 정상적으로 작동하지 않는 코드입니다.
helloWorld();
var helloWorld = function() {
console.log('Hello World');
}
자바스크립트는 오직 선언만 호이스팅할 뿐, 초기값 할당에 대한 부분은 호이스팅하지 않습니다. helloWorld
는 var 키워드와 함께 선언되었고, undefined로 호이스팅하기 때문에 helloWorld
는 함수가 아니라 변수처럼 취급됩니다.
아래의 코드는 정상적으로 작동하는 코드입니다.
var helloWorld = function() {
console.log('Hello World');
}
helloWorld();
var 변수에 대한 호이스팅을 이해하기 위해 아래의 예시를 살펴봅시다.
console.log(a); // 'undefined' 출력
var a = 3;
위 코드는 3
이 출력되도록 작성한 코드이지만 실제로는 undefined
를 출력합니다. 왜일까요?
자바스크립트는 초기값 할당에 대한 부분은 제외하고 오직 선언부만 호이스팅합니다. 즉, 컴파일 시간 동안 자바스크립트는 함수와 변수의 값을 제외한 선언부를 메모리에 저장합니다.
자바스크립트 엔진이 var
변수 선언부를 컴파일 단계에서 찾았을 때, 그 변수는 Lexical Environment에 추가될 것이고 undefined로 초기화됩니다. 그리고 실행시간 동안, 코드에서 실제로 할당이 이루어진 지점에 도달하게 되면 해당 값으로 다시 할당됩니다.
따라서 변수 선언 이후 Lexical Environment의 모습을 보면 아래와 같습니다.
LexicalEnvironment: {
a: undefined
}
이것이 3
이 아니라 undefined
를 출력하는 이유입니다. 그리고 엔진이 실행시간 동안 실제로 할당이 이루어진 지점에 도달하게되면, Lexical Environment에 저장된 값을 수정할 것입니다. 할당 이후 Lexical Environment의 모습을 보면 아래와 같습니다.
LexicalEnvironment: {
a: 3
}
먼저 아래의 예시를 봐봅시다.
console.log(a);
let a = 3;
실행 결과는 아래와 같습니다.
ReferenceError: a is not defined
위 예시에 따라 let
변수와 const
변수는 호이스팅이 되지 않는 것일까요?
이에 대한 답은 살짝 복잡할 수 있습니다. 모든 선언부는(함수, var, let, const, class)는 호이스팅이 됩니다. 하지만 var선언부는 undefined로 초기화되는 반면에 let과 const의 선언부는 uninitialized로 초기화됩니다.
let과 const의 값은 실제로 할당이 이루어진 지점에 도달해야 이루어지게 됩니다. 이 말은 즉, 실제로 할당이 이루어진 지점 이전에는 해당 변수에 접근할 수 없다는 것을 의미합니다. 변수의 선언과 변수의 초기화 사이의 변수에 접근할 수 없는 지점을 우리는 "Temporal Dead Zone" 이라고 부릅니다.
만약 자바스크립트 엔진이 let
과 const
변수가 선언된 지점에서 값을 찾을 수 없다면 undefined
로 할당을 하거나 에러를 발생시킬 것입니다.(const
의 경우)
아래의 예시를 봐봅시다.
let a;
console.log(a); // 'undefined' 출력
a = 5;
컴파일 단계 동안, 자바스크립트 엔진은 변수 a를 발견해 Lexical Environment에 a를 저장합니다. 하지만 let
변수이기 때문에 어떠한 값으로도 초기화하지 않습니다. 따라서 컴파일 단계에서의 Lexical Environment은 아래와 같습니다.
LexicalEnvironment: {
a: <uninitialized>
}
만약 우리가 a 변수가 선언되기 이전에 접근하려고 하면, 자바스크립트 엔진은 Lexical Environment에서 값을 가져오려고 할 것입니다. 하지만 a 변수는 uninitialized
이기 때문에 Reference error를 발생시킬 것입니다.
실행시간 동안, 엔진이 변수 선언부에 도달하게 되면, 변수의 값을 다시 바인딩하려고 합니다. 하지만 초기값이 할당되지 않았기 때문에 undefined
를 할당할 것입니다.
따라서 첫번째 줄을 실행하고 난 후에 Lexical Environment의 모습은 아래와 같습니다.
LexicalEnvironment: {
a: undefined
}
그리고 undefined
가 콘솔창에 출력이 되고, 변수 a에 5
가 할당됩니다. 이후에 Lexical Environment에 저장된 a
의 값을 undefined
에서 5
로 바꿀 것입니다.
참고 - let, const 변수가 실제로 할당되기 전에 실행만 되지 않는다면 참조는 할 수 있습니다. (eg. 함수 내)
function foo() {
console.log(a);
}
let a = 20;
foo(); // '20' 출력
하지만 아래의 코드는 Reference error을 유발합니다.
function foo() {
console.log(a);
}
foo(); // reference error
let a = 20;
let
변수와 const
변수의 선언처럼, 클래스 또한 호이스팅이 됩니다. 그리고 마찬가지로 클래스 또한 uninitialized
로 초기화됩니다. 따라서 "Temporal Dead Zone"에 영향을 받을 수 있습니다.
let peter = new Person('Peter', 25); // ReferenceError: Person is not defined
console.log(peter);
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
따라서 클래스를 사용하기 위해서는 먼저 선언을 해야합니다.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let peter = new Person('Peter', 25);
console.log(peter); // Person { name: 'Peter', age: 25 }
컴파일 단계에서 Lexical Environment의 모습은 아래와 같습니다.
LexicalEnvironment: {
Person: <uninitialzed>
}
그리고 클래스의 할당이 이루어진 후의 Lexical Environment의 모습은 아래와 같습니다.
LexicalEnvironment: {
Person: <Person object>
}
함수 표현식과 마찬가지로, 클래스 표현식 또한 호이스팅이 되지 않습니다. 예를 들면, 아래의 코드는 정상적으로 동작하지 않습니다.
let peter = new Person('Peter', 25); // ReferenceError: Person is not defined
console.log(peter);
let Person = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
정상적으로 동작하기 위해서는 아래와 같이 작성해야 합니다.
let Person = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let peter = new Person('Peter', 25);
console.log(peter); // Person { name: 'Peter', age: 25 }
이제 우리는 호이스팅이 이루어지는 동안, 자바스크립트 엔진은 우리가 작성한 코드를 물리적으로 움직이지 않는다는 것을 알게 되었습니다. 호이스팅 메카니즘을 적절히 이해하는 것은 호이스팅에 의해 발생하는 잠재적 버그와 혼란을 피하도록 도와줄 것입니다. undefined나 reference error와 같이 호이스팅의 사이드 이펙트(Side Effect)를 피하기 위해서는 항상 변수를 현재 스코프 최상단에서 선언하도록 하고, 선언과 함께 초기화를 해야 합니다.