이것Do! 저것Do!!

2020/04/28 - [Development] - 윈도우즈에서 일렉트론 데스크탑 어플리케이션 개발하기 1

2020/04/29 - [Development] - 윈도우즈에서 일렉트론 데스크탑 어플리케이션 개발하기 2

2020/04/29 - [Development] - 윈도우즈에서 일렉트론 데스크탑 어플리케이션 개발하기 3

 

'electron/electron-quick-start'에서 제공하는 boilerplates를 이용하여 기본적인 앱을 실행시키는 것은 큰 문제 될 것이 없는데 일렉트론에 대해 아무런 배경지식이 없는 상태에서 원하는 기능을 넣으려고 하니까 어디서부터 손을 대야할 지 정말 막막했었다. 그래서 이런 저런 개발 블로그들, 공식문서 등등을 살펴봤었는데 여러 개발 블로그들이 상당히 예전 버전의 일렉트론을 이용한 개발기를 다루고 있었고 보안 이슈등과 관련된 부분(구조적으로)도 크게 신경쓰지 않은 채로 기능 개발에 중점을 두고 있는 듯한 모양새라 그런 내용들을 그대로 따라가도 될 지 의문스러웠었다.

지금부터 기록해 놓으려는 나의 개발기도 완전 초보 수준이기에 '이렇게 하는 것이 맞다. 이렇게 해야 한다.'라고 단언할 수는 없지만, 나름 개발가이드를 준수하는 모양이라고 생각하고 있고 혹시라도 고수분들의 조언을 얻을 수 있다면 더할 나위 없이 좋은 기회가 될 것이다. 아주 아주 기초적인 기능하나 만들어 보자. 나머지는 여기서 응용해 나가면 될테니~

 

0. Goal

- 메뉴 제거

- 앱 윈도우 사이즈 변경 금지

- 파일시스템의 파일을 하나 선택하여 절대경로를 화면에 표시

구현할 내용은 무지 간단하다. 물론 현재 개발하고 있는 앱은 저기에 좀 더 많은 기능이 들어가 있기는 한데 저 정도만 할 수 있으면 나머지는 같은 패턴으로 찾아가며 할 수 있기에 충분하리라.

 

1. 메뉴 제거 & 리사이즈 금지

사실 만들려고 하는 앱이 'https://www.balena.io/etcher/'와 비슷한 모양이다.

(이 앱도 electron을 이용하여 만들어졌다.)

 

그래서 일단 'electron-quick-start'의 메뉴부터 없애고자 하였다. 아주 간단하다.

// Modules to control application life and create native browser window
const {app, BrowserWindow} = require('electron')
const path = require('path')

function createWindow () {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 1400,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    },
    resizable: false  // 리사이즈 금지
  })

  // for v5.0.0 or later
  mainWindow.removeMenu();  // 메뉴 제거

딱 두 줄이면 목표로 설정했던 기능 중에 두 개를 해결할 수 있게 된다. 그런데, 이렇게 하고 나면 메뉴에 있었던 화면을 리프레시하는 기능이나 'DevTools'를 on/off하는 기능을 사용할 수 없어서 불편하다. 물론 메뉴제거를 모든 개발이 끝난 뒤에 제일 마지막으로 해도 되겠지만, 단축키를 이용하여 리프레시 등을 할 수 있는 기능이 있어서 추가를 해 보도록 하자.

'electron-localshortcut'이란 모듈이 필요하다. 'npm install electron-localshortcut'으로 설치해 주고, 아래의 코드를 추가하면 된다.

// Modules to control application life and create native browser window
const {app, BrowserWindow} = require('electron')
const path = require('path')
const electronLocalshortcut = require('electron-localshortcut')

function createWindow () {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 1400,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    },
    resizable: false  // 리사이즈 금지
  })

  // for v5.0.0 or later
  mainWindow.removeMenu();  // 메뉴 제거

  // register F12: DevTools를 토글하기 위한 단축키로 F12를 등록
  electronLocalshortcut.register(mainWindow, 'F12', () => {
    console.log('F12 is pressed')
    mainWindow.webContents.toggleDevTools()
  });

  // register F5: 화면 reload하기 위한 단축키로 F5를 등록
  electronLocalshortcut.register(mainWindow, 'F5', () => {
    console.log('F5 is pressed')
    mainWindow.reload();
  });

  // and load the index.html of the app.
  mainWindow.loadFile('index.html')

  // Open the DevTools.
  mainWindow.webContents.openDevTools()
}

 

2. 파일시스템의 파일을 하나 선택하여 절대경로를 화면에 표시

일단 'index.html'에 파일 선택 다이얼로그를 열어 주는 버튼 한 개와 선택된 파일의 절대경로를 표시해 주는 텍스트박스를 위한 코드를 삽입해 준다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    We are using Node.js <span id="node-version"></span>,
    Chromium <span id="chrome-version"></span>,
    and Electron <span id="electron-version"></span>.

    <hr>
    <div>
      <input type="text" placeholder="Please select a file" id="abs-path" disabled="disabled"/>
      <input type="button" value="Choose a file" id="select-file"/>
    </div>

    <!-- You can also require other files to run in this process -->
    <script src="./renderer.js"></script>
  </body>
</html>

저장하고 실행해(npm start) 보면 화면에 텍스트박스와 버튼이 추가된 것을 확인할 수 있다. 그럼 이제 버튼을 클릭했을 때 다이얼로그를 화면에 표시하도록 이벤트 처리를 해 줘야 하는데......

요 부분을 하기 전에 알아둬야 할 내용들이 있다.

https://www.electronjs.org/docs/tutorial/security

 

보안, Electron 보안 관련 기능, 개발자의 책임감 | Electron

⚠️ 어떤 상황에서도 Node.js 통합을 사용하는 원격 코드를 로드하고 실행하지 않아야 합니다. 대신, Node.js 코드를 실행하기 위해 로컬 파일 (애플리케이션과 함께 패키지된) 만 사용하십시오. To display remote content, use the tag or BrowserView, make sure to disable the nodeIntegration and enable contextIsolation.

www.electronjs.org

https://www.electronjs.org/docs/api/context-bridge

 

contextBridge | Electron

Create a safe, bi-directional, synchronous bridge across isolated contexts

www.electronjs.org

그럼 일단 이벤트 처리의 흐름을 보자면 아래와 같은 순서로 진행이 될 것이다.

'버튼 클릭' -> '파일 선택 다이얼로그 표시' -> '파일 선택 후 확인'

-> '파일 선택 다이얼로그의 리턴값을 취해 파일 정보로부터 절대경로를 얻어내고 이것을 텍스트 박스에 표시'

 

그럼 먼저 '버튼 클릭'에 대한 이벤트 리스너를 아래와 같이 'renderer.js'에 추가해 준다.

// This file is required by the index.html file and will
// be executed in the renderer process for that window.
// No Node.js APIs are available in this process because
// `nodeIntegration` is turned off. Use `preload.js` to
// selectively enable features needed in the rendering
// process.

document.getElementById('select-file').addEventListener('click', function() {
    window.myapp.openfile("open-file", {calback: (result) => {
        console.log(result);
        document.getElementById("abs-path").value = result.filePath
    }})
},false)

제일 위에 있는 주석의 내용과 같이 이 파일에서는 Node.js API를 직접 호출하여 사용하는 식으로 구현을 해서는 안된다. 하지만, 여기서는 '파일 선택 다이얼로그'를 사용해야 하니까 일렉트론의 'dialog'모듈에 있는 api를 사용해야 하는데 아래 링크의 내용을 보면 이 'dialog'모듈은 또 'Main Process'용 모듈이다.

https://www.electronjs.org/docs/api/dialog

 

dialog | Electron

파일 열기와 저장, 경고, 기타 등등에 대한 기본 시스템 대화 상자를 표시합니다.

www.electronjs.org

이것 또한 여기서 바로 호출하는 식으로 사용하면 안된다. 불가능하지는 않지만 공식 문서의 설명대로 해킹의 위험이 커진다는 것이다. 때문에 아래와 같은 식으로 'ContextBridge'를 이용하여 한 단계 거쳐서 이벤트 처리를 해 주도록 유도한다.

(사실 이렇게 하는게 맞는 것인지에 대한 확인이 아직 없다. 워낙 일렉트론의 구조 등에 대한 지식이 없는 상태이다보니...ㅡㅡ;; 하지만, 이렇게 구현하고 실행해 보면 아무런 warning이나 error가 없는 것을 봐선 적어도 틀린 것은 아닐 것이라고 위안삼고 있다..^^;)

'preload.js'에 아래 내용을 추가하면 된다.

// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
  const replaceText = (selector, text) => {
    const element = document.getElementById(selector)
    if (element) element.innerText = text
  }

  for (const type of ['chrome', 'node', 'electron']) {
    replaceText(`${type}-version`, process.versions[type])
  }
})

const {dialog} = require('electron').remote
const {contextBridge} = require("electron")

contextBridge.exposeInMainWorld(
  "myapp", {
    openfile: (id, params) => {
      console.log(id, params)

      dialog.showOpenDialog({
        properties: ['openFile']
      }).then(result => {
        console.log(result.canceled)
        console.log(result.filePaths)
        if (result.filePaths !== undefined) {
            params.calback({filePath: result.filePaths})
        }
      }).catch(err => {
        console.log(err)
      })
    }
  }
)

실행해 보면 warning 메시지 하나 없이 아주 깔끔하게 동작하는 것을 확인할 수 있다.

 

자... 다시 개발 시작!!!