JS 프로그램에서 전역스코프는 우리 생각보다 많은 유용한 기능을 제공하며, 여타 스코프와는 다른 미묘한 차이가 있다.
이번 장에서는 전역스코프가 어떻게 유용한지, JS 호스트 환경에서 전역 스코프는 어디에 있고 어떻게 접근하는지 알아보자
4.1 전역 스코프를 배워야하는 이유
JS 엔진은 분리된 여러 개의 파일을 실행 시점에 하나로 연결시킨다. 이때 브라우저에서 실행되는 어플리케이션은 주로 세가지 방법을 사용한다
- ES 모듈 : 파일을 하나씩 로딩하여, import 문으로 참조한다. 이때 각 모듈을 서로의 스코프를 공유하지 않고 베타적으로 협력한다
- 번들러 : 모든 파일을 하나로 묶고, 브라우저에는 하나의 파일만 전달한다.
- 이때 다양한 빌드 환경을 구축할 수 있는데, 그 중 파일 내용 전체를 래퍼 함수나 유니버셜 모듈 등으로 하나의 스코프 안에 묶도록 해주는 방법도 있다.
1
2
3
4
(function wrappingOuterScope() {
var moduleOne = (function one() {})()
var moduleTwo = (function two(){})
})
wrappingOuterScope()
는 전역 스코프는 아니고 함수 스코프지만 애플리케이션 전체를 아우르는 스코프처럼 작동한다.moduleOne
,moduleTwo
는 모듈끼리 협력하기 위해 선언되었다
- 전역 스코프 활용 : 파일을 각각 로딩하여 전역스코프를 통해 모듈끼리 협력한다
전역스코프는 이렇게 여러 파일을 모아 협력하는 것만이 아니라 다음 경우에도 사용되며, 프로그램을 풍성하게 하는 여러 기능을 포함한다.
- JS 내장 기능
- 특정 호스팅 환경에서 제공하는 API 사용 시 : console, DOM, 타이머 등
모든 변수를 전역에 넣는 것은 안되지만, 모든 JS 프로그램이 전역 스코프를 접착제처럼 파일을 모으고 실행한다는 것은 부정할 수 없다
4.2 전역 스코프의 위치
호스팅 환경에따라 전역스코프의 위치에는 차이가 발생한다.
브라우저의 창, window 객체
브라우저는 코드 침입을 최소화하고 전역 스코프가 작동할 때 간섭도 최대한 하지 않는다.
1
2
3
var studentName = "name";
function hello() {}
위 코드를 브라우저에 불러오면 studentName
, hello
가 전역 스코프에 선언된다. == 전역 객체의 프로퍼티를 통해 해당 식별자에 접근할 수 있다
JS 명세서를 근거로는 외부스코프가 전역 스코프이고, studentName은 전역변수가 된다.
하지만 이런 작동방식이 모든 JS 호스팅 환경에서 보장되는게 아니다. 아래와 같은 예시에서는 전역 변수가 같은 이름의 전역 객체 프로퍼티를 섀도잉할 수 있다
1
2
3
4
5
6
window.something = 42;
let something = "카일";
console.log(something) // 카일
console.log(window.something) // 42
let으로 변수를 선언하면 전역 변수에는 추가되지만 전역 객체의 프로퍼티엔 추가되지 않고, 이로인해 렉시컬 식별자가 something을 참조할때 전역변수 something을 참조한다.
이러한 동작은 개발자에게 실수를 일으킬수 있다. 따라서 전역에서는 항상 var를 쓰고, 블록 스코프에서는 let, const를 쓰는 등의 규칙이 필요하다.
브라우저 JS 전역스코프는 몇몇 순수하지 못한 동작 방식을 가진다.
- DOM 요소에 id 속성을 추가하면 전역변수가 자동으로 생긴다. 하지만 이렇게 자동 등록된 변수는 되도록 사용하지 않는 것이 좋다.
window.name
프로퍼티는 window 객체에 사전에 정의된 getter/setter 이다. setter는 어떤 값을 넣든 문자열로 변환시키는 규칙을 가진다. 따라서 아래 코드와 같은 동작이 발생한다.
1
2
var name = 42;
console.log(42, typeof name) // 42 string
웹 워커
웹 플랫폼 확장 기능으로, JS 프로그램이 돌아가는 메인 스레드가 아닌 별도의 스레드에서 돌아갈 수 있게 해준다. 이때 레이스 컨디션을 방지하기 위해 메인 애플리케이션 스레드와 통신이 제한된다. 따라서 웹 워커 내에선 DOM에 접근할 수 없다.(navigator 등 몇몇 웹 API 제외)
웹 워커는 메인 JS 프로그램과 다른 프로그램이기에 전역 스코프를 공유하지 않는다. 대신에 self
를 사용하여 전역 객체를 참조할 수 있고, 메인 JS 프로그램과 마찬가지로 var, function 은 self
에 자동 등록된다.
개발자 도구와 콘솔, REPL
개발자 도구는 JS 환경을 완전히 재현하지 않는다. 일반 JS 프로그램보다 덜 업격하게 처리하는데, 따라서 일반 JS 프로그램에서는 오류가 났을 코드가 개발자 도구에서는 정상동작하는 경우가 있다.
이러한 경우 중 스코프와 관련된 예시는 아래와 같다.
- 전역 스코프의 작동 방식
- 호이스팅
- 가장 바깥 스코프에서 블록 스코프 선언을 할때, 전역 스코프에서 처리되는 것처럼 보이지만 그렇지 않다.
ES 모듈
- ES 모듈 파일에서 선언된 최상위 변수는 전역에 추가되지 않는다. 대신 모듈 범위(모듈 전역) 스코프가 된다.
- 모듈 최상위 레벨 스코프에서는 모듈 내 모든 컨텐츠가 함수에 래핑된 것처럼 묶여서 처리되며, 이는 전역 스코프의 하위 스코프가 된다. 따라서 모듈 내에서 전역 변수는 렉시컬 스코프를 통해 접근 가능하다.
- 다만 모듈 내에서 전역 변수, 객체에 접근은 할 수 있지만, 가능한 의존도를 최소화하는 것이 권장된다.
Node.js
Node.js 에서는 엔트리 파일을 포함한 모든 JS 파일을 모듈(ES 모듈 혹은 CommonJS 모듈)로 처리한다. 이런 특징은 각 JS 파일이 자체 스코프를 갖도록 한다.
Node.js 는 각 모듈을 함수로 감싸는데, var/function 선언이 전역 변수로 취급되는 것을 방지하고 함수 스코프에 포함시키기 위함이다.
1
2
3
4
5
6
7
8
9
function Module(module, require, __dirname, ...) {
var studentName = "카일";
function hello() {
console.log(studentName);
}
module.exports.hello = hello;
}
이런 API 식별자는 전역에 있지 않고, 함수 Module에 있는 매개변수처럼 모든 모듈의 스코프에 자동으로 주입된다.
node.js의 진짜 전역 변수는 global
로 접근 가능하다. 객체 global
에 프로퍼티를 추가하면 일단 전역 변수에 접근하는 것처럼 쓸 수 있다.
1
2
3
4
5
6
7
global.studentName = "카일";
function hello() {
console.log(studentName);
}
module.exports.hello = hello;
4.3 globalThis
여러 JS 호스팅 환경마다 전역 스코프 객체에 접근할 수 있는 방법은 다양하나, ES2020 에서 전역 스코프 객체 참조가 globalThis
로 표준화되었다. 따라서 요즘에서 globalThis
를 참조하면 전역 스코프 객체를 참조할 수 있다. (폴리필을 사용하여 구식 환경도 대응가능함)