ES2015 - var, const, let

var, const, let

Hello, let?

ES2015 에는 새로운 변수 선언법이 추가되었다.

예전에는 현재의 스코프에 ‘변수’를 ‘선언’ 하는 방법은 var 와 function 뿐이었다.

1
2
3
4
5
6
var a = 1; // 전역 스코프에 변수 a 선언
function b() {}; // 전역 스코프에 함수 b 선언

function c() {
var a = 3; // 함수 c 스코프에 변수 a 선언
}

이제는 새로운 변수 선언법인 const와 let 이 생겼다. 이 둘은 ES2015에 새로이 추가된 블럭 스코프에 묶이는 변수들이다.
(사실 ES2015 에서는 var 를 쓰는건 문법면에서 추천하지 않고 있다.)

const 는 불변 변수, let은 가변 변수를 선언할 때 쓰이며, 당연히 const 는 선언과 동시에 할당하지 않으면 TypeError가 발생한다.

기존에는 변수를 특정한 스코프에 묶으려면 이렇게 해야 했다.

1
2
3
4
5
6
7
8
var hello = "hello";

(function() { // IIFE function scope
var hello = "안녕";
console.log(hello) // 안녕
})();

console.log(hello); // hello

하지만 이젠 아주 간단히 해결된다.

1
2
3
4
5
6
7
8
9
let hello = "hello";

// block scope
{
let hello = "안녕";
console.log(hello) // 안녕
}

console.log(hello); // 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(); })
);

결과는

1
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

반면, 앞선 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(); })
);
// [10, 10, 10, 10, 10, 10, 10, 10, 10, 10]

var의 경우는 새로운 스코프 없이 한 변수에 인덱스가 추가되며 바인딩되었고, 결국 indexMap 에 순차적으로 추가한 함수들이 같은 스코프의 var 를 참조하면서(이걸 Closure 라고 부른다) 전부 10이 찍히게 된다.

이걸 막으려면 ES5 에서는

1
2
3
4
5
6
7
for(var i = 0; i < 10; i++) {

// 새 스코프를 생성하기 위해 즉시 실행 함수 호출(IIFE) 로 둘러싼다
(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); // 1
console.log(window.a) // 1. 전역 객체(window) 에 프로퍼티로 자동으로 잡혔다
하지만 letconst는 전역에서 변수를 선언 시 전역 스코프에는 변수를 할당하나, 전역 프로퍼티에는 변수를 할당하지 않는다. 아래 코드를 보자.

const a = 1;
console.log(a); // 1
console.log(window.a) // undefined. 전역 객체(window) 에 프로퍼티로 잡히지 않는다.

사실 ES2015의 특성 (모듈 및 블럭 스코프) 과 맞물려 전역객체에 뭔가를 할당할 일은 전혀라고 좋을 정도로 없어져 버렸기에, var는 더욱 쓸 일이 없어졌다.

호이스팅과 TDZ(Temporal Dead Zone)
호이스팅은 간단히 설명하면 이런 현상을 말한다.

1
2
3
4
5
6
7
8
9
10
11
var a = 1;
function test() {

// undefined. 코드 실행 전 함수 내부 스코프의 a가 먼저 선언되고 undefined 상태가 된다.
// 상위 스코프의 a는 shadow.
console.log(a);
var a = 3;

// 이제야 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(); // ReferenceError.

이건 실제로는 기본값 할당이 다음과 같이 처리되기에 TDZ 에 따라 ReferenceError 이다.

1
2
let a = b; // TDZ!
let b = 4;
javarouka 의 기술블로그
비동기와 Promise #1
Handlebars (for Java) 서버, 클라이언트 동시에 사용하기