Logo

[자바스크립트] var를 사용할 때 발생하는 문제들

많은 자바스크립트 강좌에서 가급적 var 대신에 let이나 const 키워드를 사용하여 변수를 선언하라고 가이드하고 있는데요.

본 포스팅에서는 var의 어떤 문제점 때문에 그런건지 알아보도록 하겠습니다.

아리송한 코드

먼저 다음 코드를 살펴보도록 하겠습니다. 무엇이 출력이 될지 예상이 되시나요?

var n = 1;
function test() {
  console.log(n);
  var n = 2;
  console.log(n);
}
test();

첫번째 console.log(n)에서 오류가 발생할 것 같기도 하고, 12가 출력될 것 같기도 한데요. 의외로 정답은 다음과 같습니다.

undefined
2

왜 이런 예상치못한 결과가 얻어지는 걸까요? 원인은 var 키워드를 사용할 경우, 변수 Hoisting 현상이 발생하기 때문입니다.

변수 Hoisting이란?

Hoisting이란 var 키워드를 사용하여 변수를 선언 시, 해당 변수가 속한 범위(scope) 최상단으로 올려버리는 현상을 일컽습니다.

그리고 주목할 점은 여기서 속한 범위는 다른 언어처럼 block 레벨이 아니라 function 레벨이라는 점입니다.

function 레벨로 변수 Hoisting 현상이 발생하면 위 코드는 다음과 같이 해석되어 집니다.

var n = 1;
function test() {
  var n; // hosting
  console.log(n);
  n = 2;
  console.log(n);
}
test();

따라서, 첫번째 console.log(n)이 실행될 시점에서는 n에 어떤 값도 할당되지 않았기 때문에 undefined가 출력되었던 것입니다.

예측하기 어려운 코드

var를 사용할 경우, 전반적으로 코드가 어떻게 작동될지 직관적으로 예측하기 어려운 경우가 자주 발생합니다.

console.log(n);

위 코드는 ReferenceError를 일으킵니다. 변수 n이 선언된적이 없기 때문이죠.

console.log(n);
var n;

하지만 아래 코드는 undefined를 출력합니다. 변수 n의 선언이 Hosting되기 때문입니다.

for 문에서 var를 사용할 때 이슈 1

특히, for 문에서 var를 사용하면 더 당황스러운 경우를 겪을 수도 있습니다.

var sum = 0;
for (var i = 0; i < 10; i++) {
  sum += i;
}
console.log("sum:", sum);
console.log("i:", i);
sum: 45
i: 10

위 코드에서 변수 i의 값은 for 루프가 끝난 다음에도 유지가 됩니다. 왜냐하면 위에 설명드린 것 처럼 var로 선언한 변수는 iffor와 같은 block 레벨 아닌 function 레벨에서 범위가 정해지기 때문입니다. 즉, for 문 안에서 var로 선언한 변수도 block을 벗어나서 유효하게 됩니다.

for 문에서 var를 사용할 때 이슈 2

다음 코드는 사용자 아이디를 받아 가짜 사용자를 콜백 함수로 넘기는 비동기 함수입니다. 마치 DB 연동처럼 setTimeout 함수를 이용하여 1초의 지연을 주었습니다.

function findUser(id, cb) {
  setTimeout(function () {
    var fakeUser = {
      id: id,
      name: "Joe#" + id,
      email: id + "@test.com",
    };
    cb(fakeUser);
  }, 1000);
}

setTimeout() 함수에 대한 좀 더 자세한 내용은 관련 포스팅을 참고바랍니다.

그리고 아래 코드는 사용자 아이디의 배열을 받아 여러 사용자의 정보를 출력해주는 함수입니다.

function findUsers(userIds) {
  for (var i in userIds) {
    findUser(userIds[i], function (user) {
      console.log("Found for id,", userIds[i]);
      console.log("=> A user founded:", user);
    });
  }
}

다음과 같이 인자를 넘겨서 위 함수를 실행을 해보면,

findUsers([3, 7, 29, 105]);

다음과 같이 다소 황당한 출력 결과를 얻게 됩니다.

Found for id, 105
=> A user founded: { id: 3, name: 'Joe#3', email: '3@test.com' }
Found for id, 105
=> A user founded: { id: 7, name: 'Joe#7', email: '7@test.com' }
Found for id, 105
=> A user founded: { id: 29, name: 'Joe#29', email: '29@test.com' }
Found for id, 105
=> A user founded: { id: 105, name: 'Joe#105', email: '105@test.com' }

왜 사용자 아이디가 배열의 마지막 원소로 고정이 되어버린 걸까요? 이 역시 var로 선언된 변수는 function scope을 가진다는 점과 비동기 함수의 non blocking 성질을 고려하면 이해할 수 있습니다. for 문에서 각 콜백 함수를 넘길 시점에는 i 값이 달랐겠지만, 각 콜백 함수가 실행될 시점에는 for 루프가 끝나서 i가 배열의 마지막 원소의 인덱스로 변경된 이후입니다.

이 쯤에서 정리해보면, var를 사용하여 변수를 선언하면, 변수 Hosting과 함수 Scope를 고려하면서 코딩을 해야합니다. 따라서 var를 사용하면 전반적으로 코드가 어떻게 작동할지 예상하기 어려운 상황이 발생하기 쉬워 집니다.

다음 포스팅에서는 ES6에서 도입된 letconst 키워드를 사용하여 이러한 문제들을 해결하는 방법들에서 알아보도록 하겠습니다.

참고