호이스팅
닭이 먼저냐, 달걀이 먼저냐
코드가 한 주 한 줄 위에서부터 차례대로 해석될 것이라고 생각하기 쉽다. 대체로 옳은 생각이지만, 바로 이런 추정 때문에 프로그램을 잘못 이해 할 수 있다.
a = 2;
var a;
console.log(a);
??? 답은 2가 나온다 왜??
위에서 아래로 처리되는 방식이 아니었으니 이번 코드에서도 그런 식으로 처리되어 똑같이 2가 출력되리라 생각할지도 모른다. 또는 a가 선언되기 전에 사용되었으니 ReferenceError가 발생한다고 생각할 수도 있다.
불행히도 둘 다 틀렸다. 출력 결과는 undefined이다.
무엇이 먼저일까. 선언문(달걀)일까? 아니면 대입문(닭)일까?
컴파일러는 두 번 공격한다.
자바스크립트 엔진이 코드를 인터프리팅하기 전에 컴파일한다는 사실을 기억해보자. 컴파일레이션 단계 중에는 모든 선언문을 찾아 적절한 스코프에 연결해주는 과정이 있었다. 바로 이 과정이 렉시컬 스코프의 핵심.
자, 이제 변수와 함수 선언문 모두 코드가 실제 실행되기 전에 먼저 처리된다고 보면 된다. 어쩌면 “var a = 2;”를 하나의 구문이라고 생각할 수 있다. 그러나 자바스크립트는 다음 두 개의 구문으로 본다.
- var a;
- a = 2;
첫째 구문은 선언문으로 컴파일레이션 단계에서 처리된다. 둘째 구문은 대입문으로 실행 단계까지 내버려둔다. 따라서 첫 번째 코드 조각은 다음과 같이 처리된다.
var a;
a = 2;
console.log(a);
첫째 부분은 컴파일레이션 과정이고, 둘째 부분은 실행 과정이다. 비슷한 방식으로 두 번쨰 코드 조각은 다음과 같이 처리된다.
var a;
console.log(a);
a = 2;
이 과정을 비유적으로 말하면 변수와 함수 선언문은 선언된 위치에서 코드의 꼭대기로 ‘끌어 올려’진다.
이렇게 선언문을 끌어올리는 동작을 ‘Hoisting’이라고 한다. 즉, 달걀(선언문)이 닭(대입문)보다 먼저다.
선언문만 끌어올려지고 다른 대입문이나 실행 로직 부분은 제자리에 그대로 둔다. 호이스팅으로 코드 실행 로직 부분이 재배치된다면 큰 혼란이 생길 수 있다.
foo(); function foo(){ console.log( a ); var a = 2; }
함수 foo()의 선언문은 끌어올려졌으므로 foo를 첫째 줄에서도 호출할 수 있다.
또한 호이스팅이 스코프별로 작동한다는 점도 중요하다. 앞에서는 오직 글로벌 스코프만 포함된 단순한 상황을 예로 들었지만, 예제의 함수 foo()내에서도 변수 a가 ( 명백하게도 프로그램의 꼭대기가 아니라 ) foo()의 꼭대기로 끌어올려진다. 예제 코드에 호이스팅을 적용해 좀 더 정확히 해석하면 다음과 같다.
function foo(){
var a;
console.log(a);
}
function foo(){
console.log(3);
}
위 예시를 통해 같은 스코프 내에서의 중복 정의가 얼마나 나쁜 방식이고 혼란스러운 결과를 내는지 잘 알 수 있다. 일반 블록 안에서 보이는 함수 선언문은 보통 둘러싼 스코프로 끌어올려지지만, 다음 코드가 보여주듯 따르지 않을 수도 있다.
WRAP UP
“var a = 2”; 는 하나의 구문 처럼 보이지만, 자바스크립트 엔진은 그렇게 보지 않는다. 엔진은 이를 ‘var a’와 ‘a = 2’라는 두 개의 독립된 구문으로 보고, 첫째 구문은 컴파일러 단계에서 처리하고 둘째 구문은 실행 단계에서 처리한다.
이것이 의미하는 바는 스코프의 모든 선언문은 어디서 나타나든 실행 전에 먼저 처리된다는 점이다. ‘호이스팅’이라 불리는 이 과정은 선언문 각각이 속한 스코프의 꼭대기로 “끌어올려”지는 작업이라고 생각할 수 있다. 그 과정에서 선언문 자체는 옮겨지지만, 함수 표현식의 대입문을 포함한 모든 대입문은 끌어올려 지지 않는다.
중복 선언을 조심하자. 일반 변수 선언과 함수선언을 섞어 사용하면 특히 더 위험하다!