스코프

범위(scope)라는 뜻에 걸맞게, 스코프는 변수의 접근 가능한 범위를 뜻한다.

전역 스코프와 지역 스코프

가장 바깥쪽의 코드 블록에 선언된 변수는 전역(global) 스코프를 가지며, 이를 흔히 전역 변수라고 부른다. 자바스크립트에서는 변수를 선언할 때 var, let, const 키워드를 사용하지만 키워드 없이도 변수를 선언할 수 있는데, 이렇게 선언된 변수는 어디에서 선언되든 전역 변수로 사용할 수 있다.

함수나 특정 구문과 같이 코드 블록 안에서 선언된 변수는 지역(local) 스코프를 가지며, 이런 변수를 지역 변수라고 한다. 지역 변수는 해당 변수가 선언된 코드 블록 안에서만 접근해 사용할 수 있으며, 그 외의 부분에서는 변수에 접근할 수 없다.

var thisIsGlobal = 10;

function fnTest() {
  thisIsGlobal = 20;
  var thisIsLocal = 100;
}

fnTest();
console.log(thisIsGlobal); // 20
console.log(thisIsLocal); // ReferenceError: thisIsLocal is not defined

전역 변수와 같은 이름의 지역 변수를 코드 블록 안에서 만들어 사용할 수도 있다. 이 경우 코드 블록 안에서는 지역 변수로 선언된 변수가 사용되며, 그 외의 경우에는 전역 변수가 사용된다.

var thisIsGlobal = 10;

function fnTest() {
  var thisIsGlobal = 20;
  console.log(thisIsGlobal);
}

fnTest(); // 20
console.log(thisIsGlobal); // 10

전역 변수는 코드의 어디에서나 접근할 수 있기 때문에 코드의 실행 흐름을 복잡하게 만들거나 실행 흐름에 의도치 않은 영향을 끼칠 수 있다. 또한 서로 다른 개발자가 같은 프로젝트에서 작업할 때 같은 변수를 서로 다르게 사용하여 프로그램에 문제를 일으킬 수도 있다. 이러한 이유로 전역 변수는 가급적 사용하지 말아야 한다.

스코프를 결정하는 방법

자바스크립트의 스코프는 렉시컬 스코프(lexical scope)이다. 'lexical'은 '사전적', '어휘적'이라는 뜻으로, 작성된 코드에 의해 컴파일 시점에 스코프가 정해진다. 렉시컬 스코프를 다르게는 정적 스코프(static scope)라고도 부른다. 한편, 실행 시점에 변수를 호출할 때 스코프가 결정되는 언어들도 있는데, 이런 언어들은 동적 스코프(dynamic scope)를 사용한다고 말한다.

var a = 10;

function fnInner() {
  console.log(a);
}

function fnOuter() {
  a = 20;
  fnInner();
}

fnOuter(); // 20
var a = 10;

function fnInner() {
  console.log(a);
}

function fnOuter() {
  var a = 20;
  fnInner();
}

fnOuter(); // 10

위의 두 코드에서 첫번째로 실행되는 fnOuter()는 전역 변수 a의 값을 변경하고, 두번째로 실행되는 fnInner()에서 그 값이 출력된다. 차이점이 있다면 위 코드에서는 전역 변수 a를 그대로 사용하는 한편 아래 코드에서는 전역 변수 a와 같은 이름의 지역 변수 a를 새로 만든다는 것인데, 아래 코드에서는 fnInner()fnOuter()에서 새로 만든 지역 변수 a의 값이 아니라 전역 변수 a의 값을 찾아 출력한다.

var 그리고 let/const의 스코프

2015년에 나온 ECMAScript 6(ES6 혹은 ES2015)에서는 변수를 선언할 때 쓰는 키워드로 기존의 var에 더해 let과 변경 불가능한 변수를 위한 const가 추가되었다. varlet의 차이점은 var로 선언된 변수는 함수 스코프를 따라 동작하며 let(그리고 const)으로 선언된 변수는 블록 스코프를 따라 동작한다는 것이다.

for (var i = 0; i < 3; i++) {
  console.log(i);
}

console.log(i); // 3

변수 ifor 반복문 안에서 선언되었으므로 반복문 바깥에서는 사용할 수 없다고 예상할 경우, 코드는 반복문 바깥에서도 i의 값을 출력하면서 예상을 배신한다. 이는 i호이스팅되어 반복문 전에 먼저 선언되고 반복문이 실행되면서 값이 초기화되는 식으로 실행되었기 때문이다.

function fnTest() {
  for (var i = 0; i < 3; i++) {
    console.log(i);
  }

  console.log(i); // 3
}

fnTest();
console.log(i); // ReferenceError: i is not defined

이 코드를 함수에 넣고 나서야 함수 안에 있는 i에 접근할 수 없게 되었다. 즉, var 변수는 함수 스코프를 따르며, 이때 변수가 함수 안에 있느냐 밖에 있느냐로 var 변수에 접근 가능한지 불가능한지를 판단한다.

한편 let이나 const로 선언된 변수는 블록 스코프를 따르며, 변수가 함수가 아닌 코드 블록 안에 있는지 밖에 있는지에 따라 let/const 변수가 접근 가능한가 불가능한가를 판단한다. 아래 코드에서 let으로 선언된 변수 ifor 반복문 안에 있는 것으로서 반복문 밖에서 접근할 수 없다.

for (let i = 0; i < 3; i++) {
  console.log(i);
}

console.log(i); // ReferenceError: j is not defined

varlet의 다른 차이로는 var 변수를 같은 이름으로 다시 선언하고 초기화할 경우 나중 값으로 변수가 덮어써지는 데 비해 let 변수는 다시 선언하면 오류를 일으킨다. 변수의 값이 바뀌어버리면 코드가 의도치 않게 동작할 수 있으며, 특히 전역 스코프에서 이런 일이 벌어진다면 영향을 받는 부분이 지역 스코프보다 커지기 때문에 문제가 커진다. let에는 일종의 안전장치가 있다고 볼 수 있는 것이다.

var a = 10;
var a = "!0";
console.log(a); // "!0"

let b = 10;
let b = "!0"; // SyntaxError: Identifier 'b' has already been declared
console.log(b); // 10

results matching ""

    No results matching ""