기존의 변수 선언 방식
모든 프로그래밍 언어에서의 변수는 선언 -> 할당 -> 사용의 과정을 거친다. 그리고 선언되어있지 않은 변수는 할당도, 사용도 불가능하다. 즉, 사전에 선언이 되어 있어야 사용이 가능하다. 자바스크립트에서는 이러한 과정에서 호이스팅이란 개념이 존재한다.
호이스팅 hoisting
호이스팅은 코드의 실행 전에, 변수의 선언 관련 정보를 미리 수집하여 실행 전에 변수에 접근할 수 있도록 하는 과정이다. MDN에서는 ‘인터프리터가 변수와 함수의 메모리 공간을 선언 전에 미리 할당하는 것을 의미’라고 설명하고 있다.
보통 호이스팅 관련한 글들을 보면 변수의 선언부를 끌어올린다
는 표현을 많이 쓴다. 아마 hoist
라는 단어의 뜻 때문인 것 같다. 무언가를 끌어올린다는 사전적 의미를 갖고 있다.
hoist : (noun) an act of raising or lifting something.
코드는 순차적이기 때문에 위/아래가 존재한다. 컴파일러/인터프리터는 위에서부터 아래로 순차적으로 코드를 해석한다. 따라서 이미지상으로 하단의(즉, 미래의) 변수들을 상단으로(현재) 끌어올린다는 표현이 이상하지가 않는 것이다.
1
2
3
4
5
run = function() {
console.log(foo)
var foo = 'foo' // 데이터 선언과 할당이 동시에 이루어진 코드.
}
run() // 'undefined'
호이스팅이 발생하여, foo
를 선언하기 이전에 콘솔에 찍어보면 undefined를 출력한다. 컴파일러가 코드를 실행하기 전에, 변수의 선언 정보들만 미리 스캔한다. var foo = 'foo'
이 부분에서 선언부분만 수집하게 되어, foo
변수를 메모리에 할당하고 문자열 ‘foo’를 할당하는 부분은 무시하고 디폴트값인 undefined
를 할당한다.
즉, 해당 변수를 초기화/사용하기도 전에 접근할 수 있다는 점을 조심해야한다.
호이스팅의 대상
호이스팅의 대상은 변수, 함수, 클래스이다. 여기서는 변수의 선언방식에 초점을 둔다면, var
, let
, const
이 세가지 방식이 있다. 이 세 가지 방식으로 선언된 변수들 모두 호이스팅이 된다, 하지만 할당 부분에서 차이가 있다. var
만 호이스팅의 대상이 된다는 말도 있는데, 내부적으로는 let
, const
도 호이스팅이 된다. 즉 실행 전에 정보 수집의 대상이 된다.
1
2
3
4
5
run = function() {
console.log(foo)
let foo = 'foo'
}
run() // ReferenceError: Cannot access 'foo' before initialization
위의 코드에서 foo is not defined
에러가 아니라 Cannot access 'foo' before initialization
에러가 나는 것은 foo
변수가 호이스팅의 대상이 되었음을 의미한다. 호이스팅의 대상이 된다는 얘기는 실행 전에 접근할 수 있음을, 엔진이 실행전에 해당 변수의 존재를 알고 있음을 의미한다. 호이스팅이 발생하지 않았다면, 선언하지 않은 변수를 사용하고자 했으므로 foo is not defined
에러가 나야 정상이다. 접근은 가능하나 값이 정의가 되어있지 않았을 뿐이다.
- var
- 선언부를 모두 수집한 후에,
undefined
를 기본값으로 일괄 할당한다.
- 선언부를 모두 수집한 후에,
- let, const
- 선언부를 모두 수집한 후에
TDZ
영역에 할당한다.TDZ
는 변수 선언 이후와 할당 전의 영역을 의미한다. 선언은 되었지만 할당 전의 변수에 대한 접근을 제한하는 역할이다.
- 선언부를 모두 수집한 후에
호이스팅 내부 동작 과정
결국엔, 호이스팅은 JS 엔진이 코드를 실행하는 과정의 일부이다. 그래서 자바스크립트 코드가 내부적으로 어떤 과정으로 실행되는지의 맥락에서 이해해 볼 필요가 있다. JS엔진 코드를 실행하기 위해 크게 두 가지 단계를 거친다.
- 실행 컨텍스트 생성
- 우선, 실행 컨텍스트를 생성한다. 그리고 해당 실행 컨텍스트 내의 코드를 쭉 훑으면서 여기서 사용할 변수들에 대한 정보를 미리 수집한다. 이 실행 컨텍스트 내의 코드를 이따 실행할건데, 어떤 애들로 구성이 되어 있나 한 번 사전 조사를 하는 과정이라고 보면 된다. 굳이 비유하자면, 출석부 호명하는 이미지 떠올리면 된다.
- 따라서, 변수의 선언부만 읽는다. 각 변수에 대한 값 할당은 임의적으로
undefined
로 할당하고, 나중에 실제로 코드가 실행될 때 코드에 따라 원래 값으로 할당한다. - 코드가 실행되기 전 상태는 실행 컨텍스트 내부의 모든 변수는
undefined
로 접근할 수 있는 상태가 된다.
- 코드 실행
- 사전 수집된 변수 정보를 가지고 코드를 한 줄씩 실행해 나간다.
위의 코드를 인터프리터 입장에서 다시 풀어서 코드로 표현해보자면 아래와 같다.
1
2
3
4
5
6
7
run = function() {
(var foo;) // 엔진이 끌어올린 변수 선언부
console.log(foo)
foo = 'foo'
}
run()
괄호 부분이 엔진이 모든 선언 부분을 처음으로 셋팅한 상태라고 볼 수 있다. 이렇게 사전에 변수에 대한 정보들을 미리 알고 난 이후에 (미리 선언해 둔 다음에) 코드를 실행해나간다.
왜 호이스팅 과정이 발생하는 걸까?
왜 이렇게 자바스크립트는 성격이 급해서 코드 실행도 전에 변수에 접근할 수 있도록 해주는 걸까? 자바스크립트의 창조주 브렌든 아이크님이 직접 답을 해주신 트윗을 발견했다. 진짜가 나타났다. 질문자님이 내가 가진 궁금증을 다 물어봐주셔서 속이 시원했다. 왜 이렇게 디자인되었는지가 궁금했다.
Q : 왜 자바스크립트는 호이스팅을 하나요? 왜 자바스크립트는 그런 방식으로 설계되었나요? 그러한 설계 방식은 무엇의 영향을 받았나요?
A : 함수 호이스팅으로 인해 다음 3가지를 할 수 있다.
- 탑다운 방식의 프로그램 구조 분해
- ‘let rec’를 자유롭게 사용하는 것
- 함수 선언 전에 함수 호출
변수에 대한 호이스팅은 다음 3가지의 의한 의도하지 않은 결과물이다.
- 함수 호이스팅
- 블록 레벨 스코프가 아님
- 1995년에 급하게 작업한 결과
let
이 도움이 될 것이다.
원래 호이스팅의 목적은 함수였다. 처음엔 함수만을 생각하고 설계했지만 변수에도 영향이 간 것으로 보인다. 생각해보면, 함수를 호출하기 위한 필수 사전조건은 함수가 정의되어 있어야한다 (=선언되어 있어야 한다.) 프로그램 내부에서 수많은 함수들이 호출될텐데, 각 함수들의 선언 순서를 모두 기억하고 순서에 따라 함수 호출을 제어한다는 것은 불가능하다. 그래서 개발자가 함수의 호출 순서에 구애받지 않고 개발하려면 모든 함수를 사전에 알고 있으면 좋지 않을까 하는 생각에서 이러한 설계가 나오지 않았을까 싶다.
정리
- 호이스팅 과정이 발생하는 이유는 자바스크립트 엔진이 그런 방식으로 설계되었기 때문이고,
- 자바스크립트 엔진이 그렇게 설계된 이유에는 함수를 편리하게 사용하게 할 목적이었다고 한다.
- 처음엔 함수 사용 목적이었으나 var 키워드를 통한 변수 선언에도 영향을 받게 되었다.
- 이러한 var의 한계점을 보완한 변수 선언 방식인 let 키워드가 등장했다.