Di Electron, aplikasimu sebenarnya kepecah jadi dua bagian yang kerjanya beda. Satu ngurusin jendela dan tampilan, satunya lagi ngurusin file, jaringan, sampai hal-hal level sistem. IPC inilah jembatan yang bikin dua bagian itu bisa saling ngobrol. Tanpa IPC, keduanya jalan sendiri-sendiri dan nggak bisa tukar data dengan rapi. Yang bakal kita bahas di sini: pola IPC yang aman dan bener buat Electron modern (v28 ke atas). Tapi sebelum mulai, enaknya kamu udah paham dulu pembagian tugas dua proses ini — udah aku bahas panjang di bab arsitektur Electron: main vs renderer.
Apa itu IPC dan kenapa dibutuhkan
IPC itu singkatan dari Inter-Process Communication, alias komunikasi antar-proses. Kenapa antar-proses? Soalnya aplikasi Electron memang jalan di dua jenis proses yang beda.
Yang pertama main process. Ini otaknya aplikasi. Dia yang bikin jendela, ngatur menu, dan punya akses penuh ke Node.js — jadi dia bisa baca file, manggil API sistem, dan seabrek hal lain.
Yang kedua renderer process. Ini wajahnya aplikasi. Tugasnya nampilin HTML, CSS, dan JavaScript di dalam jendela. Anggap aja kayak halaman web biasa yang jalan di browser.
Nah, masalahnya: renderer nggak boleh punya akses langsung ke Node.js. Kalau sampai punya, satu skrip jahat dari internet bisa ngobrak-abrik seluruh file di komputer pengguna. Makanya, tiap kali UI butuh data dari sistem, dia harus minta tolong ke main process. Permintaan tolong itulah yang lewat IPC.
Gampangnya, bayangin pelayan restoran. Pengunjung (renderer) nggak boleh nyelonong masuk dapur. Dia mesen ke pelayan. Pelayan (IPC) yang bawa pesanan ke dapur (main), terus nganterin makanannya balik ke meja.
Pola aman: preload + contextBridge
Sebelum nulis kode IPC-nya, kita butuh jembatan yang aman dulu. Jembatannya ada dua: preload script plus contextBridge.
Preload itu skrip khusus yang jalan duluan, sebelum halaman web-nya kebuka. Dia punya akses terbatas ke Node.js dan API Electron. Tugasnya cuma satu: nyiapin fungsi-fungsi aman yang boleh dipakai UI.
Terus kenapa nggak langsung buka akses Node.js di renderer aja biar praktis? Ya karena bahaya. Setelan kayak nodeIntegration: true itu ngebuka pintu lebar-lebar buat serangan. Sama halnya modul remote jadul yang sekarang udah ditinggalin. Dua-duanya jangan dipakai, titik.
Cara modern yang bener kayak gini:
// main.js
const { app, BrowserWindow } = require('electron')
const path = require('path')
function createWindow() {
const win = new BrowserWindow({
width: 1000,
height: 700,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // wajib aktif
nodeIntegration: false, // jangan dinyalakan
},
})
win.loadFile('index.html')
}
app.whenReady().then(createWindow)
contextIsolation: true itu misahin dunia preload dari dunia halaman web. Jadi kode di halaman nggak bakal bisa ngutak-atik isi preload. Ini dinding pengaman utama kita, jangan sampai lupa.
Habis itu, di preload kita cuma buka fungsi-fungsi yang kita izinin aja, lewat contextBridge.exposeInMainWorld:
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('api', {
getAppVersion: () => ipcRenderer.invoke('app:get-version'),
})
Sekarang di UI udah ada objek window.api. UI cuma boleh manggil fungsi yang kita daftarin tadi, bukan seluruh isi ipcRenderer. Kalau kamu pengin ngulik lebih dalam soal dinding pengaman ini, mampir ke keamanan Electron & contextIsolation.
Renderer ke Main: invoke & handle
Nah, ini bagian serunya. Misal UI pengin tahu versi aplikasi. Info versi ini cuma bisa dibaca dari main process, jadi mau nggak mau UI harus nanya dulu.
Polanya pakai ipcRenderer.invoke di sisi renderer, dan ipcMain.handle di sisi main. Anggap aja kayak tanya-jawab: UI nanya, main njawab, terus jawabannya balik sebagai return value. Karena prosesnya asinkron, kita pakai await.
Di main, kita siapin dulu penanganannya:
// main.js
const { app, ipcMain } = require('electron')
ipcMain.handle('app:get-version', async () => {
return app.getVersion()
})
Coba perhatiin, fungsinya ada return-nya. Nilai yang di-return itulah yang bakal nyampe ke UI. Kamu juga bebas mengembalikan objek, array, atau hasil baca file. Biar lebih kebayang, ini contoh yang agak nyata:
// main.js
const fs = require('fs/promises')
ipcMain.handle('config:read', async () => {
const isi = await fs.readFile('config.json', 'utf-8')
return JSON.parse(isi)
})
Di renderer, kita panggil lewat jembatan tadi. Kode UI-nya nggak nyentuh ipcRenderer langsung, tapi lewat window.api:
// renderer.js
async function tampilkanVersi() {
const versi = await window.api.getAppVersion()
document.querySelector('#versi').textContent = 'Versi: ' + versi
}
tampilkanVersi()
Rapi, kan? UI-nya cukup manggil fungsi biasa, dan detail IPC-nya kesimpen rapi di preload. Pola invoke + handle ini cocok buat apa aja yang butuh balasan: baca file, simpan data, manggil API, atau ngecek status.
Kalau di main tiba-tiba ada error, invoke bakal otomatis nglempar error itu ke renderer. Jadi kamu tinggal bungkus pakai try...catch:
// renderer.js
try {
const cfg = await window.api.readConfig()
} catch (err) {
console.error('Gagal baca config:', err.message)
}
Main ke Renderer: webContents.send & ipcRenderer.on
Arah kebalikannya juga nggak kalah penting. Kadang main process punya kabar yang pengin dia sampaikan ke UI tanpa diminta duluan. Contohnya progres unduhan, atau notifikasi kalau ada data baru masuk.
Buat kasus ini kita nggak pakai invoke. Gantinya, kita pakai win.webContents.send di main buat ngirim, dan ipcRenderer.on di sisi UI buat dengerin.
Misalnya nih, main ngirim progres tiap detik:
// main.js
function kirimProgres(win) {
let persen = 0
const timer = setInterval(() => {
persen += 10
win.webContents.send('unduhan:progres', persen)
if (persen >= 100) clearInterval(timer)
}, 1000)
}
Berhubung UI nggak boleh nyentuh ipcRenderer langsung, kita tambahin satu fungsi pendengar di preload. Cara yang rapi: kasih UI sebuah callback:
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('api', {
getAppVersion: () => ipcRenderer.invoke('app:get-version'),
onProgres: (callback) => {
ipcRenderer.on('unduhan:progres', (_event, persen) => callback(persen))
},
})
Catatan kecil: kita sengaja nggak nerusin objek event mentahan ke UI. Yang kita oper cuma nilai persen-nya doang. Ini lebih aman, soalnya objek event bawa macem-macem hal yang sebenernya nggak perlu diketahui UI.
Terus di renderer, tinggal kita pasang pendengarnya:
// renderer.js
window.api.onProgres((persen) => {
document.querySelector('#bar').style.width = persen + '%'
document.querySelector('#label').textContent = persen + '%'
})
Sekarang tiap kali main ngirim progres baru, tampilan UI langsung ke-update. Pola ini pas banget buat apa aja yang sifatnya "main ngabarin UI": pesan masuk, status koneksi, atau peringatan.
Tips biar IPC tetap aman
IPC itu ibarat pintu. Dan pintu yang kebuka lebar-lebar jelas berisiko. Ini beberapa kebiasaan yang bikin aplikasimu tetep aman.
Batasin jumlah channel. Bikin channel yang bener-bener kamu butuhin aja. Kasih nama yang jelas dan konsisten, misalnya app:get-version atau unduhan:progres. Nama yang berpola gini lebih gampang dilacak nantinya.
Validasi tiap argumen. Jangan langsung percaya sama data yang dateng dari renderer. Cek dulu tipe sama isinya sebelum dipakai. Ini penting banget, apalagi kalau argumennya dipakai buat baca file atau nyusun path.
// main.js
ipcMain.handle('catatan:simpan', async (_event, teks) => {
if (typeof teks !== 'string' || teks.length > 5000) {
throw new Error('Input tidak valid')
}
await fs.writeFile('catatan.txt', teks, 'utf-8')
return { ok: true }
})
Jangan ekspos seluruh ipcRenderer. Ini kesalahan yang sering banget kejadian. Hindari kode kayak exposeInMainWorld('ipc', ipcRenderer). Kalau seluruh ipcRenderer kebuka, UI bisa ngirim ke channel mana aja, termasuk yang sensitif. Mending buka fungsi yang spesifik aja, kayak contoh-contoh kita di atas.
Biarin contextIsolation tetep nyala. Ini pengaman utama yang tadi udah kita bahas. Jangan pernah dimatiin cuma demi praktis sesaat.
Sediain cara buat nglepas pendengar. Kalau kamu masang banyak ipcRenderer.on, siapin juga fungsi buat nglepasnya pas udah nggak kepakai. Ini nyegah kebocoran memori di aplikasi yang jalannya lama.
Dengan kebiasaan-kebiasaan ini, IPC bakal tetep jadi alat yang ampuh, bukan malah jadi celah keamanan.
Penutup
Sampai sini kamu udah pegang inti IPC di Electron. Renderer minta data ke main lewat invoke dan handle, balasannya berupa return value. Sebaliknya, main ngabarin renderer lewat webContents.send dan ipcRenderer.on. Semuanya lewat satu jembatan aman: preload dan contextBridge, dengan contextIsolation yang selalu nyala. Pola kayak gini yang bikin UI tetep kepisah dari Node.js — jadi aplikasimu aman sekaligus rapi. Materi lengkapnya bisa kamu telusurin lewat hub seri Belajar Electron. Di bab depan, IPC yang barusan kita pelajarin ini bakal kita manfaatin buat bikin menu, tray icon, sama notifikasi desktop yang interaktif. Sampai ketemu di sana!