var의 빈자리를 채운 let·const, 블록 레벨 스코프 정리
"모던 자바스크립트 Deep Dive"를 읽으면서 var의 문제점과 ES6에서 들어온 let·const를 정리했다. var를 거의 쓰지 않게 된 이유가 책 한 챕터 안에 다 모여 있어서, 한 번 정리해두고 싶었다.
var가 다른 언어와 어긋나는 지점
var 변수는 다른 언어 사용자가 보면 의아한 동작을 몇 가지 안고 있다. 책에서 짚는 건 크게 세 가지였다.
같은 이름으로 다시 선언해도 통과되는 변수
var는 같은 이름으로 다시 선언해도 에러가 나지 않는다. 동작이 갈리는 기준은 재선언문에 초기화문이 붙어 있는지 여부였다.
초기화문이 붙은 경우: 자바스크립트 엔진이 var 키워드가 없는 것처럼 처리한다. 즉 단순히 새 값으로 다시 할당된다.
초기화문이 없는 경우: 기존 변수의 값을 유지한다. 선언 자체는 호이스팅 단계에서 이미 처리됐기 때문에, 실행 단계에서 초기화문 없는 재선언은 값에 영향을 주지 않는다. 에러도 발생하지 않는다.
var x = 1;
var y = 1;
var x = 100;
var y;
console.log(x); // 100
console.log(y); // 1같은 이름으로 이미 var 변수가 있다는 걸 모른 채 다시 선언하면서 값까지 할당하면, 기존에 쓰던 값을 의도치 않게 덮어버리게 된다. 이게 var의 첫 번째 함정이다.
함수만 스코프로 인정한다는 함정
var는 함수의 코드 블록만 지역 스코프로 인정한다. if, for, while 같은 블록은 스코프로 보지 않는다.
그래서 함수 외부에서 선언한 var 변수는 코드 블록 내에서 선언했다고 하더라도, 스크립트 모드에서는 모두 전역 변수가 된다. 모듈 모드(ES 모듈)에서는 모듈 스코프에 갇혀 전역 객체 프로퍼티가 되지 않는다.
함수 안에서는 전역 변수를 참조하는 일이 잦은데, 거기서 같은 이름의 var 변수가 끼어들면 전역 변수가 의도치 않게 바뀌어 버린다.
선언이 위로 끌려 올라가는 변수 호이스팅
var는 변수 호이스팅이 일어난다. 선언문보다 앞에서 변수를 참조해도 undefined로 평가되어 에러 없이 통과해버린다. 코드가 위에서 아래로 읽히지 않으니 가독성이 떨어지고, 잠재적인 오류 여지를 남긴다.
let이 가져온 변화
var의 단점을 메우기 위해 ES6에서 let과 const가 들어왔다.
같은 스코프에서 중복 선언은 막는다
let으로 같은 이름의 변수를 중복 선언하면 문법 에러가 난다.
var foo = 456;
let bar = 123;
let bar = 456; // SyntaxError: Identifier 'bar' has already been declaredvar의 첫 번째 함정이 여기서 일단 막힌다.
코드 블록이 곧 스코프
let은 함수뿐 아니라 모든 코드 블록(if, for, while, try/catch 등)을 지역 스코프로 인정한다. 블록 레벨 스코프다.
let foo = 1; // 전역 변수
{
let foo = 2; // 지역 변수
let bar = 3; // 지역 변수
}
console.log(foo); // 1
console.log(bar); // ReferenceError: bar is not defined블록 안의 foo는 블록 안에서만 살고, 전역의 foo에 영향을 주지 않는다.
호이스팅이 일어나지 않는 것처럼 보이는 이유
let 변수는 호이스팅이 일어나지 않는 것처럼 보이지만, 실제로는 모든 선언문이 호이스팅된다. 차이는 "선언"과 "초기화"가 분리되어 있다는 점이다.
var는 선언과 초기화가 한 번에 끝난다. 그래서 선언문 이전에 참조해도 undefined가 나온다. 반면 let·const는 선언만 호이스팅 단계에서 처리되고, 초기화는 변수 선언문에 도달했을 때 실행된다. 그 사이 구간이 바로 일시적 사각지대(Temporal Dead Zone, TDZ)다.
두 변수의 생명 주기를 나란히 두면 차이가 분명해진다.
var는 호이스팅 단계에서 선언과 초기화가 동시에 끝나기 때문에 선언문 이전 참조가 undefined로 떨어진다. let·const는 호이스팅 단계에서 선언만 처리되고, 초기화 전까지의 구간(TDZ)에서 참조하면 ReferenceError가 발생한다.
let foo = 1; // 전역 변수
{
console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
let foo = 2; // 지역 변수
}블록 안에서 let foo가 호이스팅됐기 때문에 전역의 foo를 보지 않는다. 그러나 초기화 전이라 참조하면 TDZ 에러가 난다.
전역 변수로 선언해도 window에 붙지 않는다
var로 선언한 전역 변수와 전역 함수, 그리고 암묵적 전역(선언 없이 값을 할당한 변수)은 전역 객체 window의 프로퍼티가 된다. 그래서 window.foo로 접근할 수 있다.
let으로 선언한 전역 변수는 전역 객체의 프로퍼티가 아니다. window.foo로 접근할 수 없다. 보이지 않는 개념적인 블록인 전역 렉시컬 환경의 선언적 환경 레코드 안에 들어간다.
const는 어디까지 막아주는가
const는 상수를 선언하기 위해 도입됐지만, 반드시 상수에만 쓰는 건 아니다. 대부분의 동작은 let과 같고, 두 가지 제약이 더 붙는다.
선언과 초기화는 반드시 같이
const 변수는 선언과 동시에 초기화해야 한다.
const foo = 1;
const bar; // SyntaxError: Missing initializer in const declaration블록 레벨 스코프를 갖는 점, 호이스팅이 일어나지 않는 것처럼 동작하는 점은 let과 같다.
재할당이 막힌다
const 변수는 재할당이 금지된다.
const foo = 1;
foo = 2; // TypeError: Assignment to constant variable.원시 값을 할당한 경우에는 변수 값을 변경할 수 없으니 상수처럼 동작한다. 의미를 분명히 드러내기 위해 상수는 대문자로 적는 관습이 있다.
const TAX_RATE = 0.1;
let preTaxPrice = 100;
let afterTaxPrice = preTaxPrice + (preTaxPrice * TAX_RATE);
console.log(afterTaxPrice); // 110상수를 쓰면 값의 의미가 이름에 묻고, 변경되지 않는 고정값이라는 보장이 생긴다. 같은 값을 여러 곳에서 쓸 때 유지보수가 한곳에서 끝난다.
"재할당 금지"와 "불변"은 다르다
여기서 한 번 헷갈리고 갔다. const는 재할당을 막을 뿐, "불변"을 의미하지는 않는다. 원시 값을 할당했을 때는 결과적으로 값이 바뀌지 않지만, 객체를 할당하면 프로퍼티는 그대로 바꿀 수 있다.
const person = {
name: 'Lee'
};
person.name = 'Kim';
console.log(person); // {name: "Kim"}person 변수가 가리키는 참조 자체를 다른 객체로 바꾸는 건 막혀 있지만, 그 객체 안의 프로퍼티는 자유롭게 변경된다. const를 썼다고 객체가 동결되는 건 아니라는 걸 여기서 확인했다.
셋 중에 무엇을 기본값으로 둘 것인가
책에서는 세 키워드의 권장 사용 순서를 분명히 둔다.
- ES6 이후로는
var는 쓰지 않는다. - 기본적으로
const를 쓴다. 재할당이 필요한 경우에만let을 쓴다. - 변수의 스코프는 가능한 한 좁게 만든다.
const가 기본값인 이유는 의도치 않은 재할당을 막아주기 때문이다. 재할당이 필요하다는 게 분명할 때만 let으로 풀어준다. var의 세 가지 함정(중복 선언, 함수 레벨 스코프, 호이스팅)이 한꺼번에 해결되는 흐름이 머릿속에 정리됐다.