Electron으로 크로스 플랫폼 데스크톱 앱 만들기 #1 – 프로젝트 스캐폴딩

요즈음 모바일 앱은 Xamarin, Cordova 등 다양한 프레임워크를 이용하여 개발하는 사례가 매우 많습니다. 하지만 상대적으로 Desktop App은 이제 막 크로스플랫폼 앱 개발에 대한 논의가 시작되는 단계여서 상대적으로 정보가 많이 적고, 사용할 수 있는 리소스가 모바일 크로스플랫폼 개발에 비해서는 적은 편입니다.

연속되는 아티클 시리즈를 통하여 데스크톱 앱을 크로스플랫폼으로 개발하려고 할 때 시작할 수 있는 유용한 선택지인 Electron과 EdgeJS의 결합 시나리오에 대해 살펴보려고 합니다. 이번에는 그 중 가장 기초가 되는 처음 Electron 프로젝트를 만드는 과정을 살펴보려고 합니다.

Electron에 대하여

Electron은 Github의 크로스플랫폼 에디터인 Atom을 위하여 처음 개발되었고, 그 이후로 많은 발전을 거쳐 여러 애플리케이션의 기반 프레임워크로 현재는 널리 사용되고 있습니다. 최근 큰 주목을 받고 있는 Visual Studio Code는 물론, Slack, 잔디 등 국내외 여러 최신 소프트웨어들이 Electron을 기반으로 하이브리드 + 크로스플랫폼 소프트웨어를 개발하고 있습니다.

Electron의 생태계

Electron은 기술적으로 NodeJS의 기능과 Chromium의 렌더링 엔진을 모두 사용할 수 있습니다. 이 덕분에 Node Package Manager (NPM)과 Bower의 기능을 모두 사용할 수 있어 UI는 익숙하게 사용할 수 있는 jQuery, jQuery UI 및 jQuery Mobile이나 최근 유행하고 있는 Angular 2, React 등을 택할 수 있고, 비즈니스 로직 부분은 TypeScript나 CoffeeScript 등을 활용하여 개발할 수 있습니다.

Electron의 확장성은 여기서 그치지 않고 .NET과의 상호 운용 기능을 제공하는 EdgeJS와도 연동도 제공하고 있습니다. EdgeJS는 Windows에서는 .NET Framework 4.5 이상, Linux와 macOS에서는 Mono 4.x 이상의 프레임워크와 연동되며, C#, F# 등 다양한 언어와의 바인딩과 기존 클래스 라이브러리의 재사용을 모두 소화할 수 있습니다. 기존에 개발했던 Windows Forms나 WPF 기반의 Desktop App을 크로스플랫폼으로 전환하는 것을 염두에 두고 계시다면 현실적인 선택지 중 하나가 될 수 있습니다.

마지막으로, 데스크톱 앱을 수익화하기 위한 방법에서도 Electron은 이제 완벽한 선택지라고 할 수 있습니다. Windows의 경우 이 글을 작성하는 현재 2016년 여름에 Windows 10 Anniversary Update를 통하여 Native App을 스토어에 공식적으로 게시할 수 있게 됨에 따라 Electron App을 스토어에 등록할 수 있고, Linux의 경우 다양한 경우의 수가 있겠지만 데스크톱에서 가장 널리 쓰이는 Ubuntu Software Store에 등록할 수 있으며, Mac App Store로의 Electron App 등록은 이전부터 계속 가능했습니다.

이러한 생태계 아래에서 기존의 Native Application이나 .NET Managed Application을 Electron을 기반으로 리뉴얼을 고려해보실 수 있습니다.

첫 Electron 프로그램 만들기

Electron 기반의 개발 환경은 Windows, Linux, macOS 플랫폼이면 쉽게 구축이 가능합니다. 그 중에서도 이번 아티클에서는 Visual Studio Code를 기반으로 구축하는 예를 들어보려고 합니다.

Visual Studio Code, NodeJS, NPM, Bower를 시스템에 이미 설치한 상태임을 가정하고 진행해보도록 하겠습니다.

작업 디렉터리를 새로 하나 만들고, 해당 디렉터리에서 아래 명령을 실행하여 package.json과 bower.json을 만들어 프로젝트 기준점을 잡도록 합니다.

npm init
bower init

그 다음, electron-prebuilt 패키지를 개발 종속성 패키지로 설치하여 개발 과정 중에 Electron에 연동하여 사용할 수 있게 합니다.

npm install --save-dev electron-prebuilt

Bower를 이용하여 jQuery Mobile 패키지를 설치하겠습니다. 패키지 이름이 jquery-mobile-bower인 것을 설치해야 즉시 사용 가능한 jQuery Mobile 패키지가 설치되니 이름에 주의하여 주십시오.

bower install --save jquery-mobile-bower

이제 Electron 엔진을 사용하여 창을 띄우고 첫 UI를 표시하도록 코드를 조금 추가해보겠습니다.

package.json 파일을 만들 때, 시작점이 되는 JavaScript 파일의 이름을 따서 새 JavaScript 파일을 만듭니다. 예를 들어, package.json 파일이 아래와 같이 되어있다면 index.js 파일을 새로 만듭니다.

{
  "name": "sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "electron-prebuilt": "^1.2.2"
  },
  "dependencies": {
    "electron-edge": "^5.0.3-pre1"
  }
}

index.js 파일을 다음과 같이 만듭니다.

const electron = require('electron');
// Module to control application life.
const {app} = electron;
// Module to create native browser window.
const {BrowserWindow} = electron;

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;

function createWindow() {  
  // Create the browser window.
  win = new BrowserWindow({width: 800, height: 600});

  // and load the index.html of the app.
  win.loadURL(`file://${__dirname}/index.html`);

  // Emitted when the window is closed.
  win.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null;
  });
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (win === null) {
    createWindow();
  }
});
위의 코드에서 index.html 파일을 로드하도록 하였으므로, index.html 파일을 아래 코드와 같이 만듭니다. 아래 코드의 자세한 내용은 jQuery Mobile 튜토리얼을 참고하시면 이해하기 쉽습니다. (맥락을 정확하게 유지하기 위하여 jQuery Mobile에 대한 내용은 다루지 않겠습니다.)
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="Stylesheet" media="all" type="text/css" href="bower_components/jquery-mobile-bower/css/jquery.mobile-1.4.5.css" />
    <style type="text/css" media="all">
    body { cursor: default; }
    </style>
    <script type="text/javascript" src="compat.js"></script>
    <script type="text/javascript" src="bower_components/jquery/jquery.min.js"></script>
    <script type="text/javascript" src="bower_components/jquery-mobile-bower/js/jquery.mobile-1.4.5.min.js"></script>
</head>
<body>
    <div data-role="page" id="start">
        <div data-role="header">
            <h1>Welcome To My Homepage</h1>
        </div>
        <div data-role="main" class="ui-content">
            <p>I Am Now A Mobile Developer!!</p>
            <a href="#lastStep">Go to Next Page</a>
        </div>
        <div data-role="footer" data-position="fixed">
            <h1>Footer Text</h1>
        </div>
    </div>
    <div data-role="page" id="lastStep">
        <div data-role="header">
            <h1>Welcome To My Homepage</h1>
        </div>
        <div data-role="main" class="ui-content">
            <a href="#confirmDialog">End</a>
        </div>
        <div data-role="footer" data-position="fixed">
            <h1>Footer Text</h1>
        </div>
    </div>
    <div data-role="page" data-dialog="true" id="confirmDialog">
        <div data-role="header">
            <h1>Welcome To My Homepage</h1>
        </div>
        <div data-role="main" class="ui-content">
            <a href="#start">Go to Start page</a>
        </div>
    </div>
</body>
</html>
앞의 단락에서 이야기한 것처럼, Electron은 NodeJS와 통합된 실행 환경을 사용하고 있습니다. 이렇게 하면서 module 프로퍼티가 일반 웹 브라우저와는 다르게 미리 정의가 되는데, 이 때문에 jQuery가 전역 객체로 설정되지 않습니다. (https://blog.outsider.ne.kr/1170 아티클의 내용을 보고 도움을 얻을 수 있었습니다.)
이런 동작은 버그가 아니고 By-Design 사항입니다. 물론 필요에 따라 Chromium의 JavaScript 컨텍스트만 사용하도록 제한할 수 있지만 이렇게 할 경우 Electron의 강력한 기능을 활용할 수 없으므로 이 방법 대신 아래 코드를 compat.js로 저장하여 HTML 페이지에서 먼저 참조하고 그 다음에 jQuery를 로드하도록 수정하여 jQuery와 NodeJS 모듈을 동시에 사용할 수 있게 하려고 합니다.
window.nodeRequire = window.require;
window.nodeExports = window.exports;
window.nodeModule = window.module;
delete window.require;
delete window.exports;
delete window.module;
위의 코드를 사용하여 require, exports, module 프로퍼티를 nodeRequire, nodeExports, nodeModule로 이동시키고, jQuery가 정상적으로 초기화될 수 있게 합니다. 나중에 electron과 연동을 할 때에는 새 이름으로 정의된 객체를 사용하도록 하여 어렵지 않게 연동이 가능하게 됩니다.
이렇게 코드를 구성하고, 아래와 같이 명령을 작업 디렉터리 내에서 실행하면 앱이 실행됩니다.
node_modules/.bin/electron .
001
Production 버전으로 패키지를 만든 것이 아니고, 메뉴 등이 그대로 표시되므로 Developer Tool을 이용할 수 있습니다. Developer Tool을 View – Toggle Developer Tool 메뉴로 켜고 끌 수 있으며, 실행하면 창 하단/우측에 붙어서 나오거나 아래 그림처럼 별도의 창으로 띄울 수도 있습니다.
002

다음 아티클에서는

다음 아티클에서는 Visual Studio Code와 통합 디버깅 환경을 설정하는 방법과 JavaScript 디버깅을 손쉽게 수행할 수 있는 방안을 살펴볼 에정입니다.

댓글 남기기