자바스크립트의 변수 범위 및 호이스팅

변수 범위는 변수가 존재하는 컨텍스트이다. 이것은 어디에서 변수에 접근할 수 있는지, 그 컨텍스트에서 변수에 접근할 수 있는지를 명시적으로 나타낸다.

변수는 지역 범위(local scope)와 전역 범위(global scope) 둘 중 하나를 가진다. 지역 변수는 함수내에서 전역변수보다 높은 우선순위를 가진다. 

변수 호이스팅(Variable Hoisting)

호이스트란, 변수의 정의가 그 범위에 따라 선언과 할당으로 분리되는 것을 의미한다. 즉, 변수가 함수내에서 정의되었을 경우 선언이 함수의 최상위로, 함수 바깥에서 정의되었을 경우는 전역 컨텍스트의 최상위로 변경된다.

변수의 선언이 초기화나 할당시에 발생하는것이 아니라, 최상위로 호이스트 된다.

 

function showName() {
     console.log("First Name : " + name);
     var name = "Hwang";
     console.log("Last Name : " + name);
}
showName();
  • First Name: undefined
  • Last Name: Hwang

First Name이 undefined 인 이유는 지역변수 name이 호이스트 되었기 때문이다. 이 코드는 자바스크립트 엔진에 의해 다음과 같이 해석된다. 

function showName() {
     var name; // name 변수는 호이스트 된다. 할당은 이후에 발생
     console.log("First name : " + name); // First Name : undefined
     name = "Hwang"; // name에 값이 할당
     console.log("Last Name : " + name); // Last Name : Ford
}

호이스트 되었을때, 함수 선언은 변수선언을 덮어 쓴다.

// 다음 두 변수와 함수는 myName으로 이름이 같습니다.
var myName; // string

function myName() {
     console.log("Rich");
}
// 함수 선언은 변수명을 덮어 씁니다.
console.log(typeof myName); // function

하지만, 변수에 값이 할당될 경우에는 반대로 변수가 함수선언을 덮어쓴다. 

var myName = "Richard";

function myName() {
     console.log("Rich");
}

console.log(typeof myName); //string

var (function-scoped)

 다음 코드에서 var은 function-scoped 이기 때문에 for문이 끝난 다음에도 호출하면 값을 가지고 있게 된다. 이것은 var 변수가 호이스팅 되었기 때문이다.

for(var j=0; j<10; j++) {
  console.log('j', j)
}
console.log('after loop j is ', j) // after loop j is 10

함수로 감싸여진 경우는 빠져 나가지 않는다.

function counter () {
  for(var i=0; i<10; i++) {
    console.log('i', i)
  }
}
counter()
console.log('after loop i is', i) // ReferenceError: i is not defined

Immediately-invoked function expression (or IIFE, pronounced "iffy")

IIFE 표현법으로 사용하게 되면 다음과 같이 사용할 수 있다.

(function() {
  // var 변수는 여기까지 hoisting이 된다.
  for(var i=0; i<10; i++) {
    console.log('i', i)
  }
})()
console.log('after loop i is', i) // ReferenceError: i is not defined
(function() {
  for(i=0; i<10; i++) {
    console.log('i', i)
  }
})()
console.log('after loop i is', i) // after loop i is 10

이 코드에서는 i가 function 바깥으로 호이스팅 되어 값이 유지되게 된다. 이것을 막기 위해서 'use strict'을 쓸 수 있는데 번거롭다. 

(function() {
  'use strict'
  for(i=0; i<10; i++) {
    console.log('i', i)
  }
})()
console.log('after loop i is', i) // ReferenceError: i is not defined

let, const (block-scoped)

ES2015에서는let, const 가 추가 되었다. 호이스팅으로 인한 문제점을 해결할 수 있다. 다음 문제를 보자:

// 이미 만들어진 변수이름으로 재선언했는데 아무런 문제가 발생하지 않는다.
var a = 'test'
var a = 'test2'

// hoisting으로 인해 ReferenceError에러가 안난다.
c = 'test'
var c

letconst 를 사용하면 var과 다르게 변수 재선언이 불가능해 진다. let과 const의 차이점은 let은 변수에 재할당이 가능하지만, const는 변수 재할당이 모두 불가능(immutable)하다. 

// let
let a = 'test'
let a = 'test2' // Uncaught SyntaxError: Identifier 'a' has already been declared
a = 'test3'     // 가능

// const
const b = 'test'
const b = 'test2' // Uncaught SyntaxError: Identifier 'a' has already been declared
b = 'test3'    // Uncaught TypeError:Assignment to constant variable.

let과 const는 block-scoped 단위로 호이스팅이 일어난다.

c = 'test' // ReferenceError: c is not defined
let c

위의 코드가 ReferenceError이 발생한 이유는 첫 문장의 c는 일시적 사각지대; TDZ(temporal dead zone)이기 때문에 let은 값을 할당하기 전에 변수가 선언 되어 있어야 한다. const는 좀 더 엄격하다. 

// let은 선언하고 나중에 값을 할당이 가능하지만
let dd
dd = 'test'

// const 선언과 동시에 값을 할당 해야한다.
const aa // Missing initializer in const declaration

블럭에서 var의 문제​​​​​

블럭의 경우에도 var은 function-scope이기 때문에 값이 변경되어 버린다. 

var foo = 'bar1';
cosole.log(foo); // bar1
 
if (true) {
  var foo = 'bar2';
  console.log(foo); // bar2
}
 
console.log(foo); // bar2 -> foo는 덥쳐 씌워진다.

하지만 let과 const 를 사용하면 block-scope 범위에 있기 때문에 유효 범위가 블록, 즉 {}로 감싸지 범위에 있게 된다.

let foo = 'bar1';
console.log(foo); // bar1
 
if (true) {
  let foo = 'bar2';
  console.log(foo) // bar2
}
 
console.log(foo); // bar1

블럭 범위내의 사각지대

6번 라인에 foo는 1번 라인의 foo에서 재사용 되어 지므로 문제가 없다. 

let foo = 'bar1';
console.log(foo); // bar1
 
if (true) {
  console.log(foo) // bar1
  foo = 'bar2';
  console.log(foo) // bar2
}
 
console.log(foo); // bar2

만일 블럭내에 let이 사용된다면 사각지대 (TDZ)에 있게 되어 레퍼런스 에러가 발생하게 된다. 

 let foo = 'bar1';
console.log(foo); // bar1
 
if (true) {
  console.log(foo);
  // Uncaught ReferenceError: foo is not defined
 
  let foo = 'bar2';
}
 
console.log(foo);

결론

​​​​​​기본적으로 변수 사용시에는 const를 사용하고 재할당이 필요한 경우에만 let을 사용한다. var은 ES2015에서는 쓰지 않는다. 요즘 프로그래밍 패러다임에서는 변수를 쓰면 좋지 않다고 보는데 그 이유는 예상치 못한 변수의 변경으로 인해 많은 버그가 발생하기 때문이다. ESLint의 룰에서도 const를 우선하는 것을 권장하고 있다. 다음과 같은 코드를 보면 let을 쓰는 코드를 const로 써서 해결할 수 있다는 것을 보여준다. 

let foo, bar;
if (a === b) {
  foo = a;
  bar = b;
} else {
  foo = b;
  bar = a;
}

Destructuring과 삼항 연산자를 쓰면 다음과 같이 해결이 가능해 진다. 

 const [foo, bar] = (a === b) ? [a, b] : [b, a];

IIFE를 사용해 다음과 같이 표현할 수도 있다.

const [foo, bar] = (function() {
  if (a === b) {
    return [a, b];
  } else {
    return [b, a];
  }
})();

 

youngdeok's picture

Language

Get in touch with us

"If you would thoroughly know anything, teach it to other."
- Tryon Edwards -