JavaScript var let const 차이
JavaScript에서 변수를 선언할 때 var, let, const를 사용할 수 있다.
셋 다 이름과 값을 연결하지만 스코프, 재할당, 재선언 규칙이 다르다.
새 코드를 작성할 때는 보통 const를 기본으로 사용한다.
값을 다시 할당해야 할 때만 let을 사용한다.var는 기존 코드를 읽거나 오래된 실행 환경을 다룰 때 주로 만난다.
이 기준을 제대로 적용하려면 “값이 바뀌는가”와 “변수가 보이는 범위가 어디인가”를 함께 봐야 한다.
변수 선언의 기본 형태
세 키워드의 기본 문법은 비슷하다.
var legacyName = '현록'
let page = 1
const siteUrl = 'https://hyun.pro'var와 let으로 선언한 변수에는 나중에 다른 값을 다시 할당할 수 있다.
let page = 1
page = 2const로 선언한 binding에는 다른 값을 다시 할당할 수 없다.
const siteUrl = 'https://hyun.pro'
siteUrl = 'https://example.com' // TypeError여기서 const가 보호하는 것은 변수 이름과 값의 연결이다.
객체 내부까지 자동으로 불변이 되는 것은 아니다.
스코프의 의미
스코프는 변수에 접근할 수 있는 코드 범위다.
함수 안에서만 보이는 변수가 있고, 중괄호 block 안에서만 보이는 변수도 있다.
같은 이름이라도 스코프가 다르면 서로 다른 binding이 될 수 있다.
const message = '바깥'
function printMessage() {
const message = '함수 안'
console.log(message)
}
printMessage() // 함수 안
console.log(message) // 바깥변수 선언 키워드의 가장 큰 차이 가운데 하나가 이 스코프 규칙이다.
var의 함수 스코프
var는 함수 스코프를 가진다.
함수 안에서 선언하면 그 함수 전체에서 접근할 수 있다.
function checkLogin(isLoggedIn) {
if (isLoggedIn) {
var message = '로그인 상태'
}
console.log(message)
}message는 if block 안에서 선언됐지만 함수의 나머지 영역에서도 보인다.
중괄호가 var의 스코프를 만들지 않기 때문이다.
for (var i = 0; i < 3; i += 1) {
console.log(i)
}
console.log(i) // 3반복문 밖에서도 i에 접근할 수 있다.
작은 block 안에서만 쓰려던 변수가 바깥으로 새어 나가므로 코드가 커질수록 실수 가능성이 높아진다.
let의 블록 스코프
let은 block 스코프를 가진다.if, for, 함수처럼 중괄호로 만든 block 안에서 선언하면 그 block 안에서만 접근할 수 있다.
function checkLogin(isLoggedIn) {
if (isLoggedIn) {
let message = '로그인 상태'
console.log(message)
}
console.log(message) // ReferenceError
}message는 if block 바깥에서 보이지 않는다.
변수의 생명 범위가 실제 사용 범위와 가까워진다.
반복문에서도 loop variable이 밖으로 새지 않는다.
for (let i = 0; i < 3; i += 1) {
console.log(i)
}
console.log(i) // ReferenceErrorlet은 block 안에서 값이 여러 번 바뀌어야 할 때 적합하다.
let total = 0
for (const price of [1000, 2000, 3000]) {
total += price
}total은 계산 과정에서 재할당되므로 let을 사용한다.
각 반복에서 꺼낸 price는 다시 할당하지 않으므로 const를 사용한다.
const의 블록 스코프
const도 let과 같은 block 스코프를 가진다.
차이는 선언한 binding에 다른 값을 재할당할 수 없다는 점이다.
if (true) {
const status = 'ready'
console.log(status)
}
console.log(status) // ReferenceErrorconst는 선언할 때 초기값이 반드시 필요하다.
const status // SyntaxError나중에 값을 넣을 계획이라면 let을 사용해야 한다.
let status
if (isReady) {
status = 'ready'
} else {
status = 'pending'
}가능하다면 조건식의 결과를 바로 const에 할당해 선언과 초기화를 붙여 둘 수도 있다.
const status = isReady ? 'ready' : 'pending'재할당의 차이
재할당은 이미 선언한 변수 이름에 다른 값을 연결하는 일이다.
var와 let은 재할당할 수 있다.
var count = 0
count = 1
let page = 1
page = 2const는 재할당할 수 없다.
const limit = 10
limit = 20 // TypeError재할당이 필요한지는 변수의 실제 사용을 보고 판단한다.
값이 변할 가능성이 있다는 막연한 이유만으로 모두 let으로 선언할 필요는 없다.
현재 코드에서 다시 할당하지 않는다면 const가 의도를 더 분명하게 드러낸다.
재선언의 차이
재선언은 같은 스코프에서 같은 이름으로 선언문을 다시 쓰는 일이다.
var는 같은 스코프에서 재선언을 허용한다.
var name = '현록'
var name = 'hyun'코드가 길어지면 실수로 같은 이름을 선언해도 오류가 나지 않을 수 있다.
let과 const는 같은 스코프에서 같은 이름을 다시 선언할 수 없다.
let page = 1
let page = 2 // SyntaxErrorconst site = 'hyun.pro'
const site = 'example.com' // SyntaxError다른 block에서는 같은 이름을 새로 선언할 수 있다.
const status = 'outer'
{
const status = 'inner'
console.log(status) // inner
}
console.log(status) // outer두 변수는 스코프가 다르므로 서로 다른 binding이다.
const 객체의 변경
const는 객체를 불변으로 만들지 않는다.
객체를 가리키는 binding의 재할당만 막는다.
const user = {
name: '현록',
}
user.name = 'hyun'
user.role = 'admin'property 변경과 추가는 가능하다.
변수 자체에 다른 객체를 연결하는 것은 불가능하다.
const user = {
name: '현록',
}
user = {
name: 'hyun',
} // TypeError배열도 같은 규칙을 따른다.
const posts = []
posts.push({ id: 1, title: '첫 글' })
posts.push({ id: 2, title: '둘째 글' })배열 내부는 바뀌지만 posts binding은 같은 배열을 계속 가리킨다.
객체 자체를 바꾸지 못하게 만들고 싶다면 불변 데이터 처리 방식이나 Object.freeze() 같은 별도 도구가 필요하다.const만으로 깊은 불변성을 기대하면 안 된다.
호이스팅과 초기화 차이
var, let, const 모두 선언 정보가 코드 실행 전에 처리된다.
하지만 선언 전에 접근했을 때의 결과가 다르다.
var는 선언 전에 읽으면 undefined가 나온다.
console.log(count) // undefined
var count = 1개념적으로 선언이 함수 스코프 시작에서 만들어지고 undefined로 초기화된 뒤, 원래 코드 위치에서 값이 할당된다.
let과 const는 block 시작부터 binding이 존재하지만 선언문에 도달하기 전에는 접근할 수 없다.
console.log(page) // ReferenceError
let page = 1console.log(limit) // ReferenceError
const limit = 10block 시작부터 초기화 지점까지를 temporal dead zone 또는 TDZ라고 부른다.
let과 const가 호이스팅되지 않는다고 간단히 설명하기도 한다.
더 정확히는 선언이 처리되지만 초기화 전에 접근할 수 없다고 이해하는 편이 좋다.
가장 안전한 습관은 변수를 사용하기 전에 가까운 위치에서 선언하는 것이다.
반복문과 closure 차이
var와 let의 차이는 비동기 callback이 있는 반복문에서 선명하게 드러난다.
for (var i = 0; i < 3; i += 1) {
setTimeout(() => {
console.log(i)
}, 0)
}callback이 실행될 때 반복문은 이미 끝났고 같은 i binding의 값은 3이다.
그래서 3이 세 번 출력된다.
let을 사용하면 반복마다 새로운 binding이 만들어진다.
for (let i = 0; i < 3; i += 1) {
setTimeout(() => {
console.log(i)
}, 0)
}결과는 0, 1, 2다.
각 callback이 자기 반복의 i를 기억한다.
비동기 실행 순서와 Promise 병렬 처리 기준은 Promise 다루기에서 더 확인할 수 있다.
전역 스코프의 차이
브라우저의 일반 script 최상위에서 선언한 var는 전역 객체의 property가 될 수 있다.
var legacyValue = 1
console.log(window.legacyValue) // 1최상위 let과 const는 전역 binding을 만들지만 window property로 추가되지 않는다.
let page = 1
const limit = 10
console.log(window.page) // undefined
console.log(window.limit) // undefined환경과 script 종류에 따라 전역 처리 방식이 달라질 수 있다.
ES module에서는 각 module이 자체 scope를 가지므로 일반 script의 전역 동작과 같다고 가정하면 안 된다.
전역 변수 자체를 줄이고 module 단위로 의존성을 명시하는 편이 충돌을 예방하기 쉽다.
선언 위치와 script 실행
변수 선언 규칙을 이해해도 JavaScript 파일이 DOM보다 먼저 실행되면 요소를 찾지 못할 수 있다.
const button = document.querySelector('#save-button')이 코드가 HTML button보다 먼저 실행되면 button은 null일 수 있다.
이는 const의 문제가 아니라 script 실행 시점의 문제다.
defer, async, 문서 파싱 순서의 차이는 script async와 defer 차이에서 정리했다.
변수 선언 방식과 코드가 실행되는 시점을 분리해서 보면 원인을 찾기 쉽다.
var를 만나는 경우
새 프로젝트에서 var를 선택할 이유는 많지 않다.
block 스코프가 없고 같은 스코프의 재선언을 허용해 변수 범위를 예측하기 어렵기 때문이다.
그래도 오래된 코드나 ES2015 이전 환경을 지원하는 build 결과에서는 var를 볼 수 있다.
기존 함수를 수정할 때 var를 무조건 let이나 const로 기계적으로 바꾸면 동작이 달라질 수 있다.
다음 항목을 확인하고 변경해야 한다.
- block 바깥에서 해당 변수를 사용하는지 확인한다.
- 같은 이름을 재선언하는 코드가 있는지 확인한다.
- 반복문 callback이 하나의 binding을 공유하는지 확인한다.
- 전역 객체 property에 기대는 코드가 있는지 확인한다.
- 선언 전에 변수에 접근하는 코드가 있는지 확인한다.
동작을 파악한 뒤 작은 범위에서 바꾸고 테스트하는 편이 안전하다.
const를 기본으로 쓰는 이유
const는 이 변수 이름이 다른 값으로 재할당되지 않는다는 의도를 보여준다.
코드를 읽는 사람은 값의 출처를 추적할 때 재할당 지점을 찾지 않아도 된다.
const response = await fetch('/api/posts')
const posts = await response.json()response와 posts binding을 다시 할당하지 않는다면 const가 자연스럽다.
객체나 배열 내부를 변경할 수 있다는 이유만으로 let을 사용할 필요는 없다.
판단 기준은 binding을 다른 값으로 다시 연결하는지다.
let을 사용하는 기준
계산 과정에서 같은 변수에 값을 누적하거나 조건에 따라 나중에 값을 넣어야 하면 let을 사용한다.
let total = 0
for (const item of cart) {
total += item.price
}let message
if (response.ok) {
message = '저장했습니다.'
} else {
message = '저장하지 못했습니다.'
}함수형 표현이나 조건식으로 재할당을 줄일 수 있다면 const로 바꿀 수도 있다.
하지만 억지로 복잡한 표현을 만들기보다 읽기 쉬운 let을 사용하는 편이 나을 때도 있다.
자주 생기는 실수
const 객체의 property도 바꿀 수 없다고 생각하지 않는다.
값을 다시 할당하지 않는데 습관적으로 let을 사용하지 않는다.
var가 if와 for block 밖에서도 보인다는 점을 놓치지 않는다.
let과 const를 선언 전에 읽으면 undefined가 아니라 ReferenceError가 발생한다.
같은 스코프에서 let이나 const를 재선언하지 않는다.
반복문 callback에서 var가 하나의 binding을 공유할 수 있다는 점을 확인한다.
선택 기준
다시 할당하지 않는 값은 const로 선언한다.
const apiUrl = '/api/posts'계산 과정이나 상태 변화로 다시 할당해야 하면 let을 사용한다.
let currentPage = 1
currentPage += 1새 코드에서는 특별한 호환성 이유가 없다면 var 사용을 피한다.
기존 var 코드를 바꿀 때는 스코프, 재선언, 호이스팅, 전역 객체 의존성을 먼저 확인한다.
정리
var는 함수 스코프를 가지며 같은 스코프에서 재선언과 재할당이 가능하다.
block 밖으로 변수가 보일 수 있고 선언 전에 읽으면 undefined가 나온다.
let은 block 스코프를 가지며 재할당은 가능하지만 같은 스코프의 재선언은 불가능하다.
선언 전에 접근하면 TDZ 때문에 ReferenceError가 발생한다.
const는 block 스코프를 가지며 재할당과 같은 스코프의 재선언이 불가능하다.
객체와 배열 내부는 변경할 수 있으므로 const가 깊은 불변성을 뜻하지는 않는다.
새 코드에서는 const를 기본으로 두고, binding을 다시 할당할 때만 let을 쓰는 기준이 가장 단순하다.
참고 자료










