Electron Complete Guide | Build Desktop Apps with JavaScript
이 글의 핵심
Electron lets you build macOS, Windows, and Linux desktop apps using web technologies. This guide covers the main/renderer process model, secure IPC, native OS integration, and distribution — with a React + Vite setup.
What This Guide Covers
Electron wraps your web app in a desktop shell with access to the filesystem, system tray, notifications, and more. This guide covers the architecture, secure IPC, native APIs, and packaging for distribution.
Real-world insight: VS Code, Slack, Discord, Figma, and 1Password are all built with Electron — it’s proven for production desktop apps used by millions.
Setup (Vite + React + Electron)
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm install -D electron electron-builder @electron-toolkit/preload
1. Process Architecture
Electron has two process types:
Main Process (Node.js)
├── Creates browser windows
├── Manages app lifecycle
├── Accesses Node.js / native APIs
└── Communicates with renderer via IPC
Renderer Process (Chromium)
├── Renders your HTML/CSS/JS (React/Vue/etc.)
├── Cannot directly access Node.js APIs
└── Communicates with main via IPC (through preload)
Preload Script
├── Runs in renderer context with Node.js access
└── Bridge between main and renderer via contextBridge
2. Main Process
// src/main/index.ts
import { app, BrowserWindow, shell } from 'electron'
import { join } from 'path'
let mainWindow: BrowserWindow | null = null
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
contextIsolation: true, // required for security
nodeIntegration: false, // required for security
},
})
// Load Vite dev server in development, built files in production
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:5173')
mainWindow.webContents.openDevTools()
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
// Open external links in default browser
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url)
return { action: 'deny' }
})
}
app.whenReady().then(createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
3. Preload Script (Secure Bridge)
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
// Expose safe APIs to renderer
contextBridge.exposeInMainWorld('api', {
// File operations
readFile: (path: string) => ipcRenderer.invoke('fs:readFile', path),
writeFile: (path: string, content: string) =>
ipcRenderer.invoke('fs:writeFile', path, content),
// Dialog
openFile: () => ipcRenderer.invoke('dialog:openFile'),
saveFile: (content: string) => ipcRenderer.invoke('dialog:saveFile', content),
// App info
getVersion: () => ipcRenderer.invoke('app:getVersion'),
// Events from main to renderer
onUpdate: (callback: (version: string) => void) => {
ipcRenderer.on('update-available', (_, version) => callback(version))
return () => ipcRenderer.removeAllListeners('update-available')
},
})
TypeScript types for renderer:
// src/renderer/types/electron.d.ts
interface Window {
api: {
readFile: (path: string) => Promise<string>
writeFile: (path: string, content: string) => Promise<void>
openFile: () => Promise<string | null>
saveFile: (content: string) => Promise<string | null>
getVersion: () => Promise<string>
onUpdate: (callback: (version: string) => void) => () => void
}
}
4. IPC Handlers (Main Process)
// src/main/ipc.ts
import { ipcMain, dialog, app } from 'electron'
import { readFile, writeFile } from 'fs/promises'
export function registerIpcHandlers() {
// File read
ipcMain.handle('fs:readFile', async (_, path: string) => {
return readFile(path, 'utf-8')
})
// File write
ipcMain.handle('fs:writeFile', async (_, path: string, content: string) => {
await writeFile(path, content, 'utf-8')
})
// Open file dialog
ipcMain.handle('dialog:openFile', async () => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Text Files', extensions: ['txt', 'md', 'json'] }],
})
if (result.canceled) return null
return readFile(result.filePaths[0], 'utf-8')
})
// Save file dialog
ipcMain.handle('dialog:saveFile', async (_, content: string) => {
const result = await dialog.showSaveDialog({
filters: [{ name: 'Text Files', extensions: ['txt'] }],
})
if (result.canceled || !result.filePath) return null
await writeFile(result.filePath, content)
return result.filePath
})
// App version
ipcMain.handle('app:getVersion', () => app.getVersion())
}
5. Using the API in React
// src/renderer/App.tsx
import { useState, useEffect } from 'react'
export function Editor() {
const [content, setContent] = useState('')
const [filePath, setFilePath] = useState<string | null>(null)
const [version, setVersion] = useState('')
useEffect(() => {
window.api.getVersion().then(setVersion)
}, [])
async function openFile() {
const text = await window.api.openFile()
if (text !== null) setContent(text)
}
async function saveFile() {
const path = await window.api.saveFile(content)
if (path) setFilePath(path)
}
return (
<div>
<header>
<button onClick={openFile}>Open</button>
<button onClick={saveFile}>Save</button>
<span>v{version}</span>
</header>
<textarea
value={content}
onChange={e => setContent(e.target.value)}
style={{ width: '100%', height: '80vh' }}
/>
</div>
)
}
6. System Tray
// src/main/tray.ts
import { Tray, Menu, nativeImage, app } from 'electron'
import { join } from 'path'
export function createTray(mainWindow: BrowserWindow) {
const icon = nativeImage.createFromPath(join(__dirname, 'icon.png'))
const tray = new Tray(icon.resize({ width: 16, height: 16 }))
const menu = Menu.buildFromTemplate([
{ label: 'Show', click: () => mainWindow.show() },
{ label: 'Hide', click: () => mainWindow.hide() },
{ type: 'separator' },
{ label: 'Quit', click: () => app.quit() },
])
tray.setToolTip('My App')
tray.setContextMenu(menu)
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
})
}
7. Auto-Update
npm install electron-updater
// src/main/updater.ts
import { autoUpdater } from 'electron-updater'
export function setupAutoUpdater(mainWindow: BrowserWindow) {
autoUpdater.checkForUpdatesAndNotify()
autoUpdater.on('update-available', (info) => {
mainWindow.webContents.send('update-available', info.version)
})
autoUpdater.on('update-downloaded', () => {
autoUpdater.quitAndInstall()
})
}
Requires hosting releases on GitHub Releases, S3, or your own server.
8. App Packaging (electron-builder)
// package.json
{
"build": {
"appId": "com.yourcompany.myapp",
"productName": "My App",
"directories": { "output": "dist-electron" },
"mac": {
"category": "public.app-category.productivity",
"target": ["dmg", "zip"]
},
"win": {
"target": ["nsis"]
},
"linux": {
"target": ["AppImage", "deb"]
},
"publish": {
"provider": "github",
"owner": "your-username",
"repo": "my-app"
}
}
}
# Build for current platform
npm run build
electron-builder
# Build for all platforms (requires macOS for signing)
electron-builder --mac --win --linux
Security Checklist
new BrowserWindow({
webPreferences: {
contextIsolation: true, // ✅ must be true
nodeIntegration: false, // ✅ must be false
sandbox: true, // ✅ enable sandbox
webSecurity: true, // ✅ don't disable this
}
})
// ✅ Always validate IPC inputs
ipcMain.handle('fs:readFile', async (_, path: string) => {
// Validate path is within allowed directory
if (!path.startsWith(allowedDir)) throw new Error('Access denied')
return readFile(path, 'utf-8')
})
// ✅ Don't use shell.openExternal with user-provided input without validation
Key Takeaways
| Concept | Key point |
|---|---|
| Main process | Node.js — creates windows, manages lifecycle |
| Renderer process | Chromium — your React/Vue app |
| Preload | Bridge via contextBridge — the only safe IPC path |
| IPC | ipcRenderer.invoke → ipcMain.handle for request/reply |
| Security | contextIsolation: true, nodeIntegration: false — always |
| Packaging | electron-builder for cross-platform distribution |
| Updates | electron-updater + GitHub Releases |
Electron’s main learning curve is the two-process model and secure IPC. Once that clicks — preload exposes a safe API, renderer calls it, main handles it — everything else follows the patterns you already know from web development.
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Build cross-platform desktop apps with Electron. Covers main/renderer processes, IPC, native APIs, auto-update, performa…
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- React 18 Deep Dive | Concurrent Features· Suspense
- Vite Complete Guide | Fast Dev Server· Build Optimization
Keywords Covered in This Article (Related Search Terms)
This article covers Electron, Desktop, JavaScript, Node.js, Cross-platform, React.