본문으로 건너뛰기 Electron Complete Guide | Build Desktop Apps with JavaScript

Electron Complete Guide | Build Desktop Apps with JavaScript

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

ConceptKey point
Main processNode.js — creates windows, manages lifecycle
Renderer processChromium — your React/Vue app
PreloadBridge via contextBridge — the only safe IPC path
IPCipcRenderer.invokeipcMain.handle for request/reply
SecuritycontextIsolation: true, nodeIntegration: false — always
Packagingelectron-builder for cross-platform distribution
Updateselectron-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.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Electron, Desktop, JavaScript, Node.js, Cross-platform, React.