let const var에 대한 간단한 정리

2019년 04월 27일

개요

자바스크립트의 단골 면접중에서 유명한 문제가 있다.

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000 * i);
}

위와 같은 코드가 주어지고 출력되는 결과를 에측해보라는 문제이다. 보통 클로저의 개념에 대해서 물어볼때 가장 흔하게 물어보는 코드이다. 하지만 주의해야할 점이 있다. 해당 코드의 for문에서 선언 부분을 var를 사용하지 않고 ES6에서 주로 사용하는 let을 사용하게 되면 결과가 완전히 달라지게 된다. var를 사용한 결과는 5 5 5 5 5이고 let을 사용하게 되면 0 1 2 3 4가 출력된다. var를 사용한 출력된 결과는 친숙했지만 let을 사용해서 나온 결과를 공부하면서 정리한 내용을 바탕으로 해당 포스트를 작성하게 되었다.

var

ES6 이전의 자바스크립트에서는 var 키워드를 사용해서 변수를 정의하게 된다. 하지만 해당 키워드(ES6에서도 var를 사용할 수 있다)의 가장 큰 특징은 함수 레벨 스코프만을 지원한다는 점이다. 즉 해당 함수 블록 내에서 선언된 변수는 함수 블록 내에서만 유효하고, 특이하게도 함수내에서 중첩된 다른 블록들에서 사용된 변수들도 해당 함수 내에서는 사용할 수 있다.

(function fn() {
  var a = 10;
  console.log(b);
  if (true) {
    var b = 20;
  }

  console.log(a);
  console.log(b);
})()

> undefined
> 10
> 20

위 코드를 보면 뭔가 이상한 점을 찾을 수 있다. b라는 변수는 선언되기도 전에 사용했는데 오류를 발생하지 않는다. 어떻게 이런일이 가능할걸까? 이는 var 변수의 라이프사이클을 제대로 이해해야 한다. var를 사용해서 변수를 선언한 함수 블록내로 들어가게 되면 메모리 공간이 해당 변수에게 할당되면서 undefined 값을 가지게 된다. 그리고 해당 변수를 선언한 식(expression)에 다다르게 되면 그때서야 초기값이 설정된다. 만약 초기값을 설정하지 않았다면 그대로 undefined 값을 가진다. 이해가 잘 되지 않는다면 아래의 코드를 참고해보자.

var a = 30;
(() => {
	console.log(a);
	a = 15;
	console.log(a);
	var a = 10;
	console.log(a);
})();

console.log(a);

위 코드의 실행 결과는 30 undefined 15 10 Uncaught ReferenceError: a is not defined이다. 맨 처음 함수가 실행될때 a라는 변수에 메모리가 할당되면서 undefined라는 값으로 초기화된다. 그리고 a = 15가 수행되고 a는 15라는 값을 가진다. 그리고 제일 마지막으로 var a = 10를 최종적으로는 a는 10이라는 값이 할당된다. 마지막으로 IIFE의 스코프가 끝나고 글로벌 스코프에서는 a라는 변수에는 맨 처음에 할당한 30이란 값이 호출된다.

const let

ES6에서 var를 대체하기 위해서 추가된 변수 선언 방법이다. letvar와 거의 비슷하게 동작하지만 변수의 라이프사이클이 완전히 다르다. letconst로 선언된 변수는 블록 레벨 스코프를 가지게 된다. 변수가 선언된 블록 내로 들어오가면 변수에 메모리가 할당되지만 해당 변수는 초기화되지 않는다. 이때 변수가 선언된 곳보다 먼저 변수에 접근하거나 값을 바꾸려고 하면 ReferenceError가 발생한다. 이렇게 선언된 변수들을 이와 같이 선언된 곳보다 먼저 접근하거나 할당하려고 하면 에러가 발생하는데 이를 TDZ라고 합니다. 그리고 변수가 선언된 곳에서 해당 변수를 값을 할당시키게 되며 값을 할당하지 않는다면 해당 변수는 undefined 값을 가진다.

constlet과 거의 비슷하게 동작한다. 하지만 const를 사용해 선언된 변수는 재할당이 불가능하며 선언과 동시에 값을 할당해 주어야 한다. 그리고 할당된 값을 immutable하도록 만들어 주진 않는다. 잘 이해가 안된다면 아래 예제를 살펴보자.

const obj = {};
obj.a = 10;
obj.b = 20;
console.log(obj); > {a: 10, b: 20}

const obj2 = {};
obj2 = obj > Uncaught TypeError: Assignment to constant variable.

만약 obj를 immutable하게 만들고 싶다면 Object.freeze()을 이용한다.

정리하자면 아래 표와 같다.

  • var는 함수내에서 호이스팅이 일어나며 함수 단위의 스코프를 가진다. 또한 특이하게도 global property를 생성한다.
  • letconst는 블록 단위의 스코프를 가지며 TDZ로 들어가게 된다. 또한 글로벌 프로퍼티를 생성하지 않는다.

var and let in loop

varlet에 대한 설명을 쭉 서술했지만 아직 처음 질문은 제대로 설명되지 않는다. 개요에 있는 코드를 정확히 이해하려면 바벨을 사용해서 코드를 트렌스파일 해보면 이해가 쉽다.

for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000 * i);
}

위 코드를 변환하게 되면 아래와 같다.

var _loop = function _loop(i) {
  setTimeout(function () {
    console.log(i);
  }, 1000 * i);
};

for (var i = 0; i < 5; i++) {
  _loop(i);
}

즉 각각의 반복문안에서 하나의 스코프가 새롭게 바인딩된다. 즉 setTimeout에서 출력되는 변수 i는 각각이 다른 스코프를 가지고 있기 때문에 우리의 의도(?)대로 출력되지 않았다. 헤당 내용에 대한 스펙은 여기서 찾아볼 수 있다.

정리

  1. var 대신에 letconst를 사용하자. 서로가 모두 다르게 동작한다.
  2. let을 사용해서 for 반복문을 수행할 경우 각각의 반복마다 새로운 스코프가 바인딩된다.
  3. 다 알고 있던 코드라도 조금씩 바꿔서 다시 공부해보자.