Hello, let?
ES2015 에는 새로운 변수 선언법이 추가되었다.
예전에는 현재의 스코프에 ‘변수’를 ‘선언’ 하는 방법은 var 와 function 뿐이었다.
1 2 3 4 5 6
| var a = 1; function b() {};
function c() { var a = 3; }
|
이제는 새로운 변수 선언법인 const와 let 이 생겼다. 이 둘은 ES2015에 새로이 추가된 블럭 스코프에 묶이는 변수들이다.
(사실 ES2015 에서는 var 를 쓰는건 문법면에서 추천하지 않고 있다.)
const 는 불변 변수, let은 가변 변수를 선언할 때 쓰이며, 당연히 const 는 선언과 동시에 할당하지 않으면 TypeError가 발생한다.
기존에는 변수를 특정한 스코프에 묶으려면 이렇게 해야 했다.
1 2 3 4 5 6 7 8
| var hello = "hello";
(function() { var hello = "안녕"; console.log(hello) })();
console.log(hello);
|
하지만 이젠 아주 간단히 해결된다.
1 2 3 4 5 6 7 8 9
| let hello = "hello";
{ let hello = "안녕"; console.log(hello) }
console.log(hello);
|
그리고 const 키워드는 코드를 읽는데도 큰 도움을 준다.
변수의 불변성(immutable) 이라는 건 가독성 뿐 아니라 여러 면에서 장점이 많기 때문이다.
코드 블럭에 남발된 가변 변수의 존재는 자신의 어지간히 머리가 좋지 않은 이상 코드를 읽을 때 방해물이 되기 쉽다.
어떤 때 let 을 사용하고, 어떤때 const를 사용해야 할까
결론부터 이야기하면, 일단 모든 변수에는 const 를 사용하고 본다. 그리고 어쩔 수 없이 가변값을 다루거나 특정 변수가 미래에 변할 가능성이 있을 때만 (좀 더 정확히는 TypeError 발생 시) let 을 사용하면 된다.
실제 const만을 사용하여 코딩해보면 let을 사용할 기회가 많지 않다는 사실에 놀랄 것이다.
혹시 loop 인덱스 변수에는 사용해야 하지 않나요? 라고 물을 수 있는데, ES2015 에서는 loop 마다 새로운 변수 바인딩을 생성하기에 아무 문제가 없다.
1 2 3 4
| const fruit = { '사과': '맛있다', '바나나': '역시맛있다' } for(const key in fruit) { console.log(key, fruit[key]); }
|
루프마다 ‘새로운 바인딩’ 이라는게 중요하다.
그렇다면 바로 다음 코드를 보자. 이건 어떻게 동작할까?
1 2 3 4 5 6 7
| const indexMap = []; for(let i = 0; i < 10; i++) { indexMap.push(function() { return i; }); } console.log( indexMap.map(function(val){ return val(); }) );
|
결과는
반면, 앞선 i 를 var 로 바꾸면, 결과는 완전히 달라진다.
1 2 3 4 5 6 7 8
| const indexMap = []; for(var i = 0; i < 10; i++) { indexMap.push(function() { return i; }); } console.log( indexMap.map(function(val){ return val(); }) );
|
var의 경우는 새로운 스코프 없이 한 변수에 인덱스가 추가되며 바인딩되었고, 결국 indexMap 에 순차적으로 추가한 함수들이 같은 스코프의 var 를 참조하면서(이걸 Closure 라고 부른다) 전부 10이 찍히게 된다.
이걸 막으려면 ES5 에서는
1 2 3 4 5 6 7
| for(var i = 0; i < 10; i++) {
(function(i) { indexMap.push(function() { return i; }); })(i); }
|
라는 다소 보기 좀 불편(?) 한 코드를 사용해서 먼저 함수 스코프로 감싸야 했다.
let과 const를 쓰면 이젠 이런 코드로부터 해방이다~!
아, 혹시 위에 지나간 예제중에 let 을 const로 바꾸면 어떨까 하는 다음과 같은 코드를 생각했다면,
1 2 3
| for(const i = 0; i < 10; i++) { indexMap.push(function() { return i; }); }
|
이것은 동작하지 않는다. 루프마다 새 바인딩이 되는 것은 맞아서 앞의 const i = 0; 부분은 문제가 없지만, i++ 이 부분이 문제가 된다. const 는 불변이기 때문이다.
전역 변수와 전역 프로퍼티
JavaScript 를 어느정도 다뤄본 사람이라면 전역 변수에 대해 여러 생각이 있지만, 공통된 생각은 해롭다는 것에 동감할 것이다.
var 를 사용해서 전역에 변수를 선언할 경우, 전역에 변수를 선언한다. 이건 당연하다.
하지만 여기서 끝나는게 아니라 전역객체의 프로퍼티에도 이 변수가 프로퍼티로 잡힌다.
1 2 3 4 5 6 7 8
| var a = 1; console.log(a); console.log(window.a) 하지만 let과 const는 전역에서 변수를 선언 시 전역 스코프에는 변수를 할당하나, 전역 프로퍼티에는 변수를 할당하지 않는다. 아래 코드를 보자.
const a = 1; console.log(a); console.log(window.a)
|
사실 ES2015의 특성 (모듈 및 블럭 스코프) 과 맞물려 전역객체에 뭔가를 할당할 일은 전혀라고 좋을 정도로 없어져 버렸기에, var는 더욱 쓸 일이 없어졌다.
호이스팅과 TDZ(Temporal Dead Zone)
호이스팅은 간단히 설명하면 이런 현상을 말한다.
1 2 3 4 5 6 7 8 9 10 11
| var a = 1; function test() {
console.log(a); var a = 3;
console.log(a); }
|
ES2015 이전에선 var 기반 변수 선언과 JavaScript의 함수 스코프의 특성으로 현재 스코프의 모든 변수 선언을 실행 전 먼저 선언하고 undefined 를 할당해두는 동작을 했다. 이걸 보통 사람이 코드를 위에서 아래로 읽어나갈 때 변수들이 현재 스코프의 최상단으로 끌어올려진다(Hoisted) 다고 하여 호이스팅이라고 부른다.
let과 const 도 물론 호이스팅이 된다. 하지만 세부 내용은 좀 다르다.
var 의 함수 스코프 단위의 호이스팅이 아닌 블럭 스코프 호이스팅이며, 선언만 할뿐 실행기가 undefined 등을 할당해주는 친절함 따위도 없다. 그리고 그 변수가 완전히 할당되기 전 사용하려 하면 오류가 난다.
코드를 보자
1 2 3 4 5 6
| const a = 1 { console.log(a); const a = 10; console.log(a); }
|
뭔가 1 다음 10이 출력될 것 같지만, 이 코드는 ReferenceError
를 낸다. 일단 코드가 실행되면 바깥 스코프에 a 가 10으로 선언된다. 블럭에 진입해서 새로운 스코프가 만들어지고, 블럭 안의 a 역시 호이스팅되어 바깥 스코프의 a를 가리지만, 아직 이 변수는 사용할 수 없는 상태이다.
이것을 ES2015 에서는 TDZ - Temporal Dead Zone
이라고 부른다.
코드가 실행되서 호이스팅과는 별개로 실제 코드의 선언문을 실행하게 되면 그때서야 변수가 성공적으로 초기화되고 할당 구문이 있다면 값이 할당되고 없다면 undefined 가 할당될 것이다.
특히 ES2015에 새로 추가된 변수 해체나 파라미터 기본값 처리 시 실수의 여지가 있다.
코드를 보자.
1 2 3 4
| function test(a = b, b = 4){ console.log(a,b); } test();
|
이건 실제로는 기본값 할당이 다음과 같이 처리되기에 TDZ 에 따라 ReferenceError 이다.