Membangun CatatanKu di Electron: memahami main process dan renderer process, IPC dengan preload dan contextIsolation, serta packaging dengan electron-builder, lengkap dengan praktik app Electron pertama yang aman

Di episode 4 kita membangun CatatanKu di Tauri dengan backend Rust. Kali ini, kita membangun aplikasi yang sama di Electron — framework yang memakai Node.js sebagai backend dan Chromium sebagai renderer. Electron adalah pilihan paling matang di ekosistem desktop: digunakan VS Code, Slack, Discord, dan Figma, dengan dokumentasi serta ekosistem npm terbesar.
Perbedaan kunci dari Tauri: Electron membundel Chromium penuh (ukuran besar, ~100-150 MB) dan menjalankan Node.js di main process. Trade-off-nya: konsistensi renderer di semua sistem (tidak bergantung WebView OS) dan akses penuh ke ekosistem Node.js. Episode ini membangun CatatanKu di Electron dengan praktik keamanan modern: contextIsolation dan preload yang ketat.
ipcRenderer (dari renderer) dan ipcMain (di main) menjadi jembatan. Renderer tidak pernah mengakses sistem secara langsung.Aturan penting: jangan pernah memberi renderer akses langsung ke Node.js (nodeIntegration: true). Renderer bisa dimuat dari konten tak tepercaya — akses Node langsung di sana adalah pintu RCE. Praktik aman modern adalah preload + contextBridge, yang kita pakai di bawah.
mkdir catatanku-electron && cd catatanku-electron
npm init -y
npm install --save-dev electron electron-builder
npm install --save-dev typescript @types/nodeStruktur minimal Electron dengan TypeScript:
catatanku-electron/
├─ src/
│ ├─ main.ts # main process
│ ├─ preload.ts # jembatan aman ke renderer
│ └─ renderer/
│ ├─ index.html
│ └─ app.js # UI
├─ package.jsonimport { app, BrowserWindow, ipcMain } from "electron";
import path from "node:path";
let notes: string[] = [];
function createWindow() {
const win = new BrowserWindow({
width: 900,
height: 600,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
win.loadFile(path.join(__dirname, "renderer/index.html"));
}
app.whenReady().then(() => {
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
ipcMain.handle("notes:list", () => notes);
ipcMain.handle("notes:create", (_event, title: string) => {
notes.push(title);
return notes.length;
});Perhatikan contextIsolation: true dan nodeIntegration: false — ini wajib. Renderer tidak punya akses Node langsung; satu-satunya jembatan adalah preload yang kita tulis sendiri.
Preload dijalankan di renderer sebelum halaman dimuat, dalam konteks terisolasi. Di sinilah kita mengekspos API yang aman:
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("desktop", {
listNotes: (): Promise<string[]> => ipcRenderer.invoke("notes:list"),
createNote: (title: string): Promise<number> =>
ipcRenderer.invoke("notes:create", title),
});Renderer hanya melihat window.desktop.listNotes dan window.desktop.createNote — dua fungsi, bukan akses ke ipcRenderer utuh. Inilah prinsip minimal surface: permukaan API yang diekspos sekecil mungkin, sisanya tidak terlihat.
<!doctype html>
<html>
<body>
<input id="title" placeholder="Judul catatan" />
<button id="add">Tambah</button>
<ul id="notes"></ul>
<script src="app.js"></script>
</body>
</html>const { desktop } = window;
async function refresh() {
const notes = await desktop.listNotes();
document.getElementById("notes").innerHTML = notes
.map((n) => `<li>${n}</li>`)
.join("");
}
document.getElementById("add").addEventListener("click", async () => {
const input = document.getElementById("title");
await desktop.createNote(input.value);
input.value = "";
refresh();
});
refresh();Renderer murni DOM + window.desktop — tidak ada require, tidak ada akses filesystem, tidak ada Node. Semua aksi lewat IPC menuju main process.
Warning
nodeIntegration: true atau preload yang mengekspos ipcRenderer utuh adalah kesalahan keamanan yang sering terjadi di tutorial lama. Jangan pernah menirunya. Selalu contextIsolation: true, nodeIntegration: false, dan ekspos hanya fungsi yang dibutuhkan.
Elektron belum bisa langsung didistribusikan sebagai satu file .exe/.dmg/.deb. Perlu packaging — memasukkan app, Chromium, dan dependensi ke dalam installer. electron-builder adalah tool standarnya:
{
"main": "dist/main.js",
"scripts": {
"build": "tsc",
"start": "electron .",
"dist": "npm run build && electron-builder"
},
"build": {
"appId": "id.catatanku.app",
"productName": "CatatanKu",
"files": ["dist/**/*", "node_modules/**/*"],
"win": { "target": "nsis" },
"mac": { "target": "dmg" },
"linux": { "target": ["AppImage", "deb"] }
}
}npm run dist
# hasil: release/CatatanKu Setup 1.0.0.exe, .dmg, .AppImage, .debDetail packaging (WiX, NSIS, code signing, auto-update) dibahas menyeluruh di episode 12 dan 13. Untuk episode ini, yang penting: electron-builder menghasilkan installer per target OS dengan satu config.
| Aspek | Tauri 2 | Electron |
|---|---|---|
| Backend | Rust native | Node.js |
| Renderer | WebView sistem | Chromium bundel |
| Ukuran biner | Kecil (3-10 MB) | Besar (100-150 MB) |
| Memory footprint | Hemat | Lebih boros |
| Ekosistem | Plugin Rust, berkembang | npm terbesar, matang |
| Keamanan model | Capability-based | Context isolation + preload |
Pilihan tidak ada yang salah — keduanya menyelesaikan masalah berbeda. Tauri unggul di ukuran/keamanan; Electron unggul di konsistensi renderer dan ekosistem. Episode 26 membandingkan keputusan ini dalam konteks 2026.
Inti yang harus dibawa pulang:
contextIsolation: true wajib.ipcMain.handle + ipcRenderer.invoke — permukaan API dijaga sekecil mungkin.Di episode 6 selanjutnya kita membangun CatatanKu ketiga kalinya di Flutter Desktop — satu codebase Dart untuk desktop dan mobile dengan platform channels sebagai jembatannya. Sampai jumpa di episode 6!