JavaScript var vs let vs const 완벽 비교 | 변수 선언 방식 선택 가이드
이 글의 핵심
JavaScript var, let, const 비교 - 스코프, 호이스팅, 재할당 차이와 선택 기준
들어가며
“var를 써도 되나요?” JavaScript를 배울 때 자주 나오는 질문입니다. 이 글에서는 var, let, const의 차이를 명확히 이해하고, 실전에서 어떤 것을 써야 하는지 선택 기준을 제시합니다.
비유로 말씀드리면, var는 건물 전체가 하나의 방처럼 들리는 스피커, let은 회의실 칸막이, const는 한 번 붙인 이름표를 못 바꾸는 자리에 가깝습니다. 루프·블록 단위로 변수를 나누려면 let과 const가 맞습니다.
언제 let을, 언제 const를 쓰나요? (var는?)
| 관점 | const | let | var |
|---|---|---|---|
| 성능 | 동일 계열에서 차이는 미미 | 동일 | 호이스팅·스코프로 버그 유발이 잦음 |
| 사용성 | 재할당이 없을 때 기본 | 재할당이 필요할 때 | 레거시·특수 호환 외에는 비권장 |
| 적용 시나리오 | 대부분의 바인딩 | 카운터·스왑 등 | 구형 코드 유지 |
이 글을 읽으면
- var, let, const의 스코프 차이를 이해합니다
- 호이스팅이 각각 어떻게 동작하는지 배웁니다
- 재할당과 재선언의 차이를 익힙니다
- 실전에서 어떤 것을 써야 하는지 판단할 수 있습니다
목차
1. 빠른 비교표
| 특성 | var | let | const |
|---|---|---|---|
| 스코프 | 함수 스코프 | 블록 스코프 | 블록 스코프 |
| 호이스팅 | 선언 + 초기화 (undefined) | 선언만 (TDZ) | 선언만 (TDZ) |
| 재할당 | ✅ 가능 | ✅ 가능 | ❌ 불가능 |
| 재선언 | ✅ 가능 | ❌ 불가능 | ❌ 불가능 |
| 전역 객체 | window에 추가 | 추가 안 됨 | 추가 안 됨 |
| 권장 사용 | ❌ 사용 금지 | ✅ 재할당 필요 시 | ✅ 기본 선택 |
2. 스코프 차이
var: 함수 스코프
function testVar() {
if (true) {
var x = 10;
}
console.log(x); // ✅ 10 (블록 밖에서도 접근 가능)
}
testVar();
let/const: 블록 스코프
function testLet() {
if (true) {
let x = 10;
const y = 20;
}
console.log(x); // ❌ ReferenceError: x is not defined
console.log(y); // ❌ ReferenceError: y is not defined
}
실전 문제: 루프 변수
// var의 함정
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 3, 3, 3 (예상: 0, 1, 2)
}, 100);
}
// let으로 해결
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 0, 1, 2 ✅
}, 100);
}
이유: var는 함수 스코프이므로 루프가 끝난 후 i = 3이 됩니다. let은 각 반복마다 새로운 블록 스코프를 생성합니다.
3. 호이스팅
var: 선언 + 초기화 호이스팅
console.log(x); // undefined (에러 아님!)
var x = 10;
// 실제 동작
var x = undefined; // 호이스팅
console.log(x); // undefined
x = 10;
let/const: TDZ (Temporal Dead Zone)
console.log(x); // ❌ ReferenceError: Cannot access 'x' before initialization
let x = 10;
// TDZ: 스코프 시작부터 선언까지의 구간
{
// TDZ 시작
console.log(x); // ❌ ReferenceError
let x = 10; // TDZ 끝
console.log(x); // ✅ 10
}
실전 예제
// var의 문제
function getUser() {
if (!user) {
var user = fetchUser(); // 호이스팅으로 전체 함수 스코프
}
return user; // undefined 반환 가능
}
// let으로 해결
function getUser() {
if (!user) {
let user = fetchUser(); // 블록 스코프
}
return user; // ❌ ReferenceError (의도한 대로 에러)
}
4. 재할당과 재선언
var: 재할당, 재선언 모두 가능
var x = 10;
var x = 20; // ✅ 재선언 가능 (위험!)
x = 30; // ✅ 재할당 가능
let: 재할당 가능, 재선언 불가
let x = 10;
let x = 20; // ❌ SyntaxError: Identifier 'x' has already been declared
x = 30; // ✅ 재할당 가능
const: 재할당, 재선언 모두 불가
const x = 10;
const x = 20; // ❌ SyntaxError
x = 30; // ❌ TypeError: Assignment to constant variable
5. const의 불변성
주의: const는 참조만 불변
// 기본 타입: 완전 불변
const x = 10;
x = 20; // ❌ TypeError
// 객체: 참조는 불변, 내용은 가변
const obj = { name: 'Alice' };
obj = { name: 'Bob' }; // ❌ TypeError (참조 변경 불가)
obj.name = 'Bob'; // ✅ 가능 (내용 변경 가능)
// 배열: 참조는 불변, 요소는 가변
const arr = [1, 2, 3];
arr = [4, 5, 6]; // ❌ TypeError
arr.push(4); // ✅ 가능
arr[0] = 10; // ✅ 가능
완전 불변 객체
// Object.freeze로 완전 불변
const obj = Object.freeze({ name: 'Alice' });
obj.name = 'Bob'; // ❌ 무시됨 (strict mode에서는 TypeError)
// 깊은 불변성 (중첩 객체)
function deepFreeze(obj) {
Object.freeze(obj);
Object.values(obj).forEach(value => {
if (typeof value === 'object' && value !== null) {
deepFreeze(value);
}
});
return obj;
}
const config = deepFreeze({
api: {
url: 'https://api.example.com',
timeout: 5000,
},
});
config.api.url = 'https://evil.com'; // ❌ 무시됨
6. 실전 선택 가이드
기본 원칙
// 1순위: const (기본)
const API_URL = 'https://api.example.com';
const users = [];
// 2순위: let (재할당 필요 시)
let count = 0;
for (let i = 0; i < 10; i++) {
count += i;
}
// 3순위: var (사용 금지!)
// 레거시 코드에서만 볼 수 있음
상황별 선택
| 상황 | 선택 | 이유 |
|---|---|---|
| 상수 | const | 재할당 방지 |
| 루프 변수 | let | 블록 스코프 |
| 카운터 | let | 재할당 필요 |
| 객체/배열 | const | 참조 불변 (내용은 가변) |
| 함수 | const | 재할당 방지 |
| 레거시 | var | 기존 코드 유지보수만 |
ESLint 설정
// .eslintrc.json
{
"rules": {
"no-var": "error", // var 금지
"prefer-const": "warn", // 재할당 없으면 const 권장
"no-const-assign": "error" // const 재할당 금지
}
}
7. 흔한 실수
실수 1: var 재선언
var user = 'Alice';
// ... 100줄 후 ...
var user = 'Bob'; // ✅ 에러 없음 (의도치 않은 재선언!)
// let으로 방지
let user = 'Alice';
let user = 'Bob'; // ❌ SyntaxError
실수 2: 루프 클로저
// var의 함정
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function() {
return i;
});
}
console.log(funcs[0]()); // 3 (예상: 0)
console.log(funcs[1]()); // 3 (예상: 1)
console.log(funcs[2]()); // 3 (예상: 2)
// let으로 해결
for (let i = 0; i < 3; i++) {
funcs.push(function() {
return i;
});
}
console.log(funcs[0]()); // 0 ✅
console.log(funcs[1]()); // 1 ✅
console.log(funcs[2]()); // 2 ✅
실수 3: const 객체 수정 시도
const config = { debug: true };
// ❌ 잘못된 이해
config = { debug: false }; // TypeError
// ✅ 올바른 이해
config.debug = false; // 가능
8. 전역 객체와의 관계
var는 전역 객체에 추가
var globalVar = 'I am global';
console.log(window.globalVar); // 'I am global' (브라우저)
let globalLet = 'I am global';
console.log(window.globalLet); // undefined
const globalConst = 'I am global';
console.log(window.globalConst); // undefined
왜 문제인가?
// 기존 전역 변수 덮어쓰기
var alert = 'oops';
alert('Hello'); // ❌ TypeError: alert is not a function
마무리
JavaScript 변수 선언의 핵심:
- const를 기본으로 사용하세요
- 재할당이 필요하면 let 사용
- var는 절대 사용하지 마세요
- ESLint로 자동 검사 설정
핵심: const > let > var 순서로 고려하고, var는 레거시 코드에서만 보게 될 것입니다.
FAQ
Q1. const를 쓰면 성능이 더 좋나요?
미미한 차이입니다. const의 주요 이점은 의도 표현과 실수 방지입니다.
Q2. 모든 변수를 const로 선언해야 하나요?
재할당이 필요 없다면 const를 사용하세요. 코드 리뷰어가 “이 변수는 바뀌지 않는구나”를 바로 알 수 있습니다.
Q3. 기존 코드의 var를 모두 let/const로 바꿔야 하나요?
점진적으로 바꾸세요. 새 코드는 let/const만 사용하고, 기존 코드는 수정할 때 함께 바꾸면 됩니다.
관련 글
- JavaScript 스코프와 클로저
- JavaScript 호이스팅 완벽 가이드
- JavaScript 모던 문법
실전 체크리스트
변수 선언 체크리스트
- 기본적으로 const 사용
- 재할당 필요 시 let 사용
- var 사용 금지
- ESLint no-var 규칙 활성화
- 객체/배열도 const 사용 (참조 불변)
코드 리뷰 체크리스트
- var 사용 여부 확인
- let인데 재할당이 없으면 const로 변경
- 루프 변수는 let 사용
- 전역 변수 최소화
키워드
JavaScript, var, let, const, 변수 선언, 스코프, 호이스팅, TDZ, 재할당, 블록 스코프, 함수 스코프, 비교