자바스크립트에서 변수를 선언할 때 사용하는 var, let, const의 차이와 스코프, 호이스팅에 대해 알아봅시다.

요약 정리

성질 var let
스코프 가장 가까운 function 내부 가장 가까운 중괄호 내부
호이스팅 호이스팅됨 호이스팅되지 않음
IE 지원 모든 버전 IE11 (버그 존재1)

스코프

스코프(scope)란 중괄호({})로 둘러쌓인 코드의 영역을 뜻합니다. 반드시 중괄호만 스코프를 형성하는 것은 아닙니다. function의 경우 괄호 안에 존재하는 매개변수도 스코프에 포함됩니다. iffor는 한 줄의 코드만 가질 경우 중괄호 없이 사용할 수도 있습니다.

스코프 안에서 선언된 변수는 스코프 바깥에서 사용할 수 없습니다. 다만 let이냐 var냐에 따라 기준이 되는 스코프가 다릅니다.

let: 자신으로부터 가장 가까운 블록 스코프 안에서만 사용할 수 있습니다. 블록 스코프는 function, if, for, while, switch 등 자바스크립트의 모든 스코프를 의미합니다. 별로 쓸 일은 없긴 하지만, 자바스크립트에서는 그냥 중괄호만 써도 블록 스코프를 형성할 수 있습니다.

var: 자신으로부터 가장 가까운 function 스코프 안에서만 사용할 수 있습니다. 함수의 중괄호를 벗어나면 더 이상 그 변수를 사용할 수 없습니다. function 스코프 이외의 스코프에는 영향을 받지 않기 때문에, 같은 function이기만 하면 iffor 안에서 선언된 변수를 밖에서 사용하는 것도 가능합니다.

let, var 둘 다 맨 바깥(전역)에 존재한다면 어디서든 사용할 수 있습니다.

스코프 예제

function 내부에 선언된 var는 바깥에서 사용할 수 없습니다. let도 마찬가지입니다:

function func() {
  let letVariable = 123;
  var varVariable = 456;
}
console.log(letVariable); // 오류: ReferenceError
console.log(varVariable); // 오류: ReferenceError

if 내부에 선언된 varif 바깥에서도 사용할 수 있습니다 (여기서 var는 맨 바깥(전역)에 속합니다):

if (100 > 50) {
  var varVariable = 456;
}
console.log(varVariable); // 출력: 456

for의 괄호 부분(초기식)에서 let으로 선언한 변수는 for 스코프에 속합니다. for 바깥에서 사용할 수 없습니다:

for (let i = 0; i < 3; i++) {
  console.log(i); // 출력: 0 1 2
}
console.log(i); // 오류: ReferenceError

for의 괄호 부분(초기식)에서 var로 선언한 변수는 for 스코프가 아니라 맨 바깥(전역) 스코프에 속합니다. 그러므로 for 바깥에서도 사용할 수 있습니다:

for (let i = 0; i < 3; i++) {
  console.log(i); // 출력: 0 1 2
}
console.log(i); // 출력: 3

for에서 var를 사용하면 안 되는 이유

다음 코드는 i를 출력하는 익명 함수를 3개 보관합니다:

var functions = [];

for (var i = 0; i < 3; i++) { // var 사용
  functions.push(function () {
    console.log(i);
  });
}

functions[0](); // 출력: 3
functions[1](); // 출력: 3
functions[2](); // 출력: 3

언뜻 보기에는 0 1 2가 출력될 것처럼 보입니다. 정말 그럴까요? 아닙니다. 실제로 실행해보면 3 3 3이 출력됩니다.

반면 var 대신 let을 경우 예상했던대로 0 1 2가 출력됨을 확인할 수 있습니다:

var functions = [];

for (let i = 0; i < 3; i++) { // let 사용
  functions.push(function () {
    console.log(i);
  });
}

functions[0](); // 출력: 0
functions[1](); // 출력: 1
functions[2](); // 출력: 2

이런 이상한 결과가 나타나는 이유는, 변수를 사용할 때 독특한 절차를 거치기 때문입니다.

먼저, 함수는 함수가 정의될 때의 변수 자체만 기억하고 있을 뿐 실제 변수로부터 값을 꺼내오지는 않습니다.

var i: ivar로 선언되었으므로 function 스코프에 하나만 존재합니다. 모든 익명 함수는 function 스코프에 존재하는 하나의 i만을 가리킬 것입니다.

let i: for 문의 초기식(for (a; b; c)a 부분)에 존재하는 let의 경우 특수한 작용이 일어납니다. 이 작용은 for 루프가 돌 때마다 새로운 i를 만들고, 여기에 이전 i의 값을 대입합니다2. 이로 인해 각각의 익명 함수는 저마다 다른 i를 가리킬 것입니다.

let의 이 특수한 작용은 오직 for 문의 초기식 안에서만 일어납니다. 다음 코드는 i가 하나만 존재하므로 var와 똑같이 3 3 3이 출력됩니다:

var functions = [];

let i = 0; // for 문 바깥에서 let 사용
for (; i < 3; i++) { // 초기식을 비워 둠
  functions.push(function () {
    console.log(i);
  });
}

functions[0](); // 출력: 3
functions[1](); // 출력: 3
functions[2](); // 출력: 3

(여기서는 let을 통해 동일한 이름의 변수를 여러 개 만드는 식으로 해결했습니다. let을 사용하는 것 말고도 익명 함수를 사용해 강제로 var를 여러 개 만들어 해결하는 방법도 있습니다.)

호이스팅

변수 선언 이전에도 변수를 사용할 수 있는 현상을 호이스팅(hoisting)이라 합니다.

마치 변수를 스코프의 맨 위로 끌어올리는 것 같다고 하여, 영어로 ‘끌어올리기’라는 뜻을 가진 호이스팅(hoisting)이라는 이름이 붙었습니다.

let: 호이스팅이 일어나지 않습니다. let 선언 이전에 let으로 선언된 변수를 사용하는 것은 불가능합니다.

var: 호이스팅이 일어납니다. var 선언 전에도 스코프 안이라면(함수 안이라면) var로 선언된 변수를 사용할 수 있습니다.

호이스팅 예제

letlet이 등장하기 전에 사용할 수 없습니다:

console.log(letVariable); // 오류: ReferenceError
let letVariable;
letVariable = 123;
console.log(letVariable); // 출력: 123

varvar가 등장하기 전에도 사용할 수 있습니다:

console.log(varVariable); // 출력: undefined
var varVariable;
varVariable = 456;
console.log(varVariable); // 출력: 456

주의: 값 초기화

호이스팅으로 인해 값까지 초기화가 이루어지는 건 아닙니다.

var varVariable = 456이라고 할 경우, 내부적으로 var varVariable 부분만 함수의 맨 위로 끌어올려지고 variable = 456은 그 자리에 그대로 남는 식으로 동작합니다:

console.log(varVariable); // 출력: undefined
var varVariable = 456;
console.log(varVariable); // 출력: 456

호이스팅이 일어나는 이유

왜 호이스팅이 일어날까요? 호이스팅이 있든 없든간에 어차피 변수를 사용하지 못하는 건 똑같은데 말이죠.

우선 알아두어야 하는 사실이 있습니다. var로 선언한 변수뿐만 아니라, 함수 역시 호이스팅됩니다. 그리고 이 호이스팅이라는 기능은 함수를 순서에 상관 없이 선언할 수 있도록 해줍니다.

함수 호이스팅이 없다면 A 함수에서 B 함수를 호출할 때 B 함수가 코드 순서 상 반드시 먼저 나와야 합니다. 반대로 A함수가 먼저 나오게 되면 B함수를 찾을 수 없으므로 오류가 나오게 됩니다. 호이스팅으로 인해 모든 함수 선언이 코드의 첫부분에 존재하는 것처럼 여겨지므로 우리는 코드 순서에 상관 없이 함수를 선언할 수 있습니다3.

함수 호이스팅이 없다면 다음 코드는 제대로 동작하지 않을 것입니다:

function a() {
  b();
}
function b() {}

호이스팅은 변수가 아니라 함수를 위해 존재하는 기능입니다. 다만 초기 자바스크립트를 설계할 당시 var로 변수를 만드나 function으로 함수를 선언하나 그냥 대충 뭉뚱그려 처리했기에, var를 통한 변수 선언도 호이스팅이 일어나게 되었습니다4. 이러한 var의 호이스팅 문제로 인해 새로운 버전의 자바스크립트는 let을 도입합니다.

IE 지원

IE10(인터넷 익스플로러 10) 이하는 let을 지원하지 않습니다. IE11에서 let을 지원하기는 하지만, for 문에서 루프를 돌 때마다 변수가 만들어지지 않는 치명적인 문제1가 있습니다.

IE에서 let을 제대로 사용하기 위해서는 letvar로 바꿔주는 변환기가 필요합니다. 유명한 변환기로 바벨(Babel)이 있습니다. 온라인에서 직접 변환해보세요.

letconst의 차이

letconst는 변수냐 상수냐의 차이를 제외하고는 완전히 같습니다. let은 값을 바꿀 수 있지만 const는 그럴 수 없습니다.

다만 객체나 배열같이 값 스스로가 변형될 수 있는 경우 const라 할지라도 값이 변형될 수 있습니다. const foo = {}라고 해도 foo.abc = "Hi!"는 허용됩니다. 완전히 값을 변경할 수 없게 하기 위해서는 Immutable.js같은 라이브러리를 사용해야 합니다.

참고

  1. https://caniuse.com/#feat=let

    let variables are not bound separately to each iteration of for loops

     2

  2. http://www.ecma-international.org/ecma-262/6.0/#sec-createperiterationenvironment

    1.d. 새 루프 스코프(전문 용어로 Lexical Environment)를 생성하고, 1.e. let 또는 const로 선언된 변수들에 대하여, 1.e.iii. 이전 루프 스코프로부터 변수를 받아온 뒤, 1.e.v. 새 루프 스코프에 넣습니다. 

  3. https://twitter.com/brendaneich/status/33403701100154880

    자바스크립트의 제작자 Brendan Eich의 트윗

    function declaration hoisting is for mutual recursion & generally to avoid painful bottom-up ML-like order

  4. https://twitter.com/brendaneich/status/562313394431078400

    자바스크립트의 제작자 Brendan Eich의 트윗

    A bit more history: var hoisting was an implementation artifact. function hoisting was better motivated: …