globalThis
의 소름끼치는 폴리필globalThis
제안은 모든 자바스크립트 환경에서 전역 this
에 접근할 수 있는 메커니즘을 도입한다. globalThis
의 폴리필은 매우 간단할 것 같지만, 정확하게 만들기는 매우 어렵다. Toon이 창조적인 해결책으로 나를 깜짝놀래키기 전까지 나는 그것이 가능하다고 생각조차 못했다.
이 글은 globalThis
의 적절한 폴리필 작성이 어렵다는 것을 설명한다. 폴리필은 아래의 요구사항을 따라야한다.
먼저 용어에 대해서 정리하겠다. globalThis
는 전역 스코프의 this
값을 제공한다. 이것은 복잡한 이유로 브라우저의 전역 객체와는 다르다.
자바스크립트 모듈에서 모듈 스코프는 전역 스코프와 당신의 코드 사이에 개입되어 있다는 점에 유의해야 한다. 모듈 스코프는 글로벌 스코프의 this
를 숨기기 때문에 모듈 스코프의 최상단에서 this
는 undefined
이다.
즉, globalThis
는 "전역 객체"가 아니며, 단지 전역 스코프의 this
를 의미한다. 이 중요한 뉘앙스를 이해시켜준 Domenic에게 감사한다.
globalThis
의 대안브라우저에서 globalThis
와 동일한 것은 window
이다.
globalThis === window;
// → true
frames
도 동일하게 동작한다.
globalThis === frames;
// → true
하지만, window
와 frames
는 (웹 워커와 서비스 워커와 같은) 워커 컨텍스트 안에서 undefined
이다. 운 좋게도 self
는 모든 브라우저 컨텍스트에서 동작하므로 더 강력한 대안이다.
globalThis === self;
// → true
Node.js에서는 window
, frames
, self
을 사용할 수 없지만, global
을 사용할 수 있다.
globalThis === global;
// → true
jsvu
으로 설치하는 것과 같은 독립형 자바스크립트 엔진 쉘에서는 위에서 설명한 모든 것(window
, frames
, self
, global
)을 사용할 수 없다. 여기서는 전역 this
에 접근할 수 있다.
globalThis === this;
// → true
게다가 느슨한 모드 함수는 그들의 this
를 전역 this
로 설정할 수 있기 때문에, 코드를 전역 스코프에서 실행할 수 없더라도 전역 this
에 접근할 수 있다.
globalThis === (function() {
return this;
})();
// → true
하지만, 자바스크립트 모듈에서 최상단의 this
는 undefined
이고, 엄격한 모드의 함수안에서 this
는 undefined
이다. 그래서 이 접근방법은 여기서 동작하지 않는다.
일단 엄격한 모드의 컨텍스트 안에서 임시로 엄격한 모드를 깨고 나올 수 있는 방법은 오직 하나다. Function
생성자는 느슨한 함수를 생성한다.
globalThis === Function('return this')();
// → true
음, eval
도 같은 효과를 내기 때문에 두 가지 방법이 있다.
globalThis === (0, eval)('this');
// → true
브라우저에서 Function
생성자와 eval
을 사용하는 것은 컨텐츠 보안 정책(Content Security Policy (CSP))때문에 종종 허락되지 않는다. 웹사이트는 종종 이런 정책을 옵션으로 가져가지만, 예를 들어 크롬 확장프로그램 안에서는 시행하고 있다. 불행하게도, 이것은 적절한 폴리필이 Function
생성자와 eval
에 의존할 수 없다는 것을 의미한다.
주의:
setTimeout('globalThis = this', 0)
는 같은 이유로 제외되었다. 게다가 일반적으로 CSP에 의해서 막힌다. 또한setTimeout
을 폴리필에 사용하는 것에 반대하는 이유가 두 가지 더 있다. 첫 번째는 ECMAScript의 스펙이 아니고 모든 자바스크립트 환경에서 사용할 수 없다. 두 번째는 비동기적이어서setTimeout
이 모든곳에서 지원된다고 하더라도, 다른 코드에 의존하는 폴리필을 사용하는 것은 고통스러울 것이다.
위의 기법들을 결합하여 아래와 같이 하나의 폴리필을 만들 수 있다.
// 네이티브 globalThis 보강판. 이것은 사용하지 마세요!
const getGlobal = () => {
if (typeof globalThis !== 'undefined') return globalThis;
if (typeof self !== 'undefined') return self;
if (typeof window !== 'undefined') return window;
if (typeof global !== 'undefined') return global;
if (typeof this !== 'undefined') return this;
throw new Error('전역 객체를 찾을 수 없습니다');
};
// 주의: 전역 스코프에서 실행될 때 `globalThis`를 전역 객체로 만들기 위해
// (어휘적(lexical) 범위의 최상단 변수와는 대조적으로)
// `const` 대신에 `var`를 사용했다.
var globalThis = getGlobal();
하지만 이 코드는 엄격한 모드의 함수나 브라우저가 아닌 환경의 자바스크립트 모듈 안에서는 동작하지 않는다. (globalThis
를 지원하는 경우는 제외). 게다가 getGlobal
은 잘못된 결과를 반환할 수 있다. 왜냐하면 this
에 의지하는데, this
는 컨텍스트에 의존적이고 번들러에 의해서 변경될 수 있기 때문이다.
globalThis
의 강력한 폴리필을 작성할 수 있을까? 다음과 같은 환경이 있다고 가정해보자.
globalThis
, window
, self
, global
, this
에 의존할 수 없다.Function
생성자와 eval
을 사용할 수 없다.해결책은 찾았지만 방식이 아름답지는 않다. 이것에 대해서 잠시 생각해보자.
전역 this
에 직접 접근하는 방법을 모른채 전역 this
에 어떻게 접근할 수 있을까? 만약 globalThis
에 함수 프로퍼티를 어떻게든 지정하고, 그것을 globalThis
의 함수로써 호출한다면, 그 함수 안에서 this
를 접근할 수 있다.
globalThis.foo = function() {
return this;
};
var globalThisPolyfilled = globalThis.foo();
globalThis
나 globalThis
를 참조하는 바인딩 없이 어떻게 그것이 가능한가? 다음 코드는 실행 할 수 없다.
function foo() {
return this;
}
var globalThisPolyfilled = foo();
foo()
는 더 이상 함수로 호출되어질 수 없고, 또한 위에서 언급했듯이 엄격한 모드나 자바스크립트 모듈에서 이 함수 안의 this
는 undefined
이다. 엄격한 모드의 함수는 그들의 this
를 undefined
로 설정한다. 하지만 getter와 setter의 경우는 아니다.
Object.defineProperty(globalThis, '__magic__', {
get: function() {
return this;
},
configurable: true // 이것은 나중에 getter를 `delete` 가능하게 한다.
});
// 주의: 전역 스코프에서 실행될 때 `globalThis`를 전역 객체로 만들기 위해
// (어휘적(lexical) 범위의 최상단 변수와 대조적으로)
// `const` 대신에 `var`가 사용되어진다.
var globalThisPolyfilled = __magic__;
delete globalThis.__magic__;
위의 코드에서 globalThis
에 getter를 설정하고 getter를 통해서 globalThis
의 참조를 가저온 다음, 더이상 필요없는 getter는 삭제한다. 이 기법은 원하는 모든 상황에서 globalThis
에 접근할 수 있지만, 첫 번째 라인에서 여전히 (여기서는 globalThis
라고 부르는) 전역 this
참조에 의존하고 있다. 이 의존성을 피할 수 있을 까? globalThis
에 직접 접근하지 않고 getter를 전역적으로 설정 할 수 있을 까?
globalThis
에 getter를 설정하는 것 대신에, 전역 this
객체를 Object.prototype
으로 상속받아서 getter를 설정할 수 있다.
Object.defineProperty(Object.prototype, '__magic__', {
get: function() {
return this;
},
configurable: true // 이것은 나중에 getter를 `delete` 가능하게 한다.
});
// 주의: 전역 스코프에서 실행될 때 `globalThis`를 전역 객체로 만들기 위해
// (어휘적(lexical) 범위의 최상단 변수와 대조적으로)
// `const` 대신에 `var`가 사용되어진다.
var globalThis = __magic__;
delete Object.prototype.__magic__;
주의: 사실 ECMAScript 스펙에 전역
this
가Object.prototype
을 상속 받는다는 내용은 없고, 단지 객체여야 한다고 명시되어 있다.Object.create(null)
은Object.prototype
을 상속 받지 않은 객체를 만든다. 자바스크립트 엔진은Object.prototype
를 상속받지 않는 객체를this
로 사용하더라도 스펙에 위배되지 않으며, 이 경우 위와 같은 코드는 여전히 동작하지 않을 것이다. (실제로 IE 7은 그렇게 한다.) 운 좋게, 좀 더 최신의 자바스크립트 엔진은 전역this
가Object.prototype
을 prototype 체인으로 가져야만 한다고 동의한 것으로 보인다.
globalThis
를 이미 사용할 수 있는 최신의 환경에서는 Object.prototype
가 변화(mutating)하는 것을 막기 위해서 폴리필을 아래와 같이 수정할 수 있다.
(function() {
if (typeof globalThis === 'object') return;
Object.defineProperty(Object.prototype, '__magic__', {
get: function() {
return this;
},
configurable: true // 이것은 나중에 getter를 `delete` 가능하게 한다.
});
__magic__.globalThis = __magic__; // lolwat
delete Object.prototype.__magic__;
}());
// 이제 코드에서 `globalThis`를 사용할 수 있다.
console.log(globalThis);
혹은 __defineGetter__
를 사용할 수 있다.
function() {
if (typeof globalThis === 'object') return;
Object.prototype.__defineGetter__('__magic__', function() {
return this;
});
__magic__.globalThis = __magic__; // lolwat
delete Object.prototype.__magic__;
}());
// 이제 코드에서 `globalThis`를 사용할 수 있다.
console.log(globalThis);
이것은 지금까지 본 폴리필 중 가장 끔찍한 것이다. 소유하지 않은 객체(object)는 변경하지 않는 것이 관행인데, 이 폴리필은 이런 관행을 완전히 무시한다. 자바스크립트 엔진의 기본 원리인 프로토타입 최적화에서 설명하듯이 내장된 프로토타입을 변경하는 것은 일반적으로 나쁜 생각이다.
반면에 이 폴리필을 깨뜨릴 유일한 방법은 폴리필이 실행되기 전에 누군가가 Object
또는 Object.defineProperty
(또는 Object.prototype.__defineGetter__
)를 변경하는 것이다. 나는 이보다 더 강력한 폴리필을 생각해낼 수 없다. 여러분은 어떠한가?
이 폴리필은 범용적인 자바스크립트의 흥미로운 예이다. 폴리필은 순수한 자바스크립트 코드로 호스트 빌트인 기능에 의존하지 않고 ECMAScript가 구현된 어느 환경에서나 실행될 수 있다. 이것은 애초에 폴리필의 목표 중 하나였다. 실제로 이것이 동작하는지 확인해보자.
여기에 이 폴리필을 위한 HTML 데모 페이지가 있다. 이 데모에서는 고전적인 스크립트인 globalthis.js
와 동일한 코드로 모듈방식인 globalthis.mjs
를 사용하여 globalThis
를 출력한다. 이 데모는 폴리필이 브라우저에서 동작하는지 확인하는 데 사용될 수 있다. globalThis
는 기본적으로 Chrome 71 / V8 v7.1, Firefox 65, Safari 12.1, iOS Safari 12.2에서 지원한다. 폴리필의 흥미로운 부분을 테스트하기 위해서는 이전 버전의 브라우저에서 데모페이지를 열어야한다.
주의: 이 폴리필은 인터넷 익스플로러 10 이하의 버전에서는 동작하지 않는다. 이 브라우저들에서는
__magic__
이 전역this
의 참조로 동작함에도 불구하고,__magic__.globalThis = __magic__
으로 어떻게든지globalThis
가 전역으로 사용 가능하게 만들어주지 않는다. 즉__magic__
과window
가 모두[object Window]
이지만__magic__ !== window
라는 사실이다. 이것은 이 브라우저들이 전역 객체와 전역this
사이의 차이에 대해 혼돈될 수 있음을 나타낸다. 이전의 대안들 중 하나로 폴리필을 수정하면 IE10과 IE 9에서 동작한다. IE 8을 지원하기 위해서는Object.defineProperty
호출하는 부분을try
-catch
문으로 감싸고catch
블록에서 이전의 대안들 중 하나를 사용하면 된다. (이렇게 하는 것은 전역this
가Object.prototype
을 상속 받지 않는 IE 7 이슈 또한 피할 수 있다. IE의 이전버전에서 이 데모를 실행시켜 보아라)
Node.js와 독립형 자바스크립트 엔진에서 테스트하기 위해서는 동일한 자바스크립트 파일을 다운로드 한다.
# 폴리필과 데모 코드를 모듈로 다운로드 해라
curl https://mathiasbynens.be/demo/globalthis.mjs > globalthis.mjs
# 이 파일을 고전적인 자바스크립트 파일로 사용할 수 있도록 복사(즉, symlink) 해라
ln -s globalthis.mjs globalthis.js
이제 node
에서 테스트할 수 있다.
$ node --experimental-modules --no-warnings globalthis.mjs
Testing the polyfill in a module
[object global]
$ node globalthis.js
Testing the polyfill in a classic script
[object global]
독립형 자바스크립트 엔진 쉘에서 테스트하기 위해서는 jsvu
를 사용해서 원하는 엔진을 설치하고 스크립트를 바로 실행할 수 있다. 예를 들어 (globalThis
를 지원하지 않는) V8의 7.0버전과 (globalThis
를 지원하는) 7.1버전에서 테스트하기 위해서는 아래와 같이 하면된다.
$ jsvu v8@7.0 # Install the `v8-7.0.276` binary.
$ v8-7.0.276 globalthis.mjs
Testing the polyfill in a module
[object global]
$ v8-7.0.276 globalthis.js
Testing the polyfill in a classic script
[object global]
$ jsvu v8@7.1 # Install the `v8-7.1.302` binary.
$ v8-7.1.302 globalthis.js
Testing the polyfill in a classic script
[object global]
$ v8-7.1.302 globalthis.mjs
Testing the polyfill in a module
[object global]
이와 같은 기술로 JavaScriptCore, SpiderMonkey, Chakra 그리고 XS와 같은 다른 자바스크립트 엔진에서 테스트할 수 있다. 여기 JavaScriptCore를 사용한 예제가 있다.
$ jsvu # Install the `javascriptcore` binary.
$ javascriptcore globalthis.mjs
Testing the polyfill in a module
[object global]
$ javascriptcore globalthis.js
Testing the polyfill in a classic script
[object global]
자바스크립트를 사용하는 것은 까다롭고, 종종 독창적인 방법을 요구한다. globalThis
는 전역 this
에 쉽게 접근할 수 있게 해준다. 정확하게 동작하는 globalThis
폴리필은 보기보단 좀 더 도전적이지만, 동작하는 해결책이 있다.
정말로 필요할 때만 이 폴리필을 사용해라. 자바스크립트 모듈은 전역 상태를 변경하지 않고 기능을 쉽게 가져오거나 내보낼 수 있다. 대부분의 최신 자바스크립트 코드는 전역 this
에 접근을 필요로 하지 않는다. globalThis
는 라이브러리와 폴리필에서만 유용하다.
globalThis
폴리필이 글을 게시한 이후에 다음과 같은 npm 패키지가 이 기술을 이용해서 globalThis
폴리필을 제공하기 시작했다.
참고: 나는 이 패키지들 중 어떠한 것의 저자도 관리자도 아니다.