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
파일을 생성하고 online
과 offline
이벤트 리스너를 작성한다.
// 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에서 어플리케이션의 윈도우에 대해 표시되는 파일을 설정할 수 있다. 표시된 파일의 아이콘이 제목 표시줄에 표시되고 사용자가 Command-Click
또는 Control-Click
을 누르면 파일 경로가 있는 팝업이 표시된다.
또한 이 윈도우의 문서가 수정되었는지 여부를 파일 아이콘이 나타낼 수 있도록 윈도우의 편집 상태를 설정할 수 있다.
MacOS의 파일 표시 / 그림출처: electronjs.org/docs
위의 스크린샷은 Atom 텍스트 편집기에서 현재 열려 있는 파일을 나타내기 위해 이 기능을 사용하는 예이다.
윈도우의 표시되는 파일을 설정하려면 BrowserWindow.setRepresentedFilename와 BrowserWindow.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
일렉트론 어플리케이션을 실행하고 Commnad
나 Control
키를 누른 상태에서 제목을 클릭하면 맨 위에 홈 디렉토리의 파일이 표시되는 팝업이 표시된다.
일렉트론은 파일이나 컨텐츠를 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.shouldUseDarkColors
및 Tray
API가 올바르게 작동하려면 일렉트론 7.0.0 버전 이상을 사용하거나, 이전 버전의 Info.plist
파일에서 NSRequiresAquaSystemAppearance
을 false
로 설정해야 한다. Electron Packager
와 Electron 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:toggle
과 dark-mode:system
두 IPC 채널을 renderer
프로세스에 노출한다. 또한 toggle
과 system
이라는 두 가지 메서드를 할당해 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 채널 핸들러 메서드는 문자열 system
을 themeSource
에 할당하고 아무것도 반환하지 않는다.
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>
태그를 사용하려면 BrowserWondow
의 webPreferences
에서 webviewTag
를 true
로 설정해야 한다.
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
프로세스에서 경계를 설정하여 제어한다.
BrowserViews
는 WebContents
를 BrowserWindow
의 방식과 유사하게 구현하기 때문에 컨텐츠에 대한 최고의 제어 기능을 제공한다. 그러나 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