Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Electron에서 JavaScript 라이브러리 로딩 방법

이전 글에서 설명했듯이 Electron에서는 앱의 화면을 HTML, CSS, JavaScript로 만들기 때문에 기존에 웹사이트를 만들듯이 작업을 할 수 있다. 그래서 화면에 사용하는 HTML에서 다음과 같이 jQuery같은 라이브러리를 로딩할 것이다.

<!DOCTYPE html>
<html>
<head>
  <title>Hello World</title>
  <meta http-equiv="Content-Security-Policy" content="default-src *; script-src 'self'; style-src 'self' 'unsafe-inline';">
</head>
<body>
  <h1>Hello World</h1>
  <script src="components/jquery/dist/jquery.js"></script>
</body>
</html>

하지만 이렇게 HTML에서 라이브러리를 추가하고 사용하려고 하면 라이브러리에 따라서 사용하지 못하는 경우가 발생한다.

Electron앱에서 jQuery가 로딩되었지만 객체는 없다는 오류가 나온다


라이브러리의 전역 객체가 생기지 않는 이유

개발자 도구에서 확인하면 분명히 자바스크립트 라이브러리가 정상적으로 로딩되었는데 막상 사용하려고 하면 해당 객체가 존재하지 않는다고 나오는 것이다. 위처럼 아주 간단한 예제에서는 다양하게 테스트해볼 수 있지만, 애플리케이션을 한참 만들다가 이런 문제를 만나면 왜 이런 상황이 발생하는지 이해하지 못해서 한참을 고생하게 된다.

이는 JavaScript 라이브러리의 히스토리를 좀 알아야 하는데 Node.js가 등장한 이후 대부분의 JavaScript 라이브러리는 웹 브라우저뿐만이 아니라 Node.js 같은 CommonJS 환경에서도 사용할 수 있도록 이에 대해 지원을 하게 된다. 이는 웹 브라우저가 아닌 곳에서 사용하기도 하지만 browserify나 webpack같은 곳에서 JavaScript 파일을 번들링할 때도 사용된다. 위에서 예시로 사용한 jQuery를 살펴보자.

jQuery v2.1.4의 소스코드를 보면 다음과 같은 코드가 존재한다.

if ( typeof module === "object" && typeof module.exports === "object" ) {
  // For CommonJS and CommonJS-like environments where a proper `window`
  // is present, execute the factory and get jQuery.
  // For environments that do not have a `window` with a `document`
  // (such as Node.js), expose a factory as module.exports.
  // This accentuates the need for the creation of a real `window`.
  // e.g. var jQuery = require("jquery")(window);
  // See ticket #14549 for more info.
  module.exports = global.document ?
    factory( global, true ) :
    function( w ) {
      if ( !w.document ) {
        throw new Error( "jQuery requires a window with a document" );
      }
      return factory( w );
    };
} else {
  factory( global );
}

CommonJS 환경에서는 모듈 간의 접근을 위해 module이나 module.exports가 존재하기 때문에 이를 통해서 해당 환경이 브라우저인지 CommonJS 환경인지를 구분해서 이에 맞게 초기화를 해주는 코드이다. 이 코드를 통해서 라이브러리의 기능을 전역객체로 노출할지 아니면 module.exports로 노출할지를 판단하게 된다. 위에서 jQuery가 객체가 전역으로 생기지 않는 이유는 Electron이 화면은 크로니움 기반으로 돌리고 있지만 환경 자체는 Node.js 환경이라는 점 때문이다.

Electron의 개발자도구에서 module등의 객체를 출력할 수 있다

그래서 콘솔에서 테스트를 해보면 크롬 브라우저와는 다르게 module, module.exports가 존재하고 추가로 window외에 global이 존재하고 심지어 require() 함수도 존재한다. 웹사이트를 개발하듯이 HTML에서 jQuery를 로딩하게 되면 jQuery가 이를 CommonJS 환경으로 인식해서 jQuery$를 전역으로 생성하는 대신 exports해버리기 때문에 전역객체가 생기지 않는 것이다.

Electron은 웹 기술을 사용해서 만들지만, 웹 브라우저는 아니므로 이렇게 웹사이트와 같은 방식 대신 다른 방법으로 접근해야 한다. 앞에서 jQuery를 예로 들었지만 Node.js나 CommonJS 환경을 탐지하는 코드가 표준화되어 있지 않으므로 라이브러리마다 조금씩 다르다.

moment.js v2.10.6을 보면 다음과 같이 사용하고 있다.

typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
global.moment = factory()

lodash v3.10.1에서는 다음과 같이 사용하고 있다.

/** Detect free variable `exports`. */
var freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports;

/** Detect free variable `module`. */
var freeModule = objectTypes[typeof module] && module && !module.nodeType && module;

/** Detect free variable `global` from Node.js. */
var freeGlobal = freeExports && freeModule && typeof global == 'object' && global && global.Object && global;

/** Detect free variable `self`. */
var freeSelf = objectTypes[typeof self] && self && self.Object && self;

/** Detect free variable `window`. */
var freeWindow = objectTypes[typeof window] && window && window.Object && window;

/** Detect the popular CommonJS extension `module.exports`. */
var moduleExports = freeModule && freeModule.exports === freeExports && freeExports;

// 중략

// Some AMD build optimizers like r.js check for condition patterns like the following:
if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
  // Expose lodash to the global object when an AMD loader is present to avoid
  // errors in cases where lodash is loaded by a script tag and not intended
  // as an AMD module. See http://requirejs.org/docs/errors.html#mismatch for
  // more details.
  root._ = _;
  // Define as an anonymous module so, through path mapping, it can be
  // referenced as the "underscore" module.
  define(function() {
    return _;
  });
}
// Check for `exports` after `define` in case a build optimizer adds an `exports` object.
else if (freeExports && freeModule) {
  // Export for Node.js or RingoJS.
  if (moduleExports) {
    (freeModule.exports = _)._ = _;
  }
  // Export for Rhino with CommonJS support.
  else {
    freeExports._ = _;
  }
}
else {
  // Export for a browser or Rhino.
  root._ = _;
}

유명한 세 라이브러리를 비교해 봤는데 간단히 정리하면 다음과 같은 조건일 경우 해당 환경이 CommonJS 환경이라고 판단한다.

  • jQuery는 moduleObject이면서 module.exportsObject인 경우
  • moment.js는 exportsObject이고 moduleundefined가 아닌 경우
  • lodash는 moduleObject이면서 exportsObject인 경우

Electron의 경우 modulemodule.exports는 존재하지만 exports는 존재하지 않으므로 위 세 라이브러리에서 moment.js와 lodash는 정상적으로 동작하지만, jQuery는 동작하지 않게 된다.

Electron에서 라이브러리를 로딩하는 방법

라이브러리마다 어떻게 탐지하는지 소스를 열어볼 수도 없고 다른 문제가 생길 수도 있는 트릭코드를 넣을 수도 없으므로 Electron에서는 이렇게 HTML에서 JS 파일을 직접 불러오는 대신에 다른 방법을 사용해야 한다.

require 방식

앞에서 module.exports가 존재하고 require도 존재한다고 말했듯이 Node.js에서 사용하는 방식을 그대로 사용하면 된다.

npm을 이용해서 jQuery를 설치했다면 다음과 같이 선언할 수 있다.

window.$ = window.jQuery = require('jquery');

require('jquery')로 jQuery 모듈을 불러온 뒤에 평소 웹사이트에서 사용하듯이 사용하기 위해 window.$window.jQuery에 할당하면 된다. npm 대신 bower 등으로 라이브러리를 직접 다운받았다면 js 파일의 위치를 상대경로로 불러오면 된다.

window.$ = window.jQuery = require('./components/jquery/dist/jquery.js');

모듈에 맞게 변수를 적절하게 정의해 주어야 하지만 JS 라이브러리에서는 전역변수를 보통 알고 있으므로 이렇게 사용하는 데 크게 문제가 없다.

node-integration 사용 안 함

위처럼 require로 사용하는 게 불편하고 일반 웹사이트를 만들 때처럼 HTML에서 JavaScript 파일을 로딩해서 사용하고 싶다면 node-integration옵션을 끄면 된다.

new BrowserWindow({width: 800, height: 600, "node-integration": false});

BrowserWindow에서 "node-integration": false옵션을 지정하면 기존과 달리 module, require등을 사용할 수 없게 되므로 웹사이트를 제작하듯이 JavaScript 파일을 로딩해서 사용할 수 있게 된다. 말 그대로 노드 통합을 사용하지 않는 것이므로 이렇게 사용하려면 다른 Node 기능을 사용하지 않는지 확인해 보고 사용해야 한다.

2015/10/11 06:22 2015/10/11 06:22