let, var, const — 변수 선언의 차이
이 토픽을 마치면
var, let, const의 차이를 스코프와 재할당 관점에서 설명할 수 있고, 어떤 상황에서 무엇을 써야 하는지 판단할 수 있습니다.
세 가지 선언 키워드
JavaScript에서 변수를 선언하는 방법은 세 가지입니다:
var name = "철수"; // ES5 (옛날 방식)
let age = 25; // ES6 (2015~)
const PI = 3.14159; // ES6 (2015~)셋 다 "값을 담는 이름을 만든다"는 점은 같습니다. 차이는 스코프(어디까지 보이는가)와 재할당(값을 바꿀 수 있는가)에 있습니다.
재할당 가능 여부
let count = 0;
count = 1; // ✅ let은 재할당 가능
const MAX = 100;
MAX = 200; // ❌ TypeError: Assignment to constant variableconst는 선언 후 재할당이 불가능합니다. 한번 넣은 값을 바꿀 수 없습니다. let과 var는 재할당이 가능합니다.
주의: const는 "값이 불변"이 아니라 "변수 바인딩이 불변"입니다. 객체나 배열의 내부는 바뀔 수 있습니다:
const user = { name: "철수" };
user.name = "영희"; // ✅ 객체 내부 수정은 가능
user = {}; // ❌ 변수 자체를 다른 값으로 바꾸는 건 불가스코프 — var vs let/const
var는 함수 스코프입니다. if문이나 for문의 블록({})을 무시합니다:
function example() {
if (true) {
var x = 10;
}
console.log(x); // 10 — if 블록 밖에서도 접근 가능!
}let/const는 블록 스코프입니다. 선언된 블록({}) 안에서만 존재합니다:
function example() {
if (true) {
let y = 10;
}
console.log(y); // ReferenceError: y is not defined
}블록 스코프가 직관적입니다. 선언한 중괄호 안에서만 살아있고, 밖에서는 사라집니다.
for문에서의 차이
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 출력: 3, 3, 3 — 모두 같은 i를 참조var로 선언한 i는 for문 블록을 무시하므로, setTimeout이 실행될 때 이미 i는 3이 된 상태입니다.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 출력: 0, 1, 2 — 각 반복마다 새로운 ilet은 각 반복마다 새로운 블록 스코프를 만들어서, 각각의 i가 독립적으로 존재합니다. 이것이 let과 var의 실질적으로 가장 중요한 차이입니다.
호이스팅
호이스팅(hoisting)이란 선언이 코드 최상단으로 끌어올려지는 현상입니다.
console.log(a); // undefined (에러 아님!)
var a = 5;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 5;var는 선언이 호이스팅되면서 undefined로 초기화됩니다. 그래서 선언 전에 접근해도 에러가 나지 않습니다 — 이건 버그를 찾기 어렵게 만듭니다.
let과 const도 호이스팅되지만, 초기화 전까지 접근하면 에러를 냅니다. 이 구간을 TDZ(Temporal Dead Zone)라고 합니다. 에러가 나는 편이 디버깅에 유리합니다.
실무 가이드라인
// 1. 기본은 const
const API_URL = "https://api.example.com";
const users = [];
// 2. 값이 바뀌어야 할 때만 let
let count = 0;
count++;
// 3. var는 쓰지 않는다
// (레거시 코드에서만 볼 수 있음)| 키워드 | 스코프 | 재할당 | 호이스팅 | 사용 시점 |
|---|---|---|---|---|
| var | 함수 | ✅ | undefined로 초기화 | 쓰지 않음 |
| let | 블록 | ✅ | TDZ (에러) | 값이 변할 때 |
| const | 블록 | ❌ | TDZ (에러) | 기본값 |
모던 JavaScript에서는 const를 기본으로 쓰고, 재할당이 필요할 때만 let으로 바꿉니다. var는 역사적 이유로 남아있을 뿐, 새 코드에서는 쓸 이유가 없습니다.
클로저와 var의 유명한 버그
var의 함수 스코프가 만드는 가장 유명한 버그:
// 5개 버튼에 각각 다른 숫자를 보여주고 싶다
for (var i = 0; i < 5; i++) {
document.getElementById("btn" + i).onclick = function() {
alert(i); // 전부 5를 alert
};
}모든 버튼이 5를 표시합니다. 클릭 시점에 i는 이미 5가 되었고, 모든 콜백이 같은 i를 참조하기 때문입니다.
// let으로 바꾸면 해결
for (let i = 0; i < 5; i++) {
document.getElementById("btn" + i).onclick = function() {
alert(i); // 0, 1, 2, 3, 4
};
}let은 각 반복마다 새 스코프를 만들므로, 각 콜백이 자신만의 i를 가집니다. ES6 이전에는 IIFE(즉시 실행 함수)로 우회했는데, let의 등장으로 이 패턴이 불필요해졌습니다.
함수 선언에서의 var/let/const
함수를 만들 때도 선언 방식에 따라 차이가 있습니다:
// 함수 선언문 — 호이스팅됨
greet(); // ✅ 작동
function greet() {
console.log("안녕");
}
// const 함수 표현식 — 호이스팅 안 됨
hello(); // ❌ ReferenceError
const hello = () => {
console.log("안녕");
};함수 선언문은 코드 어디서든 호출할 수 있지만, const로 선언한 함수는 선언 이후에만 호출할 수 있습니다. 프로젝트에서는 보통 한 가지 방식으로 통일합니다.
전역 스코프에서의 차이
// 브라우저 환경에서
var globalVar = "나는 var";
let globalLet = "나는 let";
console.log(window.globalVar); // "나는 var" — window 객체에 추가됨
console.log(window.globalLet); // undefined — window에 추가 안 됨var로 선언한 전역 변수는 window 객체의 속성이 됩니다. 이것은 라이브러리 간 이름 충돌의 원인이 됩니다. let과 const는 전역 스코프에서 선언해도 window에 추가되지 않습니다.
흔한 실수와 해결
// 실수 1: const 객체의 내부를 바꾸는 건 합법
const config = { debug: true };
config.debug = false; // ✅ 가능 — 객체 내부 수정
config = {}; // ❌ 불가능 — 변수 재할당
// 실수 2: for문에 const를 쓰면
for (const i = 0; i < 3; i++) { // ❌ i++에서 재할당 에러
console.log(i);
}
// for...of에서는 const 사용 가능 (매 반복 새 바인딩)
for (const item of [1, 2, 3]) { // ✅
console.log(item);
}Object.freeze — 진짜 불변 만들기
const 객체의 내부를 정말로 바꾸지 못하게 하려면:
const config = Object.freeze({
apiUrl: "https://api.example.com",
maxRetries: 3
});
config.apiUrl = "changed"; // 조용히 무시됨 (strict mode에서는 에러)
console.log(config.apiUrl); // "https://api.example.com"Object.freeze()는 객체의 속성 추가/수정/삭제를 막습니다. 단, 얕은 동결(shallow freeze)이라 중첩 객체의 내부는 여전히 변경 가능합니다.
코드를 읽을 때 const가 보이면 "이 값은 안 바뀌는구나"라고 바로 알 수 있습니다. 이 예측 가능성이 프로젝트가 커질수록 중요해집니다.