셋 다 스코프의 위쪽으로 “등록”된다는 의미에서는 hoisting됩니다. 차이는 초기화입니다. var는 함수 스코프에 등록되면서 undefined로 초기화되기 때문에 선언 전에 읽으면 undefined가 나옵니다.
let과 const는 블록 스코프에 등록되지만 실제 선언문을 지나기 전까지 TDZ에 있어서 읽으면 에러가 납니다. 그래서 “let과 const는 hoisting되지 않는다”기보다 “hoisting되지만 초기화 전 접근이 막힌다”고 말하는 편이 더 정확합니다.
꼬리질문
조금 더 깊게 물어본다면
답변 뒤에 이어질 수 있는 질문들을 하나씩 열어볼 수 있어요.
let과 const는 hoisting되지 않는다고 말하면 틀린 건가요?
완전히 틀렸다고 보기는 어렵지만 정확한 표현은 아닙니다. let과 const도 스코프에 등록된다는 의미에서는 hoisting됩니다. 다만 선언문 실행 전까지 TDZ에 있어서 접근할 수 없기 때문에, 겉으로는 hoisting되지 않는 것처럼 보입니다.
Hoisting은 정의를 어떻게 보느냐에 따라 설명이 달라집니다. 선언이 스코프에 등록되는 관점에서는 hoisting되고, 초기화 전 접근 가능성 관점에서는 var와 다르게 막힙니다.
typeof를 쓰면 선언 전 let 변수도 안전하게 확인할 수 있나요?
아닙니다. 선언되지 않은 변수에 typeof를 쓰면 에러가 나지 않지만, TDZ에 있는 let/const 변수에 typeof를 쓰면 ReferenceError가 납니다.
console.log(typeof notDeclared); // "undefined"console.log(typeof value); // ReferenceErrorlet value = 1;
이 질문은 TDZ가 단순히 값을 읽을 때만 문제가 되는지, 식별자 접근 자체를 막는지 확인하려는 질문입니다.
TDZ는 왜 필요한가요?
선언 전에 변수를 읽는 실수를 빨리 드러내기 위해서라고 볼 수 있습니다. var처럼 조용히 undefined가 나오면 버그가 뒤늦게 발견될 수 있는데, let과 const는 선언 전 접근을 에러로 막아줍니다.
여기서 더 들어가면 “그럼 불편하기만 한 것 아닌가요?”라고 물을 수 있습니다. TDZ는 코드가 위에서 아래로 읽히는 직관과 실제 실행 결과를 더 가깝게 맞춰줍니다. 선언 전 접근을 허용하지 않기 때문에, 초기화 순서 버그를 더 빨리 발견할 수 있습니다.
var는 함수 스코프고 let과 const는 블록 스코프라는 말은 무슨 뜻인가요?
var는 if, for 같은 블록을 기준으로 새 스코프를 만들지 않고 함수 단위로 묶입니다. 반면 let과 const는 블록 안에서 선언하면 그 블록 안에서만 접근할 수 있습니다.
if (true) { var a = 1; let b = 2;}console.log(a); // 1console.log(b); // ReferenceError
여기서 꼬리로 for 루프와 closure를 물을 수 있습니다. var는 반복마다 새 binding을 만들지 않아서 비동기 callback에서 같은 값을 보게 되는 문제가 생길 수 있고, let은 반복마다 새로운 binding을 만들어 더 직관적으로 동작합니다.
함수 선언문과 var 함수 표현식은 hoisting이 어떻게 다른가요?
함수 선언문은 함수 객체까지 초기화되어 선언 전 호출이 가능합니다. 반면 var foo = function () {} 형태는 변수 foo만 undefined로 초기화되고, 함수 대입은 원래 코드 위치에서 일어납니다.
hello(); // 가능function hello() {}bye(); // TypeError: bye is not a functionvar bye = function () {};
이 질문은 “hoisting = 코드가 물리적으로 위로 이동한다”라고 단순화해서 외웠는지 확인하기 좋습니다. 실제로는 선언이 실행 컨텍스트에 먼저 등록된다고 이해하는 편이 안전합니다.
const 객체의 속성을 바꿀 수 있는데 왜 const인가요?
const가 막는 것은 변수 재할당입니다. 객체 내부 값까지 깊게 얼리는 것은 아닙니다. 객체 자체를 다른 객체로 바꾸는 것은 안 되지만, 같은 객체의 속성을 수정하는 것은 가능합니다.
여기서 한 번 더 물어보면 “그럼 진짜 불변 객체는 어떻게 만드나요?”가 나올 수 있습니다. 얕은 수준에서는 Object.freeze를 사용할 수 있지만, 중첩 객체까지 완전히 불변으로 만들려면 별도 처리가 필요합니다. const와 immutability를 같은 말로 보면 안 됩니다.
부가 설명
console.log(a); // undefinedvar a = 1;console.log(b); // ReferenceErrorlet b = 2;
var는 선언 전에도 이름을 찾을 수 있습니다. 다만 값은 아직 대입 전이라 undefined입니다.
let은 이름 자체가 없는 것처럼 보일 수 있지만, 실제로는 블록 스코프에 이미 등록되어 있습니다. 다만 선언문에 도달하기 전까지 접근할 수 없는 구간이 있고, 이 구간을 TDZ라고 부릅니다.
const는 hoisting과 TDZ 관점에서는 let과 거의 같습니다. 차이는 선언과 동시에 반드시 초기화해야 하고, 이후 재할당할 수 없다는 점입니다.
const value; // SyntaxError
다만 객체를 const로 선언했다고 해서 내부 값까지 불변이 되는 것은 아닙니다.
const user = { name: "Joon" };user.name = "Kim"; // 가능
한 줄 정리
var는 hoisting 후 undefined로 초기화되고, let과 const는 hoisting되지만 선언문 전까지 TDZ 때문에 접근할 수 없습니다.