본문 바로가기

프로그래밍 언어/Javascript

자바스크립트 스코프 ( scope ), 전역 객체

C++ 에서 보면 using namespace std; 라고 자주 보인다. std 네임스페이스를 쓰겠다는 뜻이다. 네임스페이스는 여러가지 정보가 저장된 공간이라고 봐야 한다. 

우리가 도서관에 있다면 우리는 도서관 namespace 에 있는 것이고, 그 공간안의 정보를 활용할 수 있다. 물론 핸드폰, 노트북 등을 통해서 인터넷 검색을 할 수도 있다. 그렇다면 그것은 일종의 global namespace 일 것이다.

 

모든 정보를 global 에 배치해두고 써도 되겠지만, 아래와 같이 문제가 많다.

  • 하나의 도서관에 이 세상 모든 책을 보관할 수는 없다. 공간이 부족하기 때문이다.
  • 또한 사람들이 필요로 하지 않는 책도 많을 것이다. 
  • 하나의 책을 여러 사람이 필요로 할 때도 문제가 된다.
  • 그 외에도 여러가지 문제가 많을 것이다.

프로그래밍에서도 마찬가지다. 자바스크립트도 마찬가지다. namespace 는 scope 와 비슷한 점이 있다.

  • 하나의 스코프에 모든 정보를 저장할 수는 없다.
  • 함수가 필요로 하는 정보는 소수일 수 있다. 심지어 접근 제한을 두기 위해 캡슐화를 진행하기까지 한다. 오염을 막기 위해서다.
  • 여러 사람들이 같은 이름의 변수를 생성할 때도 문제가 된다.

위와 같은 이유로 스코프를 두어 정보들을 어떤 공간에 두고 그 범위와 함께 관리하게 된다.


Function Scope vs. Block Scope

ES6 전에는 Function scope 와 Global scope 만 존재했다고 하지만, 이제는 let, const 가 추가되면서 Block scope 가 생겼다.

한가지 짚고 넘어가야할 점은 var 는 function scope 라는 점이다. 오해하는 점 중에 하나가 var 로 정의된 변수의 경우, 어디에서나 끌어쓸 수 있다고 생각하지만 아래의 예시를 보면 생각이 바뀌게 된다.

function f(){
  var value=1
}
f()
console.log(value)

위와 같이 작성하고 실행하면 ReferenceError: value is not defined 가 발생하게 된다. value 를 f 라는 함수 안에 정의하였으나 var 로 하여서 밖에서도 쓸 수 있을 것 같은 오해를 하지 말아야 한다. var 로 선언된 변수는 function scope 를 갖게 되고, f 함수 안에서 선언되었으니 f 함수 내부에서만 존재하는 값인 것이다. 

사실 함수 안에서는 var, let, const 모두 function scope 를 갖는다. 그 함수안에서만 존재하고 접근 가능한 값이다. 리턴되지 않는 한.

for (let i = 0; i < 5; ++i) {
  var value1 = 1
}
console.log(value1) // 1
for (let i = 0; i < 5; ++i) {
  let value2 = 1
}
console.log(value2) // ReferenceError: value2 is not defined

위의 예시를 보자. 위를 통해 let, const 가 갖는 block scope 가 무엇인지 알 수 있다. block (블록)이라 함은 중괄호 { } 로 묶인 공간이다. 그 공간 안에서 let, const 로 정의된 변수는 그 곳에서만 유효하다. 같은 함수 내 공간이라도 블록이 다르다면 존재하지 않는 값이 된다. 하지만 var 는 function scope 이므로 중괄호 공간 밖에서도 같은 함수내라면 접근이 가능하다.

위의 예시와 같이, 함수없이 파일 내에 쓰면 해당 파일 내의 공간 어디에서든 접근 가능하게 된다. 자바스크립트 파일을 작성 후 실행하면 해당 파일은 하나의 거대한 함수가 되어 실행되는 방식이 된다. 그렇기에 var 로 선언된 함수는 해당 파일 어디에서든 접근 가능하게 되는 것이다. 그렇다면 이제 선언과 사용의 순서에 대한 의문이 남게 된다.


Hoisting ( 호이스팅 )

아래의 예시를 보자

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

위와 같이 작성 후 실행하면,

undefined
1
1
/Users/tachyon/develop/sample/p3.js:6
  console.log(value2)
              ^

ReferenceError: Cannot access 'value2' before initialization

위와 같은 결과를 보게 된다.

 

여기서 놀라운 사실은 첫 줄이다. var value1 = 1 을 통해 아직 선언이 되지 않았음에도, undefined 라고 콘솔에 찍혔다. 에러가 나지 않았다. 이것이 바로 호이스팅이다. var value1 = 1 이라는 선언이 특정 function scope 안에 존재하면 그 함수 내에서는 (현재는 파일 내부) 어디에서도 일단 value1 에 접근은 할 수 있다. 다만 값이 부여되지 않았기에 undefined 이다. ReferenceError 가 발생하진 않는다. 

이것이 var 가 가지는 let, const 와의 중요한 차이점이다. var 로 선언된 변수는 호이스팅이 된다.

value2 는 let 으로 선언되었고, 선언 전에 접근하려 하면 ReferenceError 가 발생한다.


global object

그렇다면 파일 최상단에 var 없이 변수를 선언하면서 값을 넣으면 어떻게 될까?

function f() {
  value = 1
}
value = 5
console.log(global)
console.log(1, value, global.value, this)
f()
console.log(2, value, global.value, this)
value = 10
console.log(3, value, global.value, this)
global.value = 100
console.log(4, value, global.value, this)
this.value = 70
console.log(5, value, global.value, this)
console.log(6, module.exports, this === module.exports)

위와 같이, 입력하면 아래와 같이 출력된다.

Object [global] {
  global: [Circular],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Function]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Function]
  },
  value: 5
}
1 5 5 {}
2 1 1 {}
3 10 10 {}
4 100 100 {}
5 100 100 { value: 70 }
6 { value: 70 } true

var 를 붙이지않고 그냥 변수를 정의해버리면 자동으로 global object 에 붙게 된다.

Node.js 최상단은 이 global object 내부라고 생각할 수 있다. 그래서 마치 class 안에 필드를 추가하는 것처럼 또는 어떤 객체 안에 필드를 추가하는 것처럼, global 객체안에 필드로 붙게 된다. 

 

1번에서 value 에는 5라는 값이 저장되고 그 value 는 global 객체 안에 필드로 존재하고 있다. 그리고 this 도 찍어봤는데, 뭔가 this 가 global 일 것 같다는 생각에서였다. 하지만 여기서 this 는 global 이 아니다!

 

2번 콘솔을 보면 value 는 이제 1로 변경되었다. 함수 f 가 실행되었기 때문이다. 나중에 다루겠지만 함수는 자기 자신을 실행한 주체가 this 가 된다. 하지만 this.value 라고 하지 않았다! 그래서 함수 f 는 처음에는 자신의 function scope 내에서 value 를 찾았을테고, 없으니 자기 외곽 scope 에서 value 를 찾았다. 함수 외부의 파일 로컬 스코프에도 value 가 없어서 global 까지 접근했다. 그래서 global.value 는 1로 변경되었다. 그리고 바로 value 에 접근하려 하면 global.value 가 리턴됨도 알 수 있다.

 

3번 콘솔을 보면, 역시 바로 global.value 값이 변경되었다.

 

4번 콘솔에서는 직접적으로 global.value 에 접근하여 값을 변경했다.

 

5번 콘솔에서는 this.value 에 값을 넣어봤다. global.value 값은 변경되지 않았고, this.value 가 생성되었다.

 

그렇다면 이제 this 는 무엇일까? 여기서 this 는 module.exports 이다. module.exports 는 외부로 나가는 데이터이다. 최상단에서 this 는 바로 module.exports 였던 것이다!


global object - 다른 시도

이제 이번에는 조금 다르게 시도해보자.

function f() {
  value = 1
}
value = 5
console.log(global)
global.value = -100
console.log(1, value, global.value, this)
f()
console.log(2, value, global.value, this)
value = 10
console.log(3, value, global.value, this)

var value = -5   // <--- 추가된 부분

console.log(4, value, global.value, this)
global.value = 100
console.log(5, value, global.value, this)
this.value = 70
console.log(6, value, global.value, this)
console.log(7, module.exports, this === module.exports)

위와 같이, 중간에 var value = -5 를 넣었다. 그렇게하면 아래와 같은 결과를 받는다.

Object [global] {
  global: [Circular],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Function]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Function]
  }
}
1 5 -100 {}
2 1 -100 {}
3 10 -100 {}
4 -5 -100 {}
5 -5 100 {}
6 -5 100 { value: 70 }
7 { value: 70 } true

value 와 global.value 값이 달라졌다.

이전 예제에서는

value = 5 라고 했을 때, 바로 global 객체에 value 값이 저장됐었다.

또한, value 에 접근하려 했을 때, value 가 없다면 global.value 에서 값을 가져왔었다. 

하지만 이제 value 는 global 에 저장되지 않는다. var value = -5 라고 중간에 선언하였고, 호이스팅 되었기 때문이다.

그래서 이제 global 객체 안에는 value 가 존재하지 않는다.

global.value = -100 이라고 직접 접근해서 넣어주면 값이 생긴다.

 

이후 f 함수가 실행되면 value 에 1을 넣으려고 할테고, 위의 예제에서처럼 함수내에서 일단 value 를 찾아보았고, 없으니 함수 바깥 로컬 스코프에서 value 를 찾았다. 위의 예제와 달리, 이번에는 value 를 찾았다. 그래서 global 객체까지 접근하지 않았다. value 에 5가 있던 것을 1로 바꾸어주었다. global.value 는 바뀌지 않았고, value 값만 바뀐 것을 보기 위해 global.value = -100 을 미리 넣어두었던 것이다.

 

나머지 부분은 예상대로 진행되었다.

 

정리하면,

자바스크립트에서는 파서가 파일을 전체적으로 훑어서 var 로 정의된 변수를 호이스팅해준다. 

var 로 어디서 정의되었느냐에 따라 해당 함수 내에서 접근 가능한 변수가 된다.

let, const 는 블록 스코프여서 블록 내에서만 접근 가능하다. 그리고 호이스팅되지 않기 때문에 순서에 주의해야 한다. 정의되기 이전에 접근하면 에러가 발생한다. 

아무런 정의없이 변수를 사용하면 global 객체에 담기게 되고, 이는 아마도? 가장 최후에 접근되는 최상단 객체일 것이라고 생각된다.

또한, 최상단에서의 this 는 module.exports 이고, 이는 바로 외부 파일에서 접근하는 영역이다.