-
Notifications
You must be signed in to change notification settings - Fork 2
Differences of JavaScript contexts
nw.js 기반 Window 객체와 일반 자바스크립트 컨텍스트는 차이점이 있습니다. 예를 들면, 각 윈도우는 각각의 전역 컨텍스트를 지니며 각각의 전역 생성자를 지니고 있습니다. (Array
나 Object
등.)
이게 웹 브라우저의 일반적인 관행이지만, 이렇게 하는게 더 좋습니다. 왜냐하면,
-
객체의 프로토타입이 특정 라이브러리(Prototype)나 특정 스크립트에 변경되거나 확장되어도 다른 윈도우에 전혀 영향을 받지 않습니다.
-
개발자가 전역 스코프를 변조하는 실수(
new
가 빠진 잘못된 생성자 참조)를 저질러도 다른 윈도우에 큰 영향이 가지 않습니다. -
악성 앱이라 해도 다른 윈도우에 있는 보안적인 데이터 구조에 접근할 수 없습니다.
nw.js 에 구동하는 node 모듈들은 공유된 컨텍스트에서 돌아갑니다. (기본적으로 공유되었으나, 만약 분리된 컨텍스트 단위를 원한다면 Window.open
메소드 속성 인자에 'new-instance': true
필드 첨부하면 됩니다.)
require()
(Node.js 의 modules API) 함수를 사용하여 스크립트를 첨부하면 Node 컨텍스트로 간주됩니다. (require()
가 사용된 하위 모듈 모두 Node.js 컨텍스트를 엔진에서 처리한 후, 반환한 다음, 종료됩니다.)
만약 <script src="...">
태그나 jQuery의 $.getScript()
같은 동적 스크립트 기능을 사용할 경우, 해당 윈도우에만 범위를 가진 컨텍스트가 정의됩니다.
만약 모듈이 매니페스트 파일에 정의된 "node-main"
필드에 스크립트를 첨부할 경우, 처음에는 Node 컨텍스트에서 실행되지만, 앱이 초기화된 후 window
컨텍스트에서 실행됩니다. (자세한 사항은 “node-main” 페이지를 참고하세요.)
Node 컨텍스트는 현재 스크립트 파일이 소속된 폴더경로를 가져오는 __dirname
변수를 사용할 수 있습니다.
node.js 의 global
객체는 Node 컨텍스트의 최상위 객체입니다. 모든 웹킷 기반 윈도우의 window
객체는 전역 객체가 아니며 Node 컨텍스트에서 암묵적으로 접근할 수 없습니다. (이는 node-main 페이지의 특이한 예외사항입니다.) 예를 들면, 모듈 내에 window
객체에 접근하려면 함수 인자 등으로 명시적으로 받아내어야 합니다.
또한 이는 alert()
(엄밀히 window.alert()
메소드) 함수에 의지하면서 디버깅이 어려울 수 있습니다. 하지만 console.log()
같은 console
객체는 웹킷 엔진의 console
로 전달됩니다. 이는 “개발자 도구” 페이지 “콘솔” 탭에 기재되어 있습니다.
Node 컨텍스트 내에서는 require('nw.gui')
(nw.js 의 GUI API 접근을 위한) 루틴을 실행할 수 없습니다. Node 컨텍스트 내에서는 GUI 윈도우가 없기 때문입니다.
브라우저에서 지원하는 기능 (Worker
나 WebSocket
인터페이스) 또한 Node 컨텍스트에서 지원하지 않습니다.
웹킷 컨텍스트에 정의된 상대경로는 해당 HTML 파일이 있는 폴더의 기준으로 잡습니다 (모든 브라우저에서도). node 모듈에 대한 상대 경로는 해당 모듈이 있는 폴더의 기준으로 잡는 다는 것(node.js 가 항상 그렇죠.)이 다르니 이것을 기억해 주십시오.
예를 들면 /myApp/main.html
파일을 아래와 같이 작성합니다.
<html>
<head>
<!-- 이 스크립트는 현재 HTML 파일의 상대 경로로 찾을 겁니다. -->
<script src="components/myComponent.js"></script>
</head>
<body>
<script>
// 이 파일은 이 스크립트가 포함된 HTML 파일의 상대 경로로 잡아낼 겁니다.
var hello = require('./libs/myLib');
// __dirname 변수는 웹킷 스크립트에 없습니다. node.js 모듈 내에서만 사용 가능합니다.
console.log(__dirname); // undefined
</script>
</body>
</html>
/myApp/components/myComponent.js
파일에 다음과 같이 작성할 수 있습니다.
// 이 파일은 웹킷 컨텍스트에 돌아가기 때문에 이 파일을 첨부한 HTML 파일을 기준으로 상대 경로를 잡습니다. 즉, 위 main.html 기준이 되는 거죠.
var something = require('./util.js'); // 이 파일은 /myApp/util.js 경로 기준으로 잡고 /myApp/components/util.js 경로에서 기준잡지 않습니다.
// __dirname 변수는 여전히 정의되지 않습니다.
console.log(__dirname); // undefined
그리고 /myApp/libs/myLib.js
파일은 이렇게 작성할 수 있습니다.
// 이 파일은 Node 컨텍스트에서 구동됩니다. 이제 이 파일이 소속된 경로 기준으로 상대 경로를 잡게 됩니다.
var something = require('./otherUtil.js'); // 이 파일은 /myApp/libs/otherUtil.js 경로를 기준으로 잡습니다.
// __dirname 변수가 정의되었습니다.
console.log(__dirname); // '/myApp/libs'
컨텍스트가 일반적으로 이득인 점으로 볼 때, 어쩔 때는 어느 누군가의 코드에 따라 문제가 발생할 수 있습니다. 그리고 이를 대처할 점이 필요하게 됩니다.
가장 흔히 발생하는 문제는 instanceof
표현식입니다. 모질라 개발자 네트워크를 보셨다면, someValue instanceof someConstructor
식이 해당 생성자의 프로토타입이 확인하고자 하는 식에 잘 부여되는지 확인할 겁니다. 하지만 someValue
가 다른 컨텍스트에 속해 있다면, 상위 객체가 해당 라인에 있다 해도 someValue instanceof someConstructor
식이 필연적으로 실패하게 됩니다.
예를 들면, someValue instanceof Array
가 반드시 해당 값이 배열인지 다른 컨텍스트에서는 확인할 수가 없습니다. (자세한 내용은 “가장 확실하게 해당 자바스크립트 객체가 배열인지 아닌지 확인하기” 문서를 참고하세요.)
이와 같은 문제가 또 있는데, .constructor
속성을 바로 체크하는 식입니다.
(예를 들면, someValue instanceof Array
대신에 someValue.constructor === Array
식을 쓸 때)
다음 생성자들이 모두 컨텍스트에 종속된 전역 객체이며, 이들에게 영향을 받습니다.
-
표준 객체 형식:
Array
,Boolean
,Date
,Function
,Number
,Object
,RegExp
,String
. -
형식성 배열 형식:
ArrayBuffer
,DataView
,Float32Array
,Float64Array
,Int16Array
,Int32Array
,Int8Array
,Uint16Array
,Uint32Array
,Uint8Array
. -
오류 객체:
Error
,EvalError
,RangeError
,ReferenceError
,SyntaxError
,TypeError
,URIError
.
이들이 여러 방법으로 문제를 일으킬 겁니다.
가장 쉬운 컨텍스트 관련 문제는 바로 instanceof
식을 다른 컨텍스트에서 안쓰는 방법이 되겠습니다. 예를 들면, Array.isArray
메소드를 통해 해당 객체가 배열인지 아닌지 컨텍스트에 구애받지 않고 확인할 수 있습니다.
그러나, 이런 대체 방법이 어렵거나 다른 사람의 코드에서 발생해서 다른 생성자로 인해 발생된 문제를 해결할 경우 다른 방법이 필요하겠습니다.
When you foresee passing a value to some other context, you may providently use a constructor from that context in order to construct your value. The value then would easily pass any instanceof
checks in that context.
어떤 값이 다른 컨텍스트에서 올 때를 예상했다면, 값의 생성자를 선견지명으로 생성하여 instanceof
식을 쉽게 사용하여 확인할 수 있습니다.
예를 들면, 가장 많이 쓰는 async
모듈에서 (in its code 2013-05-20 에 사용한 코드를 보면; 이미 수정된 코드지만.) .constructor
를 사용하여 체크하게 됩니다. (다른 줄 472, 505, 545, 675, 752)
게다가 다른 컨텍스트에서 보낸 배열일 경우 실패하게 됩니다. 예를 들면, 아래 코드의 window 컨텍스트 안에 이런 식으로 코딩한다면요.
require('async').waterfall([
function(callback){
console.log('1.');
callback(null, 'one', 'two');
},
function(arg1, arg2, callback){
console.log('2.');
callback(null, 'three');
},
function(arg1, callback){
console.log('3.');
callback(null, 'done');
}
], function (err, result) {
console.log('Fin.');
if( err ) throw err;
console.log(result);
});
이 코드는 오류: waterfall 의 첫번째 인자가 반드시 함수 배열이어야 합니다.
오류를 전달합니다. (배열로 생각 안한다는 게 오류로 생각하시겠지만.)
nwglobal
모듈을 사용하여 node 컨텍스트에서 Array
객체에 접근하여 다음과 같이 고칠 수 있습니다.
require('async').waterfall( require('nwglobal').Array(
function(callback){
console.log('1.');
callback(null, 'one', 'two');
},
function(arg1, arg2, callback){
console.log('2.');
callback(null, 'three');
},
function(arg1, callback){
console.log('3.');
callback(null, 'done');
}
), function (err, result) {
console.log('Fin.');
if( err ) throw err;
console.log(result);
});
이걸로 async
모듈을 행복하게 사용하게 해줄 것입니다.
그러나, 몇몇의 경우는 값으로부터 해당 생성자를 바로 추출할 수 없습니다. (예를 들어 MDN에 의하면, 일반적인 함수 정의가 Function
생성자를 통해 생성하는 것보다 컨텍스트에 의한 영향이 적고 클로저를 만들지 않습니다.) 이런 경우에 다른 방법을 써야 겠죠.
비표준이지만 많이 쓰는 __proto__
속성은 (MDN 참조) 특정 객체의 내부 “Prototype” 속성을 변경하는 역할을 합니다. (해당 생성자의 내부 구조도 포함)
값이 다른 컨텍스트에서 왔다는 걸 가정했을 때, 값의 __proto__
속성을 해당 컨텍스트를 예상하고 바꿔야 instanceof
식으로 해당 생성자에서 생성됐는지 쉽게 확인할 수 있겠습니다.
흔히 쓰는 async
모듈을 예로 들면,
(2013-05-20 당시 작성된 코드 참조, 지금은 고쳐짐.)
instanceof Function
식으로 함수 체크를 시행했는데, (428줄) 이 식이 다른 컨텍스트에서 온 값일 경우 확인을 못하게 됩니다. 예를 들어 아래 코드를 window 컨텍스트에서 작성하면,
var getData = function (callback) {
setTimeout(function(){
console.log('1.1: got data');
callback();
}, 300);
}
var makeFolder= function (callback) {
setTimeout(function(){
console.log('1.1: made folder');
callback();
}, 200);
}
var writeFile= function(callback) {
setTimeout(function(){
console.log('1.1: wrote file');
callback(null, 'myfile');
}, 300);
}
var emailFiles= function(callback, results) {
console.log('1.1: emailed file: '+results.writeFile);
callback(null, results.writeFile);
}
require('async').auto({
getData:getData ,
makeFolder:makeFolder,
writeFile: ['getData', 'makeFolder',writeFile],
emailFiles: ['writeFile',emailFiles]
}, function(err, results) {
console.log('1.1: err: '+ err);
console.log('1.1: results: '+ results);
});
이 코드는 객체에 slice 메소드가 없습니다
오류를 뱉게 됩니다. (받아낸 값이 함수가 아니고 배열이라 배열의 slice
함수를 실행할 줄 알겠죠.)
nwglobal
모듈로 Node 컨텍스트의 Function
생성자를 사용하여 아래와 같이 수정합니다.
var getData = function (callback) {
setTimeout(function(){
console.log('1.1: got data');
callback();
}, 300);
}
getData.__proto__ = require('nwglobal').Function;
var makeFolder= function (callback) {
setTimeout(function(){
console.log('1.1: made folder');
callback();
}, 200);
}
makeFolder.__proto__ = require('nwglobal').Function;
var writeFile= function(callback) {
setTimeout(function(){
console.log('1.1: wrote file');
callback(null, 'myfile');
}, 300);
}
writeFile.__proto__ = require('nwglobal').Function;
var emailFiles= function(callback, results) {
console.log('1.1: emailed file: '+results.writeFile);
callback(null, results.writeFile);
}
emailFiles.__proto__ = require('nwglobal').Function;
require('async').auto({
getData:getData ,
makeFolder:makeFolder,
writeFile: ['getData', 'makeFolder',writeFile],
emailFiles: ['writeFile',emailFiles]
}, function(err, results) {
console.log('1.1: err: '+ err);
console.log('1.1: results: '+ results);
});
또다시 async
모듈 코딩이 즐거워졌습니다.
웹킷과 노드 컨텍스트 간 교환에는 약간의 시간이 소요됩니다.
대부분의 경우 이 지연이 큰 문제를 야기하지는 않습니다. 하지만 웹킷 컨텍스트에서 간단히 함수 실행을 지연시키는 행위 (같은 hot spot 함수 실행이 수백 번 지연할 경우. 예를 들면 웹킷에 500개의 표현할 객체가 있고 이게 윈도우에서 다시 그려질 때 사용자는 잠시 각 객체가 빈 화면을 무심코 본 후 나타나는 모습을 사용자가 지켜봐야 겠습니다.) 그때 노드의 setImmediate
함수가 실제로 여러분이 경험한 "즉시"란 단어를 그나마 떠오르게 해 줄 것입니다.
이 함수의 전반적인 사용을 (특히 웹킷 컨텍스트에서) 꾀한다면 Node의 setImmediate
대신 David Baron 씨가 구현한 setZeroTimeout
함수로 효과를 볼 수 있습니다.
// setZeroTimeout 함수를 윈도우 객체에만 넣고,
// 나머지는 클로저에 모두 숨깁니다.
(function() {
var timeouts = [];
var messageName = "zero-timeout-message";
// setTimeout 과 비슷하지만 함수 인자만 받습니다.
// 여기서는 지연 시간과 함수 인자를 전달할 수 없습니다. 시간은 항상 0이니까요.
// (만약 전달할 인자가 필요하다면 클로저 내에 있으면 됩니다.)
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, "*");
}
function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}
window.addEventListener("message", handleMessage, true);
// 이제 우리가 원하던 걸 윈도우 객체에 넣습니다.
window.setZeroTimeout = setZeroTimeout;
})();
표준 함수인 window.setTimeout(yourFunction, 0)
함수에 시간 인자를 0
으로 넣으면 효과가 덜 한데, 이는 지연 시간이 0일 경우 최소 지연 시간을 4 밀리초로 설정하는 HTML5 표준 때문입니다. (예를 들어 setTimeout
함수를 250번만 사용한다 해도 여러분 앱은 그보다 더 한 시간을 지연한 결과를 보게 될 겁니다.)