블로그 내 검색

로드 중입니다...

2016. 3. 31.

ES2015 - var, const, let

Hello, let?

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

예전에는 현재의 스코프에 '변수'를  '선언' 하는 방법은 varfunction 뿐이었다.
var a = 1; // 전역 스코프에 변수 a 선언
function b() {}; // 전역 스코프에 함수 b 선언

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

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

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

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

console.log(hello); // hello
하지만 이젠 아주 간단히 해결된다.
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 마다 새로운 변수 바인딩을 생성하기에 아무 문제가 없다.
const fruit = { '사과': '맛있다', '바나나': '역시맛있다' }
for(const key in fruit) {
  console.log(key, fruit[key]);
}
루프마다 '새로운 바인딩' 이라는게 중요하다.

그렇다면 바로 다음 코드를 보자. 이건 어떻게 동작할까?
const indexMap = [];
for(let i = 0; i < 10; i++) {
  indexMap.push(function() {  return i; });
}
console.log(
  indexMap.map(function(val){ return val(); })
);
결과는
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
반면, 앞선 i 를 var 로 바꾸면, 결과는 완전히 달라진다.
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 에서는
for(var i = 0; i < 10; i++) {

  // 새 스코프를 생성하기 위해 즉시 실행 함수 호출(IIFE) 로 둘러싼다
  (function(i) {
    indexMap.push(function() {  return i; });
  })(i);
}
라는 다소 보기 좀 불편(?) 한 코드를 사용해서 먼저 함수 스코프로 감싸야 했다.

let과 const를 쓰면 이젠 이런 코드로부터 해방이다~!

아, 혹시 위에 지나간 예제중에 let 을 const로 바꾸면 어떨까 하는 다음과 같은 코드를 생각했다면,
for(const i = 0; i < 10; i++) {
  indexMap.push(function() {  return i; });
}
이것은 동작하지 않는다. 루프마다 새 바인딩이 되는 것은 맞아서 앞의 const i = 0;  부분은 문제가 없지만, i++  이 부분이 문제가 된다. const 는 불변이기 때문이다.

전역 변수와 전역 프로퍼티

JavaScript 를 어느정도 다뤄본 사람이라면 전역 변수에 대해 여러 생각이 있지만, 공통된 생각은 해롭다는 것에 동감할 것이다.
var 를 사용해서 전역에 변수를 선언할 경우, 전역에 변수를 선언한다. 이건 당연하다.

하지만 여기서 끝나는게 아니라 전역객체의 프로퍼티에도 이 변수가 프로퍼티로 잡힌다.
var a = 1;
console.log(a); // 1
console.log(window.a) // 1. 전역 객체(window) 에 프로퍼티로 자동으로 잡혔다
하지만 let과 const는 전역에서 변수를 선언 시 전역 스코프에는 변수를 할당하나, 전역 프로퍼티에는 변수를 할당하지 않는다. 아래 코드를 보자.
const a = 1;
console.log(a); // 1
console.log(window.a) // undefined. 전역 객체(window) 에 프로퍼티로 잡히지 않는다.

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

호이스팅과 TDZ(Temporal Dead Zone)

호이스팅은 간단히 설명하면 이런 현상을 말한다.
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 등을 할당해주는 친절함 따위도 없다. 그리고 그 변수가 완전히 할당되기 전 사용하려 하면 오류가 난다.

코드를 보자
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에 새로 추가된 변수 해체파라미터 기본값 처리 시 실수의 여지가 있다.

코드를 보자.
function test(a = b, b = 4){
  console.log(a,b);
}
test(); // ReferenceError.
이건 실제로는 기본값 할당이 다음과 같이 처리되기에 TDZ 에 따라 ReferenceError 이다.
let a = b; // TDZ!
let b = 4;

결론

ES5 도 힘겨운 마당에 세상은 더이상 프로그래머들을 기다리지 않고 ES2015 로 달려가고 있다.

최신 모던 브라우저만 지원하는 문제가 있지만, Babel등의 트랜스컴파일러를 사용하면 이미 실무 레벨에서 사용할 정도의 코드가 나오긴 한다.

ES6 호환테이블 by Kangax

확실한건, ES5로는 ES2015의 모든 기능을 에뮬레이션 하기엔 무리(Promise의 Job Queue라든가 Symbol 등) 에다가 세세한 부분은 구현이 불가능해서 어느정도 비슷한 정도만 구현된 수준이기에 Babel Transfile 로는 한계가 분명 있다는건 잊지 말자.