일렉트론 기능 개발

niyu·2021년 8월 30일
0
post-thumbnail

Electron Docs를 기반으로 작성하였습니다.

온라인/오프라인 감지

온라인/오프라인 이벤트는 표준 HTML5 API인 navigator.onLine 속성을 이용해 renderer 프로세스에서 구현할 수 있다.

navigator.onLine 속성은 네트워크와의 연결이 끊긴 경우와 같은 네트워크 요청의 실패가 보장되는 경우의 false이고, 그렇지 않은 경우에는 true를 리턴한다.

navigator.onLine API를 사용해 연결 상태 표시기를 작성해보자.

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>
<body>
    <h1>Connection status: <strong id='status'></strong></h1>
    <script src="renderer.js"></script>
</body>
</html>

renderer.js 파일을 생성하고 onlineoffline 이벤트 리스너를 작성한다.

// renderer.js
function updateOnlineStatus () {
  document.getElementById('status').innerHTML = navigator.onLine ? 'online' : 'offline';
}

window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);

updateOnlineStatus();

main.js 파일을 생성하고 윈도우를 생성한다.

// main.js
const { app, BrowserWindow } = require('electron');

function createWindow () {
  const onlineStatusWindow = new BrowserWindow({
    width: 400,
    height: 100
  });

  onlineStatusWindow.loadFile('index.html');
}

app.whenReady().then(() => {
  createWindow();
  // ...
})
// ...

일렉트론을 실행하면 결과는 다음과 같은 알림이 표시된다.


Windows의 상태 표시


MacOS의 상태 표시 / 그림출처: electronjs.org/docs

macOS의 파일 표시

macOS에서 어플리케이션의 윈도우에 대해 표시되는 파일을 설정할 수 있다. 표시된 파일의 아이콘이 제목 표시줄에 표시되고 사용자가 Command-Click 또는 Control-Click을 누르면 파일 경로가 있는 팝업이 표시된다.

또한 이 윈도우의 문서가 수정되었는지 여부를 파일 아이콘이 나타낼 수 있도록 윈도우의 편집 상태를 설정할 수 있다.


MacOS의 파일 표시 / 그림출처: electronjs.org/docs

위의 스크린샷은 Atom 텍스트 편집기에서 현재 열려 있는 파일을 나타내기 위해 이 기능을 사용하는 예이다.

윈도우의 표시되는 파일을 설정하려면 BrowserWindow.setRepresentedFilenameBrowserWindow.setDocumentEdited API를 사용한다.

// main.js
const { app, BrowserWindow } = require('electron');
const os = require('os');

function createWindow () {
  const win = new BrowserWindow({
    width: 800,
    height: 600
  })
}

app.whenReady().then(() => {
  const win = new BrowserWindow();

  win.setRepresentedFilename(os.homedir());
  win.setDocumentEdited(true);
});
// ...

setRepresentedFilename의 파라미터에 파일의 경로 이름을 설정하면 파일의 아이콘이 윈도우의 제목 표시줄에 표시된다. setDocumentEdited 은 윈도우의 문서를 편집했는지 여부를 지정해 true로 설정하면 제목 표시줄의 아이콘이 회색으로 바뀐다.


MacOS에서의 일렉트론 어플리케이션 실행 / 그림출처: electronjs.org/docs

일렉트론 어플리케이션을 실행하고 CommnadControl 키를 누른 상태에서 제목을 클릭하면 맨 위에 홈 디렉토리의 파일이 표시되는 팝업이 표시된다.

파일 드래그 앤 드랍

일렉트론은 파일이나 컨텐츠를 Web 컨텐츠에서 OS로 드래그 앤 드랍하는 기능을 지원한다.

// preload.js
const { contextBridge, ipcRenderer } = require('electron');
const path = require('path');

contextBridge.exposeInMainWorld('electron', {
  startDrag: (fileName) => {
    ipcRenderer.send('ondragstart', path.join(process.cwd(), fileName));
  }
})

preload.js에서 contextBridge를 사용해 IPC 메시지를 main 프로세스로 보낼 window.electron.startDrag 메서드를 주입(inject)한다.

<!-- index.html -->
<div 
     style="border:2px solid black;border-radius:3px;padding:5px;display:inline-block" 
     draggable="true" 
     id="drag">
  Drag me
</div>
<script src="renderer.js"></script>

draggable 요소를 index.html에 추가하고 renderer.js 스크립트를 추가한다.

// renderer.js
document.getElementById('drag').ondragstart = (event) => {
  event.preventDefault();
  window.electron.startDrag('drag-and-drop.md');
}

위의 contextBridge를 통해 추가한 메서드를 호출하여 drag 이벤트를 처리하도록 renderer 프로세스를 설정한다.

// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');
const https = require('https');

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js');
    }
  });

  win.loadFile('index.html');
}

const iconName = path.join(__dirname, 'iconForDragAndDrop.png');
const icon = fs.createWriteStream(iconName);

// Create a new file to copy - you can also copy existing files.
fs.writeFileSync(path.join(__dirname, 'drag-and-drop.md'), '# First file to test drag and drop');

https.get('https://img.icons8.com/ios/452/drag-and-drop.png', (response) => {
  response.pipe(icon);
});

app.whenReady().then(createWindow);

ipcMain.on('ondragstart', (event, filePath) => {
  event.sender.startDrag({
    file: path.join(__dirname, filePath),
    icon: iconName,
  })
});

ondragstart 이벤트에 대한 응답으로 webContents.startDrag(item) API를 호출하도록 한다.

일렉트론 어플리케이션을 실행한 후 브라우저 창에서 바탕 화면으로 마크다운 파일을 드래그앤드랍을 하면 아래와 같다.


MacOS에서의 드래그앤드랍 / 그림출처: electronjs.org/docs


Windows에서의 드래그앤드랍

오프스크린 렌더링

오프스크린(Offscreen) 렌더링을 사용하면 비트맵에 BrowserWindow의 컨텐츠를 얻을 수 있다. 일렉트론의 오프스크린 렌더링은 Chromium Embedded Framework 프로젝트와 비슷한 접근방식을 사용한다.

이때 두 방식의 렌더링을 사용할 수 있고, 효율적으로 하기 위해 변경된 영역만 paint 이벤트에 전달된다. 웹 페이지에 아무 일도 발생하지 않으면 프레임이 생성되지 않는다. 오프스크린 창은 항상 프레임이 없는 창으로 작성된다.

또한 렌더링을 중지하거나, 계속하거나, 프레임 속도를 변경할 수 있으며, 값이 클수록 이익 없이 성능 손실만 가져오기 때문에 최대 프레임률은 240이다.

** 오프스크린 이미지: 사용자가 보고 있는 화면에 보이지 않는 이미지

두가지 렌더링 방식

🔎 GPU 가속 방식
GPU 가속 렌더링은 합성(composition)에 GPU가 사용되는 것을 의미한다. GPU에서 프레임을 복사해야 하기 때문에 더 많은 성능이 필요해 다른 소프트웨어 출력 장치보다 좀 더 느리다. 이 방식의 장점은 WebGL과 3D CSS 애니메이션이 지원된다는 것이다.

🔎 소프트웨어 출력 장치 방식
이 방식은 CPU에서 렌더링을 위해 소프트웨어 출력 장치를 사용하기 때문에 프레임 생성이 더 빠르다. 따라서 이 방식을 GPU 가속 방식보다 선호된다.
이 방식을 사용하려면 app.disableHardwareAcceleration() API 를 호출하여 GPU 가속을 사용하지 않도록 설정해야 한다.

// main.js
const { app, BrowserWindow } = require('electron');
const fs = require('fs');

app.disableHardwareAcceleration();

let win;

app.whenReady().then(() => {
  win = new BrowserWindow({ webPreferences: { offscreen: true } });

  win.loadURL('https://github.com');
  win.webContents.on('paint', (event, dirty, image) => {
    fs.writeFileSync('ex.png', image.toPNG());
  });
  win.webContents.setFrameRate(60);
});

일렉트론 어플리케이션을 실행한 후 어플리케이션의 작업 폴더로 이동해 렌더링된 이미지를 찾을 수 있다.


Windows에서의 실행 화면


렌더링된 ex.png 이미지

다크 모드

🔎 사용자 고유의 인터페이스 자동 업데이트
앱에 자체 다크 모드가 있는 경우 시스템의 다크 모드 설정과 동기화하여 켜고 꺼야 한다. 이 작업은 CSS media query의 prefers-color-scheme 를 사용하여 수행할 수 있다.

🔎 사용자 고유의 인터페이스 수동 업데이트
nativeTheme 모듈의 themeSource 속성을 가지고 Light/Dark 모드를 수동으로 전환할 수 있다. 이 속성 값은 renderer 프로세스로 전파된다. prefers-color-theme 와 관련된 모든 CSS 규칙이 그에 따라 업데이트된다.

🔎 macOS에서의 다크 모드 세팅
macOS 10.14 Mojave에서 애플은 macOS에 새로운 시스템 차원의 다크 모드를 도입했다. 일렉트론 앱에 다크 모드가 있는 경우 nativeTheme를 사용해 시스템 전체에 다크 모드 설정을 따르도록 할 수 있다.

MacOS 10.15 Catalina에서 Apple은 MacOS에 새로운 자동 다크 모드 옵션을 도입했다. 카탈리나에서 nativeTheme.shouldUseDarkColorsTray API가 올바르게 작동하려면 일렉트론 7.0.0 버전 이상을 사용하거나, 이전 버전의 Info.plist 파일에서 NSRequiresAquaSystemAppearancefalse로 설정해야 한다. Electron PackagerElectron Forge 는 애플리케이션 빌드 시간 동안 Info.plist 변경을 자동화하는 darwinDarkModeSupport 옵션이 있다.

일렉트론 8.0.0 버전 이상에서 사용하려면 Info.plist 파일의 NSRequiresAquaSystemApperance 키를 true로 설정해야 한다.

nativeTheme를 이용해 어플리케이션의 테마를 변경하는 예제를 작성해보자.

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
    <link rel="stylesheet" type="text/css" href="./styles.css">
</head>
<body>
    <h1>Hello World!</h1>
    <p>Current theme source: <strong id="theme-source">System</strong></p>

    <button id="toggle-dark-mode">Toggle Dark Mode</button>
    <button id="reset-to-system">Reset to System Theme</button>

    <script src="renderer.js"></script>
  </body>
</body>
</html>
// style.css
@media (prefers-color-scheme: dark) {
  body { background: #333; color: white; }
}

@media (prefers-color-scheme: light) {
  body { background: #ddd; color: black; }
}

이 예에서는 두 개의 요소가 포함된 HTML 페이지를 렌더링한다. theme-source 요소는 현재 선택된 테마를 보여주며, 두 개의 button 요소는 컨트롤이다. CSS 파일은 prefers-color-scheme 미디어 쿼리를 사용해 body 요소의 배경과 텍스트 색상을 설정한다.

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('darkMode', {
  toggle: () => ipcRenderer.invoke('dark-mode:toggle'),
  system: () => ipcRenderer.invoke('dark-mode:system')
});

preload.js 스크립트는 darkMode 라는 window 객체에 새 API를 추가한다. 이 API는 dark-mode:toggledark-mode:system 두 IPC 채널을 renderer 프로세스에 노출한다. 또한 togglesystem이라는 두 가지 메서드를 할당해 renderer에서 main 프로세스로 메시지를 전달한다.

// renderer.js
document.getElementById('toggle-dark-mode').addEventListener('click', async () => {
  const isDarkMode = await window.darkMode.toggle();
  document.getElementById('theme-source').innerHTML = isDarkMode ? 'Dark' : 'Light';
});

document.getElementById('reset-to-system').addEventListener('click', async () => {
  await window.darkMode.system();
  document.getElementById('theme-source').innerHTML = 'System';
});

renderer.js 파일은 button 기능을 제어한다. addEventListener를 사용하여 각 버튼에 click 이벤트 리스너를 추가한다. 각 이벤트 리스너 핸들러는 해당 window.darkMode API 메서드를 호출한다.

// main.js
const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron');
const path = require('path');

function createWindow () {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js');
    }
  })

  win.loadFile('index.html');

  ipcMain.handle('dark-mode:toggle', () => {
    if (nativeTheme.shouldUseDarkColors) {
      nativeTheme.themeSource = 'light';
    } else {
      nativeTheme.themeSource = 'dark';
    }
    return nativeTheme.shouldUseDarkColors;
  });

  ipcMain.handle('dark-mode:system', () => {
    nativeTheme.themeSource = 'system';
  });
}

app.whenReady().then(() => {
  createWindow();
});
// ...

ipcMain.handle 메서드를 이용해 main 프로세스가 버튼의 클릭 이벤트에 대응할 수 있다.

dark-mode:toggle IPC 채널 핸들러 메서드는 shouldUseDarkColors의 속성을 확인하고 해당되는 themeSource를 설정한 다음 현재 shouldUseDarkColors 속성 값을 반환한다. 이 리스너의 반환 값이 theme-source 요소에 올바른 텍스트를 할당하는 데 사용된다.

dark-mode:system IPC 채널 핸들러 메서드는 문자열 systemthemeSource에 할당하고 아무것도 반환하지 않는다.

Electron Fiddle을 사용해 예제를 실행한 다음 Toggle Dark Mode 버튼을 클릭해보자. 앱이 밝은 배경색과 어두운 배경색을 번갈아 표시하기 시작할 것이다.


MacOS에서의 다크 모드 / 그림출처: electronjs.org/docs

Windows에서의 다크 모드

임베디드 웹 페이지

BrowserWindow에 third-party 웹 컨텐츠를 포함하려는 경우, 사용할 수 있는 옵션은 세 가지가 있다. <iframe> 태그, <webview> 태그 및 <BrowserViews> 태그이다. 각 기능은 조금씩 다르며 상황에 따라 유용하게 쓰인다.

🧩 Iframes

일렉트론에서 아이프레임은 일반 브라우저에서의 아이프레임처럼 동작한다. 페이지의 iframe 요소는 해당 컨텐츠 보안 정책이 허용하는 경우 외부 웹 페이지를 표시할 수 있다. <iframe> 태그에서 사이트의 기능 수를 제한하려면 sandbox 속성을 사용하고 지원하려는 기능만 허용하는 것이 좋다.

<!DOCTYPE html>
<html>
<body>
  <iframe 
    src="https://electronjs.org" 
    width="90%" 
    height="300" 
    sandbox="allow-forms"
    frameborder="1" 
    scrolling="yes">
  </iframe>
</body>
</html>

🧩 WebViews

어플리케이션의 안정성에 영향을 미칠 수 있는 WebView는 사용하지 않는 것이 좋다. iframe이나 Electron의 BrowserView와 같은 대안을 사용하는 것을 권장한다.

웹뷰는 Cromium의 WebViews를 기반으로 하며 일렉트론에서 명시적으로 지원하지 않는다. 향후 일렉트론 버전에서 웹뷰 API를 계속 사용할 수 있다는 보장은 없다. <webview> 태그를 사용하려면 BrowserWondowwebPreferences에서 webviewTagtrue로 설정해야 한다.

webview 태그는 일렉트론 내에서만 작동하는 커스텀 요소이며, IPC를 사용하여 비동기적으로 <webview>와 통신한다. <webview> 요소에는 webContents와 유사한 커스텀 메서드와 이벤트가 많이 있어 컨텐츠를 보다 효과적으로 제어할 수 있다.

<webview 
  id="foo" 
  src="https://www.github.com/" 
  style="display:inline-flex; width:640px; height:480px">
</webview>

<iframe>에 비해 <webview>는 속도가 약간 느린 경향이 있지만 third-party 컨텐츠 로드 및 통신과 다양한 이벤트를 처리하는 데 있어 훨씬 뛰어난 제어 기능을 제공한다.

🧩 BrowserViews

BrowserView는 DOM의 일부가 아니다. 대신 main 프로세스에서 작성되고 제어된다. 기존 윈도우 위에 있는 또 다른 웹 컨텐츠 레이어일 뿐이다. 즉, 사용자의 BrowserWindow 컨텐츠와 완전히 분리되어 있으며 위치가 DOM이나 CSS에 의해 제어되지 않는다. 대신 main 프로세스에서 경계를 설정하여 제어한다.

BrowserViewsWebContentsBrowserWindow의 방식과 유사하게 구현하기 때문에 컨텐츠에 대한 최고의 제어 기능을 제공한다. 그러나 BrowserView는 DOM의 일부가 아니라 DOM 위에 중첩되어 있으므로 수동으로 위치를 관리해야 한다.

// main.js
const { BrowserView, BrowserWindow } = require('electron');

function createWindow () {
  const win = new BrowserWindow({ width: 800, height: 600 });

  const view = new BrowserView();
  win.setBrowserView(view);
  view.setBounds({ x: 0, y: 0, width: 600, height: 300 });
  view.webContents.loadURL('https://electronjs.org');
}

app.whenReady().then(() => {
  createWindow();
});


Windows에서의 BrowserView

0개의 댓글