본문으로 건너뛰기

안동민 개발노트

본문 시작

let, const와 블록 스코프

var의 함수 스코프·재선언·호이스팅 문제를 확인하고 let·const의 블록 스코프와 TDZ, 재할당 규칙을 익힙니다.

우리는 7장 자바스크립트 심화 II에서 비동기 처리, 모듈 시스템, 디자인 패턴까지 실무에서 자주 마주치는 주제를 폭넓게 다뤘습니다.

이제 기본 웹 애플리케이션을 만들 수 있는 기반은 충분히 갖춰진 상태입니다.

다음 단계는 최신 표준 문법을 정확히 쓰는 것입니다.

자바스크립트는 ECMAScript 표준을 따라 매년 진화하고, 같은 기능도 예전 방식보다 더 안전하고 읽기 쉬운 문법으로 바뀌어 왔습니다.

특히 ES2015(ES6)는 전환점이었습니다.

단순 문법 추가가 아니라, 개발자가 오류를 줄이고 코드를 명확하게 작성하도록 언어의 방향을 바꿔 놓았습니다.

이 장에서는 ES2015 이후 핵심 문법을 다룹니다.

첫 주제는 var에서 let/const로 넘어가는 이유와 블록 스코프(Block Scope)입니다.

이번 장에서는 var의 한계를 짚고, let/const 특징과 스코프 개념을 중심으로 정리합니다.

lexical binding 초기화 수명

STATE · WEB PLATFORM CONTRACT

lexical binding 초기화 수명

scope 진입 → TDZ → declaration initialization → readable binding → let reassignment 또는 const rejection의 상태 전이

let·const 바인딩 초기화 수명주기 scope 진입 때 let const binding이 uninitialized TDZ에 생기고 declaration 실행 시 값을 초기화해 읽을 수 있으며 let 재할당은 새 값을 저장하고 const 재할당 시도는 TypeError를 던지지만 기존 binding은 그대로 readable인 상태 전이 scope 진입binding 생성 TDZuninitialized읽으면 ReferenceError declaration 실행initializer 평가 · 초기화 readable bindinglet 또는 const 값scope 종료까지 유지 let 재할당새 값 저장 const 재할당 시도binding 변경 금지 TypeError eventcatch 가능 · 기존 값 유지 CONST ERROR IS AN ABRUPT COMPLETION, NOT A DESTROYED BINDING
  1. scope 진입 → TDZ

    let·const binding은 생기지만 uninitialized라 declaration 전 읽기는 ReferenceError다.

  2. declaration initialization

    initializer를 평가한 뒤 값을 저장하면 readable binding이 된다.

  3. let

    재할당하면 새 값을 저장하고 같은 readable 상태로 돌아간다.

  4. const

    재할당 시도는 TypeError를 던진다. catch 뒤에도 기존 binding과 값은 그대로 readable이다.

const 재할당 오류는 binding의 종말이 아니다. 갑작스러운 완료가 발생할 뿐, 기존 값과 scope 수명은 유지된다.


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 (함수 스코프)

이는 다른 언어에서 블록 스코프가 일반적인 것과 달라 혼란을 야기했습니다.

변수 호이스팅 (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의 문제점을 해결하기 위해 도입된 변수 선언 방식입니다.

같은 블록 코드를 var와 let으로 실행했을 때 블록 밖 접근 결과 비교

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 — 바인딩은 선언된 블록 안에서만 유효하다.

비교 조건은 동일하며 선언 키워드만 다르다. 오류는 let의 결함이 아니라 블록 경계를 지키는 결과다.

블록 스코프 (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); // 결과: hello

TDZ는 개발자가 변수를 선언하기 전에 사용하는 실수를 방지하여 코드의 안정성을 높입니다.


키워드: 상수 선언

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.

객체나 배열 내부까지도 변경 불가능하게 만들려면, 깊은 복사(deep copy)를 사용하거나 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 서버 주소를 가정한 값입니다.


ES6 선언 키워드 정리

핵심은 var 습관을 줄이고 블록 스코프 기반으로 상태 수명을 의도적으로 제어하는 것입니다.

재할당 필요 여부에 따라 const와 let을 선택하고 var는 기존 코드 해석에 한정하는 흐름
선언 키워드 선택 흐름 새 코드에서 값의 재할당이 필요하지 않으면 const, 필요하면 let을 선택한다. var는 기존 코드 해석에만 남긴다. 새 코드 작성 값을 다시 대입해야 하나? YES NO let const
  • 재할당이 없다

    기본 선택은 const다.

  • 재할당이 필요하다

    let을 사용하고 변경 지점을 좁게 유지한다.

var는 새 코드의 선택지가 아니라 기존 코드의 스코프와 호이스팅을 해석할 때 확인한다.

이번 장에서는 자바스크립트에서 변수를 선언하는 새로운 표준인 letconst 키워드, 그리고 그와 함께 도입된 블록 스코프(Block Scope) 개념에 대해 심도 있게 학습했습니다.

var는 함수 스코프, 호이스팅, 재선언 허용 때문에 예측하기 어려운 상황을 만들 수 있습니다.

letconst는 블록 스코프와 TDZ를 통해 선언 위치와 사용 범위를 더 분명하게 드러냅니다.

현대 자바스크립트에서는 const를 기본으로 두고, 재할당이 필요한 경우에만 let을 사용합니다.

객체와 배열을 const로 선언해도 내부 값은 바뀔 수 있으므로, 재할당과 내부 변경을 구분해야 합니다.

마지막으로 선언 키워드를 실제 코드 리뷰에서 볼 때는 값의 변경 여부, 블록 밖 노출 여부, 초기화 시점을 함께 확인합니다.

const, let, var의 스코프, 재선언, 재할당, 선언 전 접근을 비교한 표
새 코드는 const를 기본으로 하고 재할당이 필요한 경우에만 let을 선택한다.
키워드 스코프 같은 스코프 재선언 재할당 선언 전 접근
const 기본 블록 불가 다른 값으로 바인딩 변경 불가 TDZ에서 ReferenceError
let 블록 불가 가능 TDZ에서 ReferenceError
var 함수 가능 가능 undefined로 읽힐 수 있음
const 기본
스코프 블록
같은 스코프 재선언 불가
재할당 다른 값으로 바인딩 변경 불가
선언 전 접근 TDZ에서 ReferenceError
let
스코프 블록
같은 스코프 재선언 불가
재할당 가능
선언 전 접근 TDZ에서 ReferenceError
var
스코프 함수
같은 스코프 재선언 가능
재할당 가능
선언 전 접근 undefined로 읽힐 수 있음
const는 객체 내부를 불변으로 만들지 않는다. 바인딩을 다른 값으로 재할당하지 못하게 할 뿐이다.