01 — What is Electron
이 문서가 답하는 질문: “Electron은 정확히 무엇인가 — 라이브러리? 프레임워크? 브라우저의 한 종류? 어떤 부품으로 만들어졌고, 어떻게 한 프로세스 트리에 산다는 것인가?” 한 줄 답 (Pyramid Top): “Electron은 Chromium 렌더러 + V8 + Node.js + libuv를 한 binary로 묶고 그 사이를 IPC로 연결한 데스크톱 런타임이다. 프레임워크가 아니라 런타임이고, 라이브러리가 아니라 패키지된 실행 환경이다.”
Why — 왜 존재하는가
2013년 GitHub의 Atom 에디터 팀이 마주한 문제는 단순했다 — 데스크톱 앱을 만들어야 하는데 우리는 웹 기술밖에 모른다. 당시 선택지는 셋이었다.
| 선택지 | 문제 |
|---|---|
| 네이티브 (Cocoa·WPF·Qt) | 플랫폼별로 세 번 만들어야 함. 팀의 기술 스택과 거리가 멈 |
| 브라우저 안의 웹 | OS 파일·트레이·메뉴·단축키에 접근 불가. 브라우저가 꺼지면 앱도 꺼짐 |
| Adobe AIR · CEF · NW.js | 죽어가거나, 너무 저수준이거나, 분리가 약함 |
“브라우저 엔진을 직접 가지고 와서, Node를 그 옆에 두고, 둘을 IPC로 묶자.” 이것이 atom-shell의 본래 발상이었다. 한 번 만들어두면 macOS·Windows·Linux 모두에서 같은 코드가 돈다. Node 생태계의 npm 모듈이 그대로 쓰인다. 웹 개발자가 추가 학습 없이 데스크톱 앱을 만든다.
이 발상이 2015년 Electron 1.0으로 일반화되고, 같은 해 Microsoft가 VS Code를 이 위에 올리면서 데스크톱 웹앱의 사실상 표준 런타임이 됐다.
핵심을 다시 정리하면 — Electron은 문제가 아니라 답이다. *“웹 기술을 안 버리고 데스크톱 OS API에 닿을 수 있는가?”*에 대한 답.
| Electron이 아닌 것 | 실제 |
|---|---|
| ”프레임워크” | React·Vue처럼 UI 패턴을 강제하지 않는다. UI 레이어는 네가 고른다 |
| ”Node의 GUI 라이브러리” | Node가 Chromium을 부르는 게 아니라, 둘이 나란히 실행된다 |
| ”Chromium 사용자 정의 빌드” | Chromium 코드를 그대로 쓰되, V8 isolate를 Node와 공유하는 패치만 얹는다 |
| ”브라우저” | URL 바도 없고 history도 없고 같은 origin 정책의 의미도 다르다 |
How — 어떻게 동작하는가
한 프로세스 트리, 여러 역할
Electron 앱이 실행되면 OS는 프로세스 트리를 본다. 부모 프로세스 하나(Main)와 그 아래 자식 프로세스들(Renderer·GPU·Utility)이 함께 산다.
| 프로세스 | 안에 사는 것 | 권한 |
|---|---|---|
| Main | Node.js + libuv + Electron 메인 API (app·BrowserWindow·Menu·Tray) | 가장 강함 — Node 전부 + OS 전부 |
| Renderer | Chromium content process — Blink + V8 + 일부 Web API | 기본은 웹 페이지 수준 — sandbox 켜면 Node 안 보임 |
| Preload | Renderer마다 1개. 격리된 JS 컨텍스트. Node와 Web 둘 다 접근 가능 | 좁고 명시적 — contextBridge로만 노출 |
| GPU | Chromium GPU 프로세스 | GPU 가속·합성 |
| Utility | 네트워킹·오디오·v22+의 사용자 정의 utility | Main이 위임한 만큼 |
이 분리는 Chromium의 멀티프로세스 아키텍처를 그대로 따른다. Chromium은 탭 하나가 죽어도 브라우저가 안 죽도록 Site Isolation을 만들었는데, Electron은 그 사이트 격리 모델을 데스크톱 창 격리 모델로 빌려 쓴다.
한 binary에 들어 있는 부품
electron 명령으로 실행할 때 그 binary가 들고 있는 것들.
| 부품 | 역할 | 출처 |
|---|---|---|
| Chromium content shell | 렌더링·합성·네트워크·DevTools | upstream Chromium |
| V8 | JS 엔진 — Main과 Renderer가 서로 다른 isolate를 가짐 | upstream V8 (Chromium과 같은 버전) |
| Node.js | Main 프로세스의 런타임 (require·CommonJS·ESM·내장 모듈) | upstream Node — Electron 버전마다 묶이는 버전이 정해짐 |
| libuv | Node의 이벤트 루프 — Chromium의 이벤트 루프와 통합 | Node 안 |
| Electron 본체 | 위 부품들을 묶고 추가 API(app·BrowserWindow·ipcMain 등) 제공 | Electron 코드 |
| NAPI native modules ABI | 네이티브 모듈이 Electron 위에서 빌드되도록 하는 인터페이스 | N-API |
여기서 결정적인 사실 한 가지: V8 isolate는 프로세스 단위로 분리된다. Main의 V8과 Renderer의 V8은 완전히 다른 메모리 공간이다. 따라서 Main에서 만든 객체를 Renderer에서 그대로 못 본다 — IPC를 거쳐 직렬화해서 보낸다.
이벤트 루프 통합
Node의 libuv와 Chromium의 message loop은 본래 서로 다른 메커니즘이다. Electron의 가장 비밀스러운 패치 한 줄은 이 둘을 한 스레드 안에서 함께 돌리는 것이다. 그래서 Main 프로세스에서 setTimeout도 동작하고 app.on('ready', ...)도 동작하고 fs.readFile도 동작한다 — 셋이 같은 이벤트 루프를 본다.
이 통합은 historically Chromium의 MessagePump를 libuv가 펌프질하는 방식으로 구현됐다. 자세히 알 필요는 없지만, 왜 한 콜백이 다른 콜백을 막을 수 있는지를 이해하는 데 도움이 된다.
What — 구체 사양·수치·예시
버전 매핑 (대표 LTS)
Electron 메이저 버전마다 Chromium·Node·V8 버전이 고정 묶음으로 따라온다.
| Electron | Chromium | Node | V8 | 출시 |
|---|---|---|---|---|
| 28 | 120 | 18.18 | 12.0 | 2023-12 |
| 29 | 122 | 20.9 | 12.2 | 2024-02 |
| 30 | 124 | 20.11 | 12.4 | 2024-04 |
| 31 | 126 | 20.14 | 12.6 | 2024-06 |
| 32 | 128 | 20.16 | 12.8 | 2024-08 |
| 33 | 130 | 20.18 | 13.0 | 2024-10 |
| 34 | 132 | 20.18 | 13.2 | 2025-01 |
→ Electron 업그레이드는 곧 Chromium·Node 동시 업그레이드다. 이 점이 보안 측면에서 핵심 — Chromium에 0-day가 나오면 Electron 앱도 함께 패치해야 한다. 자세히는 02-history-and-ecosystem에서 다룬다.
디스크 풋프린트 (참고치)
| 항목 | 크기 |
|---|---|
| Electron binary 자체 (압축 전) | ~200 MB |
사용자 앱 코드 (asar 묶기 전) | 보통 1~50 MB |
사용자 앱 코드 (asar 적용 후) | 거의 동일 (단순히 묶을 뿐) |
| 설치된 앱 디스크 점유 (macOS Slack 기준) | ~400 MB |
메모리 풋프린트 (참고치)
| 시나리오 | 메모리 |
|---|---|
| 빈 앱 (Main + Renderer 1개) | 약 90~130 MB |
| Main만 (창 0개) | 약 40~60 MB |
| Renderer 1개 (DOM 거의 비움) | 약 50~80 MB |
| 실제 앱 (VS Code 빈 워크스페이스) | 약 300~500 MB |
→ 메모리는 프로세스 수 × 한 프로세스 비용. BrowserWindow 1개 늘릴 때마다 한 Renderer 프로세스가 추가된다. 자세히는 07-performance-memory에서.
가장 작은 Electron 앱 — 3파일
my-app/
├── package.json
├── main.js
└── index.html// package.json
{
"name": "my-app",
"version": "1.0.0",
"main": "main.js",
"scripts": { "start": "electron ." }
}// main.js ← Main 프로세스 (Node.js + Electron API)
const { app, BrowserWindow } = require('electron');
app.whenReady().then(() => {
const win = new BrowserWindow({ width: 800, height: 600 });
win.loadFile('index.html');
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});{/* index.html ← Renderer 프로세스 (Chromium에서 로드) */}
<!doctype html>
<html><body>
<h1>Hello Electron</h1>
<p>Chromium <span id="chrome"></span> · Node <span id="node"></span></p>
<script>
document.getElementById('chrome').textContent = process.versions.chrome;
document.getElementById('node').textContent = process.versions.node;
</script>
</body></html>npx electron .로 실행하면 — Main 프로세스가 app.whenReady를 기다리고, ready 이벤트가 오면 BrowserWindow를 띄우고, 그 안에서 Renderer가 index.html을 로드해 화면에 그린다.
여기에 깔린 비밀 — 위 index.html의 <script>에서 process.versions가 동작한다. 이건 Node가 Renderer에서 보인다는 뜻이다. 이게 보안적으로 위험한 디폴트라서 Electron 12+부터 contextIsolation: true가 디폴트가 됐고, 이런 코드는 작동하지 않는다. 이걸 풀어내는 게 04-mental-model의 preload 파트.
운영체제 통합 API (Main에서 부르는 것들의 예)
| API | 하는 일 |
|---|---|
app | 앱 라이프사이클 (ready·before-quit·activate) |
BrowserWindow | OS 창 생성·이동·최대화 |
Menu · MenuItem | 네이티브 메뉴바 |
Tray | OS 트레이/메뉴바 아이콘 |
dialog | 파일 열기·저장·alert 다이얼로그 |
shell | 파일 탐색기·기본 브라우저 열기 |
clipboard | OS 클립보드 |
globalShortcut | 전역 단축키 |
Notification | OS 알림 |
powerMonitor | 절전·잠금 이벤트 |
session | 쿠키·캐시·proxy 관리 |
이 API들은 Main에서만 부른다. Renderer가 직접 부르려고 하면 없거나 거부된다 (보안 모델 — 05-security).
What-if — 잘못 쓰면
1) “Electron = Chromium만 있으면 된다”고 보면
증상: 빌드 사이즈를 줄이려고 Node를 빼려 한다 — 나는 fs도 안 쓰고.
원인: Electron은 Chromium과 Node를 하나로 묶은 binary다. Node를 빼면 그건 Electron이 아니다.
대응: 작은 binary가 목표라면 Tauri나 OS webview 기반 도구로. 05-when-to-choose-electron에서 결정 트리.
2) “Renderer는 그냥 브라우저처럼 코드 짜면 된다”고 보면
증상: fetch 호출이 CORS에 막힌다. localStorage가 각 BrowserWindow마다 다르게 보인다. 보안 경고 콘솔에 도배.
원인: Renderer는 브라우저처럼 동작하지만 — 디폴트는 더 빡빡하다. webSecurity · contextIsolation · sandbox 등 보안 옵션이 모두 켜진 상태가 권장 디폴트.
대응: 운영체제·파일에 닿는 코드는 Main으로 옮긴다. CORS는 Main에서 우회하거나 session.webRequest로 헤더를 만진다.
3) “Main과 Renderer가 같은 메모리 공간이라고” 보면
증상: Main에서 만든 EventEmitter를 Renderer에서 그대로 받으려다 실패.
원인: 둘은 서로 다른 V8 isolate. 그 사이엔 직렬화된 메시지만 다닌다. 함수·클래스 인스턴스·Buffer는 전달되지 않거나 복제된다.
대응: ipcRenderer.invoke('do-thing', plainData) → ipcMain.handle('do-thing', (e, plainData) => result). JSON 직렬화 가능한 평범한 객체만.
4) “Electron 버전은 그냥 새 거 쓰면 된다”고 보면
증상: 메이저 버전 올렸더니 native module이 깨지고 빌드가 안 됨.
원인: 네이티브 모듈은 V8/Node ABI에 묶여 컴파일된다. Electron 메이저 버전이 바뀌면 재컴파일해야 한다.
대응: electron-rebuild 또는 @electron/rebuild를 빌드 파이프라인에 둔다. 자세히는 04-native-integration.
Insight — 흥미로운 이야기
”Electron은 Chrome이 아니다 — content shell이다”
많은 사람이 “Electron은 Chrome을 내장한다”고 말하지만 정확히는 Chrome이 아니라 Chromium의 content_shell이다. content_shell은 Chromium 프로젝트가 임베디드 임베딩용으로 만든 최소 셸 — 즐겨찾기·동기화·확장·UI 크롬이 전부 빠진 Chromium 코어. Electron은 그 위에 자기만의 UI 셸(BrowserWindow·Menu·Tray)을 얹는다. 그래서 Electron 앱에 주소창이 안 보이고 즐겨찾기가 없는 이유가 코드 자체에 깔려 있다.
”Node를 Chromium 안에 끼우는 게 가능했던 이유 — V8 공유”
Node도 V8을 쓰고 Chromium도 V8을 쓴다. 같은 엔진 위에 올린 두 런타임이라 이론적으로 한 프로세스에 둘 다 살 수 있었다. 단, libuv의 이벤트 루프와 Chromium의 message loop을 통합하는 패치가 필요했고, 그게 Electron 본체 코드의 가장 중요한 한 줄이다. 같은 시기 동일한 발상을 시도한 NW.js(원래 node-webkit)는 Renderer에 Node를 통째로 노출하는 길을 갔고, Electron은 Main과 Renderer를 분리하는 길을 갔다. 시간이 지나며 분리 모델이 보안적으로 이김.
”BrowserWindow의 이름이 ‘Window’가 아닌 이유”
Electron의 창 객체가 Window가 아니라 BrowserWindow인 것은 — 이건 그냥 OS 창이 아니라 그 안에 Chromium 페이지를 가진 창이라는 사실을 이름으로 못박은 것. 같은 이유로 webContents라는 별도 객체가 있고, BrowserWindow는 OS 창의 추상, webContents는 그 안의 Chromium 페이지의 추상으로 분리된다. 한 BrowserWindow가 N개의 webContents(BrowserView·webview tag 등)를 품을 수 있도록 처음부터 설계된 것. 자세히는 02-window-lifecycle.
요약
Electron은 부품의 묶음이다 — Chromium 렌더러 + V8 + Node.js + libuv를 한 binary로 묶고 IPC로 연결한다. 그 안에는 권한이 다른 여러 프로세스가 한 트리에 살고, 서로 다른 V8 isolate가 메시지로만 대화한다. 웹 기술은 그대로지만 격리·배포·권한 모델은 모두 다시 쓰인다.
다음 문서: 02-history-and-ecosystem — 이 런타임은 어디서 왔고, 지금 누가 쓰는가.