스코프 체인부터 호이스팅·TDZ까지
자바스크립트 난코스 로드맵 W1~W2 정리. 직접 만든 VN(비주얼 노벨) 엔진을 연습장 삼아, “예측 → 실제 → 왜”의 순서로 익힌 기록이다.
변수 하나를 두고 늘 두 가지 질문이 따라온다. “이 변수는 여기서 보이나?”(스코프), 그리고 “이 변수를 지금 써도 되나?”(호이스팅·TDZ). 앞은 공간의 문제, 뒤는 시간의 문제다. 둘을 차례로 정리한다.
1부. 스코프 체인 — 변수는 어디서 보이는가
한 줄 정의
변수를 찾을 때 자기 스코프 → 바깥 → … → 전역까지 한 겹씩 올라가며 찾고, 끝까지 없으면 ReferenceError. 위로만 간다.
내가 틀렸던 것 (예측 → 실제)
let x = "바깥";
function outer() {
let y = "안쪽";
console.log(x); // 예측 "바깥" → 실제 "바깥" ✅
console.log(y); // 예측 "안쪽" → 실제 "안쪽" ✅
}
outer();
console.log(y); // 예측 "바깥" → 실제 ReferenceError: y is not defined ❌마지막 줄에서 틀렸다. y는 outer 안에서 태어난 변수라 함수 밖에는 존재하지 않는다. 그래서 참조하는 순간 에러다.
왜 그런가
- 방향은 항상 안 → 밖, 한 방향이다. 밖에서 안(아래로)은 못 보고, 형제 함수끼리(옆으로)도 못 본다.
- 자기 스코프에 없으면 한 겹 위로 올라가며 찾는다 = 스코프 체인. 전역까지 올라가도 없으면 그때 에러.
- 가까운 게 이긴다 (섀도잉): 올라가다 같은 이름을 만나면, 가장 먼저 만난 것을 쓰고 멈춘다.
- “어디서 호출했나”가 아니라 “코드에 어디 적혔나”로 정해진다. 그래서 렉시컬(lexical) 스코프라고 부른다.
섀도잉 — 알아챌 정도만
let user = "현우";
function greet() {
let user = "수아"; // 같은 이름 재선언 → 바깥 user를 가린다
console.log(user); // "수아"
}실무에선 헷갈리니 같은 이름을 피하는 게 낫다. “분명 바깥에서 정했는데 값이 다르네?” 하는 버그를 만나면 **“안쪽에 같은 이름이 있나?”**부터 의심하면 된다.
내 코드와 연결 (VN 엔진)
render() 함수는 script·i를 인자로 받지 않았는데도 잘 쓴다. 이유는 단순하다. 둘이 render **바깥(전역)**에 적혀 있고, render는 안쪽이라 한 겹 올라가 전역에서 찾은 것이다. 엔진이 이미 이 원리로 돌고 있었다.
한 줄 요약: 함수 안에서 만든 변수는 그 함수 안에서만 살아 있어 밖에선 참조할 수 없다. 반대로 바깥 변수는 스코프 체인을 타고 올라가 찾을 수 있어 안쪽에서 보인다.
2부. 호이스팅과 TDZ — 변수는 언제 태어나는가
공간 문제를 정리했으니, 이제 시간 문제다. 같은 스코프 안이라도 선언 줄보다 위에서 변수를 건드리면 무슨 일이 벌어질까?
한 줄 정의
JS는 실행 전에 선언을 스코프 맨 위로 끌어올린다(호이스팅). 단, 끌어올려지는 건 선언이고 값 할당은 제자리에 남는다.
내가 틀렸던 것 (예측 → 실제) — 4개 전부 빗나감
console.log(a); var a = 1; // 예측 1 → 실제 undefined ❌
console.log(b); let b = 2; // 예측 2 → 실제 ReferenceError ❌
sayHi(); function sayHi(){ /*..*/} // 예측 error → 실제 정상 호출 ❌
sayBye(); var sayBye = function(){};// 예측 동작 → 실제 TypeError ❌왜 그런가 (4개 풀이)
- (1)
var a→ undefined: 선언만 위로 올라가var a;(값 없음 = undefined)가 되고,a = 1은 제자리에 남는다. 그래서 에러는 없고 값만 비어 있다. - (2)
let b→ ReferenceError:let/const도 끌어올려지긴 하지만 초기화는 안 된다. 실제 선언 줄 전까지 건드리면 에러. 이 구간이 바로 TDZ다. - (3)
sayHi→ 정상 호출: 함수 선언문은 이름 + 본체까지 통째로 호이스팅된다. 그래서 위에서 호출해도 된다. - (4)
sayBye→ TypeError: 이건 함수 표현식 =var에 함수를 담은 것이라 (1)번 규칙을 따른다.var sayBye만 올라가undefined가 되고,undefined()를 호출하니 TypeError.
핵심: 호이스팅의 두 단계
| 선언 (이름 등록) | 초기화 (값 자리) | |
|---|---|---|
var | 위로 ⬆ | 같이 위로 ⬆ → undefined |
let/const | 위로 ⬆ | 제자리 (선언 줄에서) → 그 전엔 TDZ |
→ var와 let을 가르는 건 결국 “초기화가 언제 되느냐” 하나다.
에러 종류 구분 (음미 포인트)
- (2) ReferenceError: 이름 자체를 아직 쓸 수 없다 (TDZ).
- (4) TypeError: 이름은 있는데(
undefined) 함수처럼 호출해서 타입이 틀렸다.
한 줄 요약:
var는 호이스팅 때 초기화까지 돼서undefined가 들어가지만,let/const는 선언만 되고 초기화는 실제 선언 줄에서 일어난다. 그 사이 구간(스코프 시작 ~ 선언 줄)이 TDZ이고, 여기서 쓰면 에러가 난다.
실무 교훈
변수·함수는 쓰기 전에 위에 선언하면 호이스팅을 신경 쓸 일이 거의 없다. var 대신 let/const를 쓰는 이유 중 하나가, TDZ라는 실수 예방 장치 덕분이다. (참고로 var는 함수 스코프, let/const는 블록 스코프다.)
두 개념을 한 문장으로
스코프 체인은 변수가 어디서 보이는지(공간)를, 호이스팅·TDZ는 변수가 언제부터 쓸 수 있는지(시간)를 정한다. 그리고 둘을 관통하는 공통점이 하나 있다 — “코드에 어디 적혔는가”가 모든 것을 결정한다는 점이다. 이 감각은 다음 난코스인 **클로저(W3)**에서 결정적으로 중요해진다.