호이스팅
이제 스코프라는 개념에 어느 정도 익숙해졌..나? 어디서 얻허게 선언되는지에 따라 변수가 다른 여러 수준의 스코프에 붙게 되는 과정도 이해했을 것이다. 함수 스코프와 블록 스코프 모두 이점에서는 똑같은 규칙에 따라 작동한다. 한 스코프 안에서 선언된 변수는 바로 그 스코프에 속한다.
선언문이 스코프의 어디에 있는지에 따라 스코프에 변수가 추가되는 과정에 미묘한 차이가 있다. 여기서 그 차이에 대해 살펴보자.
닭이 먼저냐 달걀이 먼저냐
자바스크립트 프로그램이 실행되면 코드가 한 줄 한 줄 위에서부터 차례대로 해석될 것이라고 생각하기 쉽다. 대체로 옳은 생각이지만, 바로 이런 추정 대문에 프로그램을 잘못 이해하는 경우가 있다. 다음 코드를 보자.
a = 2;
var a;
console.log(a);
console.log()
의 결과로 무엇이 출력될까? 많은 개발자는 결괏값이 undefined
로 나오리라 예상한다. var a
선언이 a = 2
뒤에 있어서 해당 변수가 재정의되어 기본값인 undefined
를 가질 것이라 추측하는 것은 자연스러운 일이다. 그러나 출력 결과는 2다.
console.log(a);
var 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;
이 과정을 비유적으로 말하면 변수와 함수 선언문은 선언된 위치에서 코드의 꼭대기로 끌어롤려진다. 이렇게 선언문을 끌러올리는 동작을 '호이스팅'이라고 한다. 즉, 달걀(선언문)이 닭(대입문)보다 먼저다.
선언문만 끌어올려지고 다른 대입문이나 실행 로직 부분은 제자리에 그대로 둔다. 호이스팅으로 코드 실행 로직 부분이 재배치 된다면 큰 혼란이 올 수 있다.
foo();
function foo() {
console.log(a); // undefined
var a = 2;
}
함수 foo의 선언문은 끌어올려졌으므로 foo를 첫째 줄에서도 호출할 수 있다.
호이스팅이 스코프 별로 작동한다는 점도 중요하다. 앞에서는 오직 글로벌 스코프만 포함된 단순한 상황을 예로 들었지만, 예제의 함수 foo()
내에서도 변수 a가 (명백하게도 프로그램의 꼭대기가 아니라) foo()
의 꼭대기로 끌어올려진다. 예제 코드에 호이스팅을 적용해 좀 더 정확히 해석하면 다음과 같다.
function foo() {
var a;
console.log(a); // undefined
a = 2;
}
foo();
함수 선언문은 이와 같이 끌어올려지지만 함수 표현식은 다르다.
foo(); // not ReferenceError, but TypeError
var foo = function foo() {
// ...
};
변수 확인자 foo는 끌어올려져 둘러싼 스코프에 붙으므로 foo()
호출은 실패하지 않고, ReferenceError도 발생하지 않는다. 그러나 foo()
가 undefined
값을 호출하려 해서 TypeError라는 오작동을 발생시킨다.
또 기억할 것은 함수 표현식이 이름을 가져도 그 이름 확인자느 해당 스코프에서 찾을 수 없다는 점이다.
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
이 코드에 호이스팅을 적용하면 다음과 같이 해석된다.
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function(){
var bar = //...
//...
}
함수가 먼저다
함수와 변수 선언문은 모두 끌어올려진다. 그러나 미묘한 차이가 있는데, 먼저 함수가 끌어올려지고 다음으로 변수가 올려진다.
foo(); // 1
var foo;
function foo() {
console.log(1);
}
foo = function () {
console.log(2);
};
결괏값으로 2가 아니라 1이 출력된다! 엔진은 이 코드를 다음과 같이 해석한다.
function foo() {
console.log(1);
}
foo(); // 1
foo = function () {
console.log(2);
};
var foo
가 중복 선언문이라는 점을 보자. var foo
는 function foo()
선언문보다 앞서 선언됐지만, 함수 선언문이 일반 변수 위로 끌어올려졌다. 많은 중복 변수 선언문이 사실상 무시됐지만 중복함수 선언문은 앞선 것들을 겹쳐 쓴다.
foo(); // 3
function foo() {
console.log(1);
}
var foo = function () {
console.log(2);
};
function foo() {
console.log(3);
}
앞에서 살펴본 점들이 그저 흥미로운 학술적 상식에 지나지 않는 것처럼 들릴 수 있겠지만, 이를 통해 같은 스코프 내에서의 중복 정의가 얼마나 나쁜 방식이고 혼란스러운 결과를 내는지 잘 알 수 있다. 일반 블록 안에서 보이는 함수 선언문은 보토 둘러싼 스코프로 끌어올려지지만, 다음 코드가 보여주듯 따르지 않을 수도 있다.
foo();
var a = true;
if (a) {
function foo() {
console.log("a");
}
} else {
function foo() {
console.log("b");
}
}
이 동작은 맹신할 수 있는 것이 아니라 자바스크립트 차후 버전에서는 바뀔 수 있따는 점을 알아야 한다. 즉, 블록 내 함수 선언은 지양하는 것이 좋다.
정리하기
var a = 2
는 하나의 구문처럼 보이지만, 자바스크립트 엔진은 그렇게 보지 않는다. 엔진은 이를 var a
와 a = 2
라는 두 개의 독립된 구문으로 보고, 첫째 구문은 컴파일러 단계에서 처리하고 둘째 구문은 실행 단계에서 처리한다.
이것이 의미하는 바는 스코프의 모든 선언문은 어디서 나타나든 실행 전에 먼저 처리된다는 점이다. '호이스팅'이라 불리는 이 과정은 선언문 각각이 속한 스코프의 꼭대기로 '끌어올려' 지는 작업이라고 생각할 수 있다. 그 과정에서 선언문 자체는 옮겨지지만, 함수 표현식의 대입문을 포함한 모든 대입문은 끌어올려 지지 않는다.
중복 선언을 조심하자. 일반 변수 선언과 함수 선언을 섞어 사용하면 특히 더 위험하다.
참고
- You Don't Know JS ( 한빛 미디어 )
'JAVASCRIPT' 카테고리의 다른 글
[JS][스코프 클로저] 반복문과 클로저 (0) | 2022.02.18 |
---|---|
[JS][스코프 클로저] 클로저 개요 (0) | 2022.02.18 |
[JS][함수 vs 블록 스코프] 스코프 역할을 하는 블록 (0) | 2022.02.17 |
[JS][함수 vs 블록 스코프] 스코프 역할을 하는 함수 (0) | 2022.02.16 |
[JS][함수 vs 블록 스코프] 일반 스코프에 숨기 (0) | 2022.02.16 |
댓글