← 글 목록

notes

15장. let, const 키워드와 블록 레벨 스코프

모던 JS Deep Dive — let·const와 블록 레벨 스코프 핵심 정리.

참고: 이웅모 《모던 자바스크립트 Deep Dive》 15장 · [MDN let](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let) · [MDN const](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const) · [MDN TDZ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#temporal_dead_zone_tdz)

var만 쓰던 시절엔 중복 선언·함수 스코프 누수·선언 전 undefined가 기본값이었다. let/const는 블록 칸·TDZ·재할당 규칙으로 그 구멍을 막는다. “새 키워드 외우기”가 아니라 스코프와 초기화 시점의 계약을 바꾸는 장이다.

flowchart TB
  V[var] --> BAD[중복·함수스코프·관대 호이스팅]
  L[let] --> OK[블록·TDZ·재할당 OK]
  C[const] --> BEST[블록·TDZ·재할당 금지]
  BEST --> NOTE[바인딩 고정 ≠ 깊은 불변]

15.1 var의 문제

15.1.1 중복 선언 허용

같은 스코프에서 var x를 두 번 써도 에러가 아니다. 의도와 다른 덮어쓰기가 조용히 통과한다.

var x = 1;
var x = 2; // OK with var — 실수여도 침묵
console.log(x); // 2

15.1.2 함수 레벨 스코프

var는 블록(if/for/{})을 무시하고 함수까지만 칸을 친다. 블록 안에서 선언한 것처럼 보여도 함수 전체로 호이스팅된다.

function f() {
  if (true) {
    var hidden = 1;
  }
  console.log(hidden); // 1 — 블록이 울타리가 아님
}

루프와 클로저가 겹치면 고전 버그:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3 3 3 — i가 하나, 루프 끝난 뒤 값

15.1.3 변수 호이스팅 (관대한 쪽)

var 선언은 스코프 맨 위로 끌어올려진 것처럼 보이고, 할당 전엔 undefined다. “아직 안 만들었는데 읽힘”이 에러가 아니라 값으로 나온다.

console.log(a); // undefined
var a = 1;

15.2 let

15.2.1 중복 선언 금지

같은 스코프에서 let x를 두 번 쓰면 SyntaxError. 실수가 침묵하지 않는다.

15.2.2 블록 레벨 스코프

{} / if / for / while / switch 등이 칸이 된다.

{
  let x = 1;
}
// console.log(x); // ReferenceError

for (let i = …)는 반복마다 바인딩이 달라지는 모델이라 클로저 친화적이다.

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0 1 2

15.2.3 호이스팅과 TDZ

let도 스코프 시작부터 “존재”하지만, 초기화 전 구간(TDZ) 에서 읽·쓰면 ReferenceError다. undefined로 관대하게 넘어가지 않는다.

{
  // TDZ starts
  // console.log(x); // ReferenceError
  let x = 1; // initialization
  console.log(x); // 1
}

WHY: “조립 전 가구에 앉지 마라.” 실패가 즉시라 디버깅이 쉽다.

15.2.4 전역 객체와 let

전역 let은 var처럼 window 프로퍼티로 붙지 않는 것이 일반적이다. 전역 스코프 바인딩은 생기지만, 전역 객체 프로퍼티 맵과는 별도의 환경에 가깝다.

var a = 1;
let b = 2;
// 브라우저에서: window.a === 1, window.b === undefined (보통)

15.3 const

15.3.1 선언과 초기화

const는 선언과 동시에 초기화가 필수다. const x; x = 1;은 문법 에러.

15.3.2 재할당 금지

바인딩(명찰)을 다른 값으로 갈아끼울 수 없다. 불변 객체라는 뜻이 아니다.

const user = { name: '민섭' };
user.name = 'Kim'; // OK — 객체 내용은 변경 가능
// user = {}; // TypeError — 바인딩 재할당 금지

15.3.3 상수

원시 값을 const에 두면 “이름 붙인 상수”로 쓰기 좋다. 객체·배열을 상수처럼 얼리려면 Object.freeze(얕음, 16장) 등 별 전략이 필요하다.

const MAX_RETRY = 3;
const COLORS = Object.freeze({ ok: 'green', err: 'red' });

선택 가이드

기본은 const. 재할당이 필요할 때만 let. var는 레거시·특수한 호이스팅 의존 코드.

예제 — 왜 이렇게 쳐 봤냐

function scoreBoard(scores) {
  const total = scores.reduce((a, b) => a + b, 0); // 바인딩 고정
  let grade = 'C';
  if (total > 90) grade = 'A';
  else if (total > 70) grade = 'B';
  return { total, grade };
}

WHY: total은 다시 안 바뀌니 const. grade는 분기마다 바뀌니 let.

// TDZ를 이용한 즉시 실패
function readConfig() {
  // console.log(port); // ReferenceError if uncommented before let
  let port = Number(process.env.PORT || 3000);
  return port;
}

헷갈리던 장면 / 함정

  • const = 깊은 불변이라고 착각 → 중첩 객체는 여전히 mutable
  • var 루프 + 클로저 → 전부 마지막 값
  • let을 스위치 케이스에 쓸 때 블록 없이 선언하면 충돌 가능 → case 안에 {}
  • TDZ를 “호이스팅 없음”으로 오해 → 호이스팅은 있으나 초기화 전 접근이 금지
  • 전역 let을 window.x로 읽으려다 undefined

질문 → 자답

Q. const만 쓰면 재할당이 필요할 때는? A. 그 순간 let. 또는 새 바인딩을 만드는 쪽(const next = …)으로 불변 스타일.

Q. for-of에서도 const? A. for (const item of list) 가능. 매 반복 새 바인딩. 재할당이 필요하면 let.

Q. var를 완전히 지워야 하나? A. 새 코드에선 사실상 yes. 레거시·트랜스파일 타깃·특수한 함수 스코프 의존만 예외.

웹 보강 메모

MDN TDZ 설명과 const의 “binding immutability” 문구를 다시 읽었다. ESLint prefer-const / no-var가 이 장과 같은 방향을 강제한다.

실무로 바로

  • 린트: no-var, prefer-const
  • 루프 콜백: let/const 블록 스코프 전제
  • 객체 “상수”: freeze는 얕음 → 구조적 공유·불변 라이브러리는 별도
  • 스위치: case마다 블록

var / let / const를 한 파일에서 마주칠 때

레거시 파일에 var가 남아 있고 새 코드에 let/const가 섞이면, 같은 이름이 함수 스코프와 블록 스코프에 동시에 있을 수 있다. 읽기 전에 “이 식별자의 칸이 어디인지”를 먼저 그린다.

function mixed() {
  var x = 1;
  {
    let x = 2; // 블록 칸 — 바깥 var x와 별개
    console.log(x); // 2
  }
  console.log(x); // 1
}

if / for 없이 블록만 쓰는 패턴

일시적 이름을 가두고 싶을 때 단독 {}를 쓰기도 한다. let/const 전용 트릭이다(var는 의미 없음).

{
  const tmp = expensive();
  use(tmp);
}
// tmp는 여기 없음

const와 객체 파라미터

function update(user) {
  // user = { ...user, name: 'x' }; // 재할당 — 스타일 선택
  user.name = 'x'; // 프로퍼티 변경 — const 파라미터여도 가능
}

파라미터도 const처럼 재할당을 피하려면 관례·린트(no-param-reassign)로 막는다. 언어가 파라미터를 const로 만들지는 않는다(기본은 재할당 가능 바인딩).

실무 마이그레이션 순서

  • var → let (동작 유지 우선)
  • 재할당 없는 let → const (prefer-const)
  • 루프·클로저 테스트
  • 전역 var는 모듈로 이동 검토

여기서 잠깐 💁‍♂️

16장은 프로퍼티 플래그·freeze로 “값 쪽 불변”을 다룬다. 15의 const는 바인딩 이야기다. 축을 섞지 말 것.