출시·고도화 중
Electron 안내서 · 3/6
Electron을 제대로 쓰려면 프로세스 모델과 프로세스 사이의 통신(IPC), 그리고 그 위에 세워진 보안 모델을 이해해야 합니다. 이 장에서는 BrowserWindow, ipcMain · ipcRenderer, contextBridge, 컨텍스트 격리와 보안 점검 목록을 차례로 다룹니다.
Electron은 Chromium의 다중 프로세스 구조를 그대로 물려받았습니다.
app), 창(BrowserWindow), 메뉴, 트레이, 대화 상자 같은 네이티브 기능을 다룹니다.webContents)마다 하나씩 생기며 웹 페이지를 실행합니다. 기본 설정에서는 Node.js에 접근할 수 없습니다.utilityProcess.fork()로 메인 프로세스에서 띄우는 Node.js 자식 프로세스입니다. 무거운 계산이나 충돌 위험이 있는 작업을 메인 프로세스에서 떼어 낼 때 씁니다.메인 프로세스가 오래 걸리는 작업으로 막히면 모든 창이 멈춘 것처럼 보입니다. 무거운 작업은 비동기 API나 유틸리티 프로세스로 옮깁니다.
// main.js
const { utilityProcess } = require('electron')
const path = require('node:path')
const child = utilityProcess.fork(path.join(__dirname, 'worker.js'))
child.on('message', (result) => console.log('결과:', result))
child.postMessage({ numbers: [1, 2, 3] })
// worker.js
process.parentPort.on('message', (event) => {
const sum = event.data.numbers.reduce((a, b) => a + b, 0)
process.parentPort.postMessage(sum)
})BrowserWindow는 창 하나와 그 안의 렌더러를 만듭니다. 크기, 제목 표시줄, 최소 크기 같은 창 옵션과 함께 webPreferences로 렌더러의 보안 설정을 정합니다.
const win = new BrowserWindow({
width: 1000,
height: 700,
minWidth: 600,
show: false, // 내용이 준비된 뒤에 보여 깜빡임을 줄인다
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // 기본값
nodeIntegration: false, // 기본값
sandbox: true, // 기본값
},
})
win.once('ready-to-show', () => win.show())
win.loadFile('index.html')contextIsolation, nodeIntegration: false, sandbox는 현재 Electron의 기본값입니다. 위처럼 적어 두면 누군가 실수로 바꾸는 것을 막는 문서 역할을 합니다.
렌더러는 운영체제 기능을 직접 쓸 수 없으므로, 메인 프로세스에 IPC로 요청합니다. 가장 많이 쓰는 형태는 요청과 응답입니다. 메인 프로세스에서 ipcMain.handle로 처리기를 등록하고, 렌더러 쪽(프리로드)에서 ipcRenderer.invoke로 호출하면 Promise로 결과를 받습니다.
// main.js
const { ipcMain, dialog } = require('electron')
ipcMain.handle('dialog:openFile', async (event) => {
const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openFile'] })
return canceled ? null : filePaths[0]
})// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('api', {
openFile: () => ipcRenderer.invoke('dialog:openFile'),
setTitle: (title) => ipcRenderer.send('set-title', title),
onUpdateCounter: (callback) => {
ipcRenderer.on('update-counter', (_event, value) => callback(value))
},
})응답이 필요 없는 알림은 ipcRenderer.send와 ipcMain.on으로 보냅니다. 반대로 메인 프로세스가 렌더러에 먼저 알릴 때는 win.webContents.send를 쓰고, 프리로드에서 ipcRenderer.on으로 받아 콜백으로 넘깁니다.
// main.js
ipcMain.on('set-title', (event, title) => {
const win = BrowserWindow.fromWebContents(event.sender)
win.setTitle(title)
})
win.webContents.send('update-counter', 1)
// renderer.js
document.getElementById('open').addEventListener('click', async () => {
const filePath = await window.api.openFile()
if (filePath) document.getElementById('file').textContent = filePath
})
window.api.onUpdateCounter((value) => console.log('카운터:', value))컨텍스트 격리가 켜져 있으면 프리로드 스크립트와 웹 페이지는 서로 다른 JavaScript 세계에서 실행됩니다. 페이지의 스크립트가 프리로드의 변수나 Electron API를 몰래 바꿀 수 없다는 뜻입니다. 두 세계를 잇는 유일한 통로가 contextBridge.exposeInMainWorld이며, 이를 통해 넘긴 함수와 값만 페이지에서 보입니다.
이때 ipcRenderer 객체를 통째로 노출하면 페이지가 아무 채널로나 메시지를 보낼 수 있게 되어 격리의 의미가 사라집니다. 위 예제처럼 용도가 분명한 함수만 노출하고, 콜백에 event 객체를 그대로 넘기지 않습니다.
Electron 공식 보안 점검 목록의 핵심은 다음과 같습니다.
nodeIntegration을 켜지 않습니다.contextIsolation과 sandbox를 끄지 않습니다.event.senderFrame)과 인자를 검증합니다.shell.openExternal에는 검증한 주소만 넘깁니다.win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https://example.com/')) shell.openExternal(url)
return { action: 'deny' }
})
win.webContents.on('will-navigate', (event, url) => {
if (!url.startsWith('file://')) event.preventDefault()
})ipcMain.handle · ipcRenderer.invoke, 단방향 알림은 send · on, 메인에서 렌더러로는 webContents.send를 씁니다.contextBridge로 좁은 API만 노출합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.