호이스팅 — 코드가 위로 올라가는 게 아니다
Article
11강에서 함수 선언문은 정의보다 위에서 불러도 됐고, 표현식은 안 됐다. 12강에서 var는 블록을 무시했다.
두 현상의 뿌리가 같다. 호이스팅이다.
이상한 코드부터 본다
console.log(name);
var name = "지원";
- undefined
에러가 나야 할 것 같은데 undefined가 나온다. 없는 변수를 읽었다면 ReferenceError여야 하는데, 존재는 하되 값이 없는 상태다.
let으로 바꾸면 또 다르다.
console.log(name);
let name = "지원";
- ReferenceError: Cannot access 'name' before initialization
에러 문구를 잘 보자. "없다(is not defined)"가 아니라 **"초기화 전에는 접근할 수 없다"**고 한다. name이 있다는 건 알고 있다는 뜻이다.
실행 전에 한 번 훑어본다
자바스크립트는 코드를 만나면 곧바로 실행하지 않는다. 두 단계를 거친다.
- 생성 단계선언을 전부 찾아 메모리에 올린다
- 실행 단계한 줄씩 위에서 아래로 실행한다
생성 단계에서 자바스크립트는 코드를 훑으며 "여기 name이라는 변수가 있구나", "여기 greet이라는 함수가 있구나" 하고 미리 자리를 잡아둔다. 그래서 실행 단계에서 첫 줄이 name을 물으면 이미 알고 있다.
무엇이 어떻게 올라가나
핵심은 선언과 초기화가 분리된다는 것이다.
var name = "지원";
이 한 줄은 사실 두 가지 일이다. name을 선언하는 것과, "지원"을 할당하는 것. 생성 단계에는 선언만 처리되고, 할당은 실행 단계에서 그 줄에 도달해야 이뤄진다.
| 대상 | 생성 단계에서 | 그 전에 접근하면 |
|---|---|---|
| var | 선언 + undefined 로 초기화 | undefined |
| let · const | 선언만 (초기화 안 함) | ReferenceError (TDZ) |
| 함수 선언문 | 함수 전체가 통째로 | 정상 실행 |
| 함수 표현식 | 변수 규칙을 따름 | 변수와 동일 |
var만 undefined로 초기화된다. 그래서 값이 없는 채로 읽혔다.
let과 const는 자리는 잡히되 값이 채워지지 않는다. 이 선언은 됐지만 아직 쓸 수 없는 구간을 TDZ(Temporal Dead Zone, 일시적 사각지대)라 부른다.
- 01
생성 단계 — 이름만 등록
name이라는 변수가 있다는 걸 안다. 하지만 값은 넣지 않는다. - 02
TDZ 구간 — 접근 금지
코드 맨 위부터
let name줄 직전까지. 여기서 읽으면 ReferenceError 다.console.log(name); // 여기가 TDZ - 03
선언 줄에 도달 — 초기화
이제
"지원"이 들어가고 TDZ 가 끝난다. 이후로는 자유롭게 쓴다.let name = "지원"; // TDZ 종료
TDZ는 불편한 게 아니라 안전장치다. 값을 넣기도 전에 쓰는 실수를 즉시 잡아준다. var처럼 조용히 undefined를 주면 버그가 한참 뒤에 터진다.
함수는 통째로 올라간다
함수 선언문만 특별 대우를 받는다.
greet();
function greet() {
console.log("안녕");
}
- 안녕
생성 단계에서 함수 전체가 메모리에 올라가므로 어디서든 부를 수 있다.
표현식은 다르다. 11강에서 봤던 그 에러다.
sayHi();
const sayHi = function () {
console.log("안녕");
};
- ReferenceError: Cannot access 'sayHi' before initialization
const sayHi는 그냥 변수다. 변수 규칙을 따르니 TDZ에 걸린다. 오른쪽이 함수라는 사실은 아무 상관이 없다.
function greet() {}
선언문
- 함수 전체가 생성 단계에 등록
- 어디서든 호출 가능
- 정의를 아래에 둬도 된다
const greet = () => {}
표현식
- 이름만 등록, TDZ 에 갇힘
- 정의 아래에서만 호출 가능
- 읽는 순서 = 실행 순서
실행 컨텍스트
지금까지 말한 "생성 단계와 실행 단계를 거치는 코드 한 덩어리"의 정식 이름이 실행 컨텍스트다.
만들어지는 때는 두 번이다.
- 전역 실행 컨텍스트
- 함수 실행 컨텍스트
- 그 함수의 지역 변수
- 바깥 스코프로 가는 연결
- 함수 실행 컨텍스트
함수를 부를 때마다 새 실행 컨텍스트가 만들어지고, 그 안에서 다시 생성 단계와 실행 단계가 돌아간다. 그래서 함수 안에서도 호이스팅이 일어난다.
function test() {
console.log(inner);
var inner = "안쪽";
}
test();
- undefined
콜 스택 — 실행 컨텍스트가 쌓이는 곳
10강에서 잠깐 본 그 스택이다. 만들어진 실행 컨텍스트는 콜 스택에 쌓인다.
function third() {
console.log("가장 안쪽");
}
function second() {
third();
}
function first() {
second();
}
first();
- third()실행 중
- second()
- first()
- 전역 실행 컨텍스트맨 처음
Call Stack
프로그램이 시작되면 전역 실행 컨텍스트가 바닥에 깔린다. 함수를 부를 때마다 위로 쌓이고, 끝나면 위에서부터 사라진다. 가장 위에 있는 것이 지금 실행 중인 코드다.
스택이 한계를 넘으면 어떻게 되는지도 봤다.
function forever() {
forever();
}
forever();
- RangeError: Maximum call stack size exceeded
돌아올 줄 모르고 계속 쌓기만 하니 넘친다.
그래서 어떻게 쓰나
호이스팅을 활용하려 하지 말고, 없는 셈 치고 코드를 쓴다.
- 01
var 를 쓰지 않는다
undefined가 조용히 나오는 상황 자체를 없앤다.const를 기본으로 쓴다. - 02
변수는 쓰기 직전에 선언한다
TDZ 를 만날 일이 아예 없어진다.
- 03
함수도 정의를 위에 둔다
선언문이라 아래에 둬도 동작하지만, 읽는 사람은 위에서 아래로 읽는다.
정리하면
- 호이스팅은 코드가 움직이는 게 아니다. 실행 전에 선언을 먼저 수집할 뿐이다.
var는undefined로 초기화되고,let·const는 초기화되지 않아 TDZ에 걸린다.- TDZ는 불편이 아니라 안전장치다. 값을 넣기 전에 쓰는 실수를 바로 잡아준다.
- 함수 선언문은 통째로 등록되고, 표현식은 변수 규칙을 따른다.
- 코드 한 덩어리를 실행하는 단위가 실행 컨텍스트이고, 이것이 콜 스택에 쌓인다.
- 콜 스택은 한 번에 하나씩만 처리한다. 이게 싱글 스레드의 의미다.
다음 강의는 클로저다. 스코프와 실행 컨텍스트를 배웠으니 이제 이해할 수 있다. 자바스크립트에서 가장 유명하고, 가장 오해받는 개념이다.