스코프 체인부터 호이스팅·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 ❌

마지막 줄에서 틀렸다. youter 안에서 태어난 변수라 함수 밖에는 존재하지 않는다. 그래서 참조하는 순간 에러다.

왜 그런가

  • 방향은 항상 안 → 밖, 한 방향이다. 밖에서 안(아래로)은 못 보고, 형제 함수끼리(옆으로)도 못 본다.
  • 자기 스코프에 없으면 한 겹 위로 올라가며 찾는다 = 스코프 체인. 전역까지 올라가도 없으면 그때 에러.
  • 가까운 게 이긴다 (섀도잉): 올라가다 같은 이름을 만나면, 가장 먼저 만난 것을 쓰고 멈춘다.
  • “어디서 호출했나”가 아니라 “코드에 어디 적혔나”로 정해진다. 그래서 렉시컬(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

varlet을 가르는 건 결국 “초기화가 언제 되느냐” 하나다.

에러 종류 구분 (음미 포인트)

  • (2) ReferenceError: 이름 자체를 아직 쓸 수 없다 (TDZ).
  • (4) TypeError: 이름은 있는데(undefined) 함수처럼 호출해서 타입이 틀렸다.

한 줄 요약: var는 호이스팅 때 초기화까지 돼서 undefined가 들어가지만, let/const는 선언만 되고 초기화는 실제 선언 줄에서 일어난다. 그 사이 구간(스코프 시작 ~ 선언 줄)이 TDZ이고, 여기서 쓰면 에러가 난다.

실무 교훈

변수·함수는 쓰기 전에 위에 선언하면 호이스팅을 신경 쓸 일이 거의 없다. var 대신 let/const를 쓰는 이유 중 하나가, TDZ라는 실수 예방 장치 덕분이다. (참고로 var는 함수 스코프, let/const는 블록 스코프다.)


두 개념을 한 문장으로

스코프 체인은 변수가 어디서 보이는지(공간)를, 호이스팅·TDZ는 변수가 언제부터 쓸 수 있는지(시간)를 정한다. 그리고 둘을 관통하는 공통점이 하나 있다 — “코드에 어디 적혔는가”가 모든 것을 결정한다는 점이다. 이 감각은 다음 난코스인 **클로저(W3)**에서 결정적으로 중요해진다.