호이스팅(Hoisting)이란?
결론부터 말하자면 호이스팅이란 변수의 생성과 실제 값의 할당이 각각 다른 시점에 실행되는 문제로 인해 변수를 선언한 위치에 상관없이 항상 최상단에 선언된 것처럼 동작하는 것을 말합니다.
정확하게 어떤 과정에 의해서 호이스팅 현상이 발생하는지는 아래에서 자세하게 알아보겠습니다.
위에서 설명한 호이스팅의 정의에서 약간의 오해가 발생할 수 있는데, 선언부가 물리적으로 최상단으로 이동하는 것이 아닙니다.
선언부의 위치는 그대로이지만, 마치 최상단에 선언된 것처럼 동작한다는 것입니다.
따라서 아래와 같은 코드도 오류 없이 잘 작동됩니다.
console.log(x); // undefined
var x = 10;
하지만 변수에 할당한 값이 출력되는 것이 아닌 undefined
가 출력되는 모습을 볼 수 있습니다.
그 이유는 이전 포스트에서도 확인할 수 있지만, 아래에서 다시 설명하겠습니다.
그럼 왜 호이스팅 현상이 발생할까?
여기에서 호이스팅의 발생 원인인 실행 컨텍스트 생성 과정이 등장합니다.
실행 컨텍스트의 생성 과정이 기억나시나요?
대부분 함수 호출로 인해 생성되는 실행 컨텍스트는 아래의 과정을 거치게 됩니다.
활성 객체 생성 → arguments 객체 생성 → 스코프 체인 생성 → 변수 생성 → this 바인딩 → 코드 실행
그 중에서 변수 생성 부분을 자세하게 파고들어볼까요?
이전 포스트에서 설명한 변수 생성 과정은 아래와 같습니다.
- 함수로 인해 생성된 실행 컨텍스트의 경우, 함수 인자 각각 프로퍼티가 생성되고, 값이 할당된다.
- 함수 선언문으로 작성된 함수 객체 프로퍼티가 생성되고, 생성된 함수 객체로 값이 할당된다.
- 지역 변수 프로퍼티(함수 표현식 포함)가 생성되고, undefined 값이 할당된다.
이 중에서 3번 설명이 위에서 살펴본 예제에 대한 호이스팅 발생 원인이 되겠네요.
자바스크립트 엔진은 변수 생성 과정에서 함수 내부를 한 번 훑으며 선언된 변수 또는 함수 선언문 형태의 함수 객체 프로퍼티를 메모리에 추가합니다.
여기서 메모리에 추가된 프로퍼티가 함수 객체이냐 지역 변수이냐에 따라 동작이 달라지게 되는데, 일반적인 지역 변수의 경우에는 표현식을 만나기 전까지는 초기화 과정이 이루어지지 않아 undefined 값이 할당됩니다.
하지만, 함수 선언문 형태로 작성된 함수 객체의 경우에는 곧바로 해당 함수 객체 값을 할당받게 됩니다.
따라서 아래와 같은 코드도 정상적으로 동작할 뿐만 아니라 값도 제대로 출력되는 모습을 볼 수 있습니다.
console.log(getTen()); // 10
function getTen() {
return 10;
}
호이스팅 발생 원인을 더 자세하게 파고들어보자
이를 위해서는 렉시컬 환경(Lexical Environment)에 대해 알아야 합니다.
기본적으로 자바스크립트는 실행 중인 함수, if문 또는 for문 등에서 사용되는 코드 블록, 스크립트 전체에 대해 렉시컬 환경을 생성합니다.
이렇게 생성된 렉시컬 환경에서는 선언된 모든 지역 변수를 프로퍼티로 저장하는 환경 레코드(Environment Record)와 외부 렉시컬 환경에 대한 참조 부분으로 구성되어 있습니다.
이 중 환경 레코드는 변수 또는 함수 이름을 key 값으로, 할당된 값을 value로 하여 만들어진 key-value 쌍을 저장한 객체입니다.
따라서 자바스크립트 엔진은 실행 컨텍스트 생성 과정 중, 변수 생성 단계에서 찾아낸 변수 또는 함수 객체의 프로퍼티를 모두 렉시컬 환경에 추가하게 되죠.
그러므로 만약 해당 값을 참조하는 경우가 발생한다면, 가장 먼저 해당 렉시컬 환경을 찾아보게 됩니다.
이런 이유로 인해서 아래 코드에 의해 생성되는 렉시컬 환경은 다음과 같습니다.
var result = 100;
printResult(); // 100
function printResult() {
console.log(result);
}
EnvironmentRecord: {
result: 100,
printResult: <Function Object>
}
그럼 이제 var로 선언한 변수와 함수 선언문으로 정의한 함수 객체가 어떻게 호이스팅이 발생하게 되는지 알아봅시다.
우선, 저희에게 아래와 같은 예제 코드가 있다고 가정해보겠습니다.
console.log(x); // undefined
console.log(getTen()); // 10
var x = 10;
function getTen() {
return 10;
}
그럼 변수 생성 단계에서 아래와 같이 렉시컬 환경이 구성될 것입니다.
EnvironmentRecord: {
x: undefined,
getTen: <Function Object>
}
이제 실제 코드 실행 단계에 접어들었다고 생각해봅시다.
가장 먼저 마주친 코드는 변수 x를 출력하는 코드입니다.
위에서 변수를 참조할 일이 생기면 자바스크립트 엔진은 가장 먼저 현재 렉시컬 환경을 찾아본다고 했죠.
렉시컬 환경에서는 변수 x의 값이 undefined로 초기화되어 있습니다.
따라서 undefined 값을 출력하게 되고, 그 다음으로 만난 함수 호출 부분은 어떻게 될까요?
getTen 함수는 실제 함수 객체 값이 렉시컬 환경에 저장되어 있습니다.
그러므로 아무 이상 없이 getTen 함수가 정상적으로 작동하여 10을 출력합니다.
그럼 함수 표현식은?
함수 표현식은 선언한 변수에 함수 객체 값을 할당하는 것입니다.
즉, 일반적인 지역 변수 선언과 동일하다는 것입니다.
따라서 실행 컨텍스트 생성 과정 중 변수 생성 과정에서 지역 변수 프로퍼티를 추가하는 방식과 동일하게 작동됩니다.
console.log(getTen()); // TypeError: getTen is not a function
console.log(typeof getTen); // undefined
var getTen = function () {
return 10;
};
var로 정의했기 때문에 변수 생성 단계에서는 undefined 값을 할당하게 됩니다.
따라서 undefined 값을 가진 변수는 함수 호출이 불가능하기 때문에 위와 같이 TypeError가 발생합니다.
ES6에서 추가된 let과 const
설명을 시작하기에 앞서 아래 코드 결과를 확인해봅시다.
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 10;
var로 선언한 변수와는 다르게 let으로 선언한 변수는 선언부 위에서 참조가 불가능합니다.
이는 const로 선언한 변수에도 동일하게 적용되는데, 그럼 let과 const로 선언한 변수는 호이스팅 현상이 발생하지 않는 걸까요?
실행 결과를 보면 그렇게 보이지만, 실제로는 어떤 방식으로 선언을 하든 호이스팅 현상이 발생합니다.
하지만 렉시컬 환경에서 var로 선언한 변수와 차이점이 존재하죠.
var로 선언한 변수는 undefined 값으로 초기화됩니다.
하지만 x는 아래와 같이 렉시컬 환경에 프로퍼티가 추가됩니다.
EnvironmentRecord: {
x: <uninitialized>
}
여기에서는 undefined나 null도 아닌 uninitialized가 등장하는데요, uninitialized는 특수 내부 상태로서 자바스크립트 엔진이 변수를 인지할 수는 있지만 참조할 수 없는 상태를 의미합니다.
따라서 무조건 x 변수의 선언부 이후에 값을 참조해야만 합니다.
const도 마찬가지로 let 변수와 동일하게 작동됩니다.
코드의 가독성을 떨어뜨리는 원인 중 하나
만약 코드가 짧은 경우에는 크게 상관이 없겠지만, 코드량이 엄청나게 많을수록 호이스팅 현상으로 인해 코드의 가독성이 엄청나게 떨어질 수 있습니다.
따라서 자바스크립트 언어 개발 참여자 중 한 명인 더글라스 크락포드는 함수 표현식만 사용을 권장했죠.
이와 마찬가지로 함수 선언 뿐만이 아니라 변수 선언에서도 var를 통한 선언이 아닌, ES6의 let이나 const를 통해 선언하는 것이 가독성을 높이는 길이라고 볼 수 있습니다.
Source
-
인사이드 자바스크립트 도서
-
모던 JavaScript 튜토리얼 - 변수의 유효 범위와 클로저