let, const와 블록 스코프
var의 함수 스코프·재선언·호이스팅 문제를 확인하고 let·const의 블록 스코프와 TDZ, 재할당 규칙을 익힙니다.
우리는 7장 자바스크립트 심화 II에서 비동기 처리, 모듈 시스템, 디자인 패턴까지 실무에서 자주 마주치는 주제를 폭넓게 다뤘습니다.
이번 절은 실행 컨텍스트와 클로저에서 다룬 이름의 유효 범위와 초기화를 선언 키워드에 적용합니다. 먼저 이름을 찾는 스코프와 그 이름에 값을 대입하는 시점을 구별해 봅시다.
다음 단계는 최신 표준 문법을 정확히 쓰는 것입니다.
자바스크립트는 ECMAScript 표준을 따라 매년 진화하고, 같은 기능도 예전 방식보다 더 안전하고 읽기 쉬운 문법으로 바뀌어 왔습니다.
특히 ES2015(ES6)는 전환점이었습니다.
단순 문법 추가가 아니라, 개발자가 오류를 줄이고 코드를 명확하게 작성하도록 언어의 방향을 바꿔 놓았습니다.
이 장에서는 ES2015 이후 핵심 문법을 다룹니다.
첫 주제는 var에서 let/const로 넘어가는 이유와
블록 스코프(Block Scope)입니다.
이번 장에서는 var의 한계를 짚고, let/const 특징과 스코프 개념을 중심으로 정리합니다.
let·const 바인딩 초기화와 재할당 — 상태 추적
같은 바인딩의 상태가 미초기화 TDZ→선언 초기화→재할당 시도에 따라 달라진다. let의 새 값과 const 오류 뒤 기존 값 유지, 바깥 동명 변수 미탐색을 추적한다.
STATE · WEB PLATFORM CONTRACT
let·const 바인딩 초기화와 재할당
선언 위치에서 초기화되기 전에는 읽을 수 없다. 초기화 이후 let은 재할당할 수 있고, const의 재할당 오류는 기존 값을 바꾸지 않는다.
- scope 진입 → TDZ
let·const binding은 생기지만 uninitialized라 declaration 전 읽기는 ReferenceError다.
- declaration initialization
초기값을 평가해 저장하면 읽을 수 있다. 초기값 없는 let 선언은 undefined로 초기화한다.
- let
재할당하면 새 값을 저장하고 같은 readable 상태로 돌아간다.
- const
재할당 시도는 TypeError를 던진다. catch 뒤에도 기존 binding과 값은 그대로 readable이다.
초기화 완료 뒤에 읽기 가능
let은 재할당한 새 값을 읽는다. const의 재할당 시도는 오류를 던지지만 기존 값은 그대로 유지한다.
주황색 반환선은 let의 새 값 저장, 점선은 const 오류 이후 기존 값 유지를 뜻한다. TypeError는 catch로 처리할 수 있다. const가 가리키는 객체의 속성 변경은 별개다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 조건·주석 | scope 진입 → TDZ: let·const binding은 생기지만 uninitialized라 declaration 전 읽기는 ReferenceError다. |
| 원도표 조건·주석 | declaration initialization: 초기값을 평가해 저장하면 읽을 수 있다. 초기값 없는 let 선언은 undefined로 초기화한다. |
| 원도표 조건·주석 | let: 재할당하면 새 값을 저장하고 같은 readable 상태로 돌아간다. |
| 원도표 조건·주석 | const: 재할당 시도는 TypeError를 던진다. catch 뒤에도 기존 binding과 값은 그대로 readable이다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| TDZ | binding: uninitialized / 읽기: ReferenceError | 블록 이름은 있지만 선언 초기화 전이다 | 같은 이름의 바깥 값을 대신 검색하지 않는다 |
| 초기화 | binding: 초기화됨 / 읽기: 현재 값 | 선언을 실행해 값을 저장한다 | 선언 실행 뒤에만 해당 binding을 사용할 수 있다 |
| 재할당 비교 | let: 새 값 / const: TypeError/기존 값 유지 | let은 새 값을 저장하고 const 재할당은 실패한다 | catch하는 확장 가정 없이는 오류 뒤 실행이 계속된다고 쓰지 않는다 |
let·const 바인딩 초기화와 재할당 — 상태 추적
같은 바인딩의 상태가 미초기화 TDZ→선언 초기화→재할당 시도에 따라 달라진다. let의 새 값과 const 오류 뒤 기존 값 유지, 바깥 동명 변수 미탐색을 추적한다.
STATE · WEB PLATFORM CONTRACT
let·const 바인딩 초기화와 재할당
선언 위치에서 초기화되기 전에는 읽을 수 없다. 초기화 이후 let은 재할당할 수 있고, const의 재할당 오류는 기존 값을 바꾸지 않는다.
- scope 진입 → TDZ
let·const binding은 생기지만 uninitialized라 declaration 전 읽기는 ReferenceError다.
- declaration initialization
초기값을 평가해 저장하면 읽을 수 있다. 초기값 없는 let 선언은 undefined로 초기화한다.
- let
재할당하면 새 값을 저장하고 같은 readable 상태로 돌아간다.
- const
재할당 시도는 TypeError를 던진다. catch 뒤에도 기존 binding과 값은 그대로 readable이다.
초기화 완료 뒤에 읽기 가능
let은 재할당한 새 값을 읽는다. const의 재할당 시도는 오류를 던지지만 기존 값은 그대로 유지한다.
주황색 반환선은 let의 새 값 저장, 점선은 const 오류 이후 기존 값 유지를 뜻한다. TypeError는 catch로 처리할 수 있다. const가 가리키는 객체의 속성 변경은 별개다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 조건·주석 | scope 진입 → TDZ: let·const binding은 생기지만 uninitialized라 declaration 전 읽기는 ReferenceError다. |
| 원도표 조건·주석 | declaration initialization: 초기값을 평가해 저장하면 읽을 수 있다. 초기값 없는 let 선언은 undefined로 초기화한다. |
| 원도표 조건·주석 | let: 재할당하면 새 값을 저장하고 같은 readable 상태로 돌아간다. |
| 원도표 조건·주석 | const: 재할당 시도는 TypeError를 던진다. catch 뒤에도 기존 binding과 값은 그대로 readable이다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| TDZ | binding: uninitialized / 읽기: ReferenceError | 블록 이름은 있지만 선언 초기화 전이다 | 같은 이름의 바깥 값을 대신 검색하지 않는다 |
| 초기화 | binding: 초기화됨 / 읽기: 현재 값 | 선언을 실행해 값을 저장한다 | 선언 실행 뒤에만 해당 binding을 사용할 수 있다 |
| 재할당 비교 | let: 새 값 / const: TypeError/기존 값 유지 | let은 새 값을 저장하고 const 재할당은 실패한다 | catch하는 확장 가정 없이는 오류 뒤 실행이 계속된다고 쓰지 않는다 |
var 키워드의 문제점 (복습 및 심화)
var는 함수 스코프를 따르며, 몇 가지 독특한 동작 때문에 개발자에게 혼란을 주거나 의도치 않은 버그를 발생시키기 쉬웠습니다.
함수 스코프 (Function Scope): var로 선언된 변수는 오직 함수 내부에서만 지역 변수로 취급되며, if 문이나 for 문과 같은 블록 내부에서는 지역 변수로 취급되지 않고 해당 함수의 스코프(또는 전역 스코프)에 속하게 됩니다.
if (true) {
var x = 10;
console.log(x); // 결과: 10
}
console.log(x); // 결과: 10 (if 블록 밖에서도 접근 가능)
function introduce() {
var name = "김철수";
console.log(name); // 결과: 김철수
}
introduce();
// console.log(name); // ReferenceError: name is not defined (함수 스코프)함수 안의 name이 바깥으로 전달되는 것은 아닙니다. 다만 브라우저에는 window.name이라는 별도 이름이 이미 있으므로, 위 주석 줄을 해제했을 때 항상 ReferenceError라고 판단하면 안 됩니다. 함수 지역 변수와 주변 환경의 동명 값을 구별하세요.
변수 호이스팅 (Variable Hoisting): var로 선언된 변수는 선언부가 스코프의 최상단으로 끌어올려진 것처럼 동작합니다.
할당은 원래 위치에서 이루어지지만, 선언만 먼저 처리됩니다.
이 때문에 변수를 선언하기 전에 참조해도 오류가 발생하지 않고 undefined가 출력됩니다.
console.log(myVar); // 결과: undefined
var myVar = "hello";
console.log(myVar); // 결과: hello
// 실제로는 다음과 같이 동작합니다.
// var myVar;
// console.log(myVar);
// myVar = "hello";
// console.log(myVar);이는 코드를 예측하기 어렵게 만들고 잠재적인 버그의 원인이 됩니다.
변수 재선언 허용: var는 동일한 이름의 변수를 여러 번 재선언해도 오류를 발생시키지 않습니다.
var myName = "Alice";
console.log(myName); // 결과: Alice
var myName = "Bob"; // 동일한 이름으로 재선언해도 에러 없음
console.log(myName); // 결과: Bob이는 실수로 기존 변수를 덮어쓸 위험이 있어 대규모 프로젝트에서 버그를 유발할 수 있습니다.
이러한 var의 문제점들은 자바스크립트 개발을 어렵게 만드는 주요 원인이었습니다.
let 키워드: 새로운 변수 선언의 시작
let 키워드는 var의 문제점을 해결하기 위해 도입된 변수 선언 방식입니다.
COMPARISON
var와 let: 블록 밖 접근 결과
선언 키워드만 바꾼 같은 예제다. var는 함수 안에 남고, let은 if 블록 밖에서 접근할 수 없다.
var
function compareVar() {
if (true) {
var count = 10;
}
console.log(count);
}
compareVar();
출력 10 — compareVar 함수 스코프의 바인딩이 if 블록 밖에도 남는다.
let
function compareLet() {
if (true) {
let count = 10;
}
console.log(count);
}
compareLet();
결과 ReferenceError — 바인딩은 선언된 블록 안에서만 유효하다.
블록 스코프 (Block Scope)
let으로 선언된 변수는 선언된 블록({}) 안에서만 유효합니다.
블록은 함수, if 문, for 문, while 문 등의 중괄호로 둘러싸인 영역을 의미합니다.
let y = 100; // 전역 스코프 또는 상위 블록 스코프
if (true) {
let z = 20; // if 블록 내부에서만 유효
console.log(z); // 결과: 20
console.log(y); // 결과: 100 (상위 스코프 변수는 접근 가능)
}
// console.log(z); // ReferenceError: z is not defined (블록 밖에서 접근 불가)
for (let i = 0; i < 3; i++) {
console.log(i); // 결과: 0, 1, 2
}
// console.log(i); // ReferenceError: i is not defined (for 블록 밖에서 접근 불가)블록 스코프는 코드를 더 예측 가능하게 만들고, 변수 이름 충돌의 위험을 줄여줍니다.
변수 재선언 불가능
let은 동일한 스코프 내에서 같은 이름의 변수를 재선언하는 것을 허용하지 않습니다.
let fruit = "사과";
// let fruit = "바나나"; // SyntaxError: 'fruit' has already been declared이는 개발자의 실수를 방지하고 코드의 견고성을 높여줍니다.
변수 호이스팅과 TDZ
let 변수도 호이스팅되지만, var와 다르게 초기화되기 전까지는 접근할 수 없습니다.
이 영역을 TDZ (Temporal Dead Zone, 시간상 사각지대)라고 부릅니다.
TDZ에 있는 변수에 접근하려고 하면 ReferenceError가 발생합니다.
console.log(myLetVar); // ReferenceError: Cannot access 'myLetVar' before initialization
let myLetVar = "hello";
console.log(myLetVar); // 앞의 TDZ 접근을 제거했을 때 hello; 원문 그대로는 앞의 예외로 도달하지 못함TDZ는 개발자가 변수를 선언하기 전에 사용하는 실수를 방지하여 코드의 안정성을 높입니다.
const 키워드: 상수 선언
const 키워드는 let과 마찬가지로 블록 스코프를 따르며, 변수 호이스팅과 TDZ의 특성도 공유합니다.
하지만 가장 중요한 차이점은 상수(constant)를 선언할 때 사용된다는 것입니다.
즉, 초기화한 바인딩에 다른 값을 다시 대입할 수 없습니다. 그 값이 객체라면 객체 내부를 바꾸는 연산은 별개입니다.
재할당 불가능
const로 선언된 변수는 선언과 동시에 값을 할당해야 하며, 이후에는 재할당이 불가능합니다.
const PI = 3.14159;
console.log(PI); // 결과: 3.14159
// PI = 3.14; // TypeError: Assignment to constant variable. (재할당 불가능)
// const TAX_RATE; // SyntaxError: Missing initializer in const declaration (선언과 동시에 할당 필수)객체/배열과 const
const가 재할당을 금지한다는 것은 같은 이름을 다른 값에 다시 연결할 수 없다는 의미입니다. 실제 메모리 주소를 확인하는 규칙으로 이해할 필요는 없습니다.
객체나 배열과 같은 참조 타입의 경우, const로 선언해도 객체/배열 내부의 프로퍼티나 요소는 변경할 수 있습니다.
const user = {
name: "Jane",
age: 25
};
user.age = 26; // 객체 내부의 프로퍼티 변경은 가능
console.log(user.age); // 결과: 26
// user = { name: "Mike", age: 30 }; // 하지만 객체 자체를 재할당하는 것은 불가능
// TypeError: Assignment to constant variable.
const colors = ["red", "green"];
colors.push("blue"); // 배열에 요소 추가는 가능
console.log(colors); // 결과: ["red", "green", "blue"]
// colors = ["yellow"]; // 배열 자체를 재할당하는 것은 불가능
// TypeError: Assignment to constant variable.깊은 복사는 원본과 참조를 분리할 뿐 복사본의 변경을 막지는 않습니다. Object.freeze()도 기본적으로 해당 객체의 자체 속성만 동결하므로, 중첩 객체까지 동결하려면 별도의 처리가 필요합니다.
var vs let vs const
현대 자바스크립트 개발에서는 다음과 같은 규칙을 따르는 것이 일반적입니다.
-
기본적으로
const를 사용합니다.- 변수의 값이 변경될 일이 없는 상수나, 참조할 객체/배열이 재할당될 일이 없는 경우에 사용합니다.
- 이는 코드의 의도를 명확히 하고, 잠재적인 버그를 줄이는 데 큰 도움이 됩니다.
-
재할당이 필요한 경우에만
let을 사용합니다.- 반복문 내의 카운터 변수, 조건에 따라 값이 변경될 수 있는 변수 등에 사용합니다.
-
var는 더 이상 사용하지 않는 것을 권장합니다.- 레거시 코드(오래된 코드)를 유지보수하는 경우가 아니라면
var를 새로운 코드에서 사용하는 것은 피하는 것이 좋습니다.
- 레거시 코드(오래된 코드)를 유지보수하는 경우가 아니라면
// 전역 설정값, 절대 변하지 않는 값
const API_URL = "http://localhost:4000";
const MAX_ITEMS_PER_PAGE = 20;
// 한 번 할당되면 변하지 않는 참조형 값 (내부 변경은 가능)
const config = {
theme: "dark",
version: "1.0.0"
};
config.theme = "light"; // 가능
let counter = 0; // 값이 증가해야 하므로 let
for (let i = 0; i < 5; i++) { // 반복문 카운터는 let
counter += i;
}
if (someCondition) {
let result = "Success"; // if 블록 내에서만 필요한 변수는 let
console.log(result);
}
// console.log(result); // 에러위 API_URL은 별도로 실행한 로컬 Mock 서버를 가정합니다. someCondition도 앞서 정의된 조건값이 있어야 하는 예시용 이름입니다.
예시
같은 이름이 바깥에 있어도 현재 블록의 선언이 그 이름을 가립니다. 아래 코드는 각각 새 파일이나 독립 실행 범위에서 실행하세요. catch는 예상한 오류 뒤에도 다음 줄을 확인하기 위한 것입니다.
const limit = 10;
{
try {
console.log(limit);
} catch (error) {
console.log(error.name);
}
let limit = 3;
console.log(limit);
}
console.log(limit);출력은 ReferenceError, 3, 10 순서입니다. 블록에 진입할 때 내부 limit 바인딩은 있지만 아직 초기화되지 않았습니다. 따라서 바깥의 10으로 넘어가 읽지 않습니다. let limit = 3을 지난 뒤에는 내부 값을 읽고, 블록을 나오면 바깥 이름을 다시 읽습니다. 호이스팅은 소스 줄을 실제로 이동시키는 작업이 아니라 이러한 이름 생성·초기화 규칙을 설명하는 표현입니다.
스스로 확인하기
let limit = 3;을 try 앞에 옮기고 나머지는 그대로 두면 세 번의 출력은 무엇인가요? 오류가 사라진 이유를 스코프 변경과 초기화 시점 변경 중에서 골라 설명해 보세요.
풀이 확인
3, 3, 10입니다. 블록 경계는 그대로지만 첫 읽기 전에 내부 이름이 초기화되었습니다. 바깥의 limit은 재할당되지 않았습니다.
기본값 없는 일반 let 선언은 선언 평가 때 undefined로 초기화됩니다. 반면 일반 const 선언에는 초기값이 필요합니다. for (const value of values)처럼 반복문이 매번 값을 제공하는 문법은 별도로 해석합니다.
선언과 초기화의 범위는 MDN const 설명에서 다시 확인할 수 있습니다.
ES6 선언 키워드 정리
핵심은 var 습관을 줄이고 블록 스코프 기반으로 상태 수명을 의도적으로 제어하는 것입니다.
DECISION TREE
새 코드의 선언 키워드 선택
다른 값을 다시 대입해야 하는지 확인한다. 재할당이 없으면 const, 있으면 let을 선택한다.
- 재할당이 없다
기본 선택은
const다. - 재할당이 필요하다
let을 사용하고 변경 지점을 좁게 유지한다.
var는 새 코드의 선택지가 아니라 기존 코드의 스코프와 호이스팅을 해석할 때 확인한다. 객체의 속성 변경은 바인딩 재할당과 다르므로 const로도 가능하다.
이번 장에서는 자바스크립트에서 변수를 선언하는 새로운 표준인 let과 const 키워드, 그리고 그와 함께 도입된 블록 스코프(Block Scope) 개념에 대해 심도 있게 학습했습니다.
var는 함수 스코프, 호이스팅, 재선언 허용 때문에 예측하기 어려운 상황을 만들 수 있습니다.
let과 const는 블록 스코프와 TDZ를 통해 선언 위치와 사용 범위를 더 분명하게 드러냅니다.
현대 자바스크립트에서는 const를 기본으로 두고, 재할당이 필요한 경우에만 let을 사용합니다.
객체와 배열을 const로 선언해도 내부 값은 바뀔 수 있으므로, 재할당과 내부 변경을 구분해야 합니다.
마지막으로 선언 키워드를 실제 코드 리뷰에서 볼 때는 값의 변경 여부, 블록 밖 노출 여부, 초기화 시점을 함께 확인합니다.
COMPARISON
const·let·var의 선언 경계
스코프, 재선언, 재할당, 선언 전 접근을 같은 축으로 비교한다. const의 제한은 객체 내부가 아닌 바인딩에 적용된다.
| 키워드 | 스코프 | 같은 스코프 재선언 | 재할당 | 선언 전 접근 |
|---|---|---|---|---|
const 기본 |
블록 | 불가 | 다른 값으로 바인딩 변경 불가 | TDZ에서 ReferenceError |
let |
블록 | 불가 | 가능 | TDZ에서 ReferenceError |
var |
함수 · 최상위는 전역/모듈 | 가능 | 가능 | undefined로 읽힐 수 있음 |
const기본- 스코프 블록
- 같은 스코프 재선언 불가
- 재할당 다른 값으로 바인딩 변경 불가
- 선언 전 접근 TDZ에서 ReferenceError
let- 스코프 블록
- 같은 스코프 재선언 불가
- 재할당 가능
- 선언 전 접근 TDZ에서 ReferenceError
var- 스코프 함수 · 최상위는 전역/모듈
- 같은 스코프 재선언 가능
- 재할당 가능
- 선언 전 접근
undefined로 읽힐 수 있음
const는 객체 내부를 불변으로 만들지 않는다. 바인딩을 다른 값으로 재할당하지 못하게 할 뿐이다.