⚡ Electron0. Electron의 기초01 — What is Electron

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)이 함께 산다.

프로세스안에 사는 것권한
MainNode.js + libuv + Electron 메인 API (app·BrowserWindow·Menu·Tray)가장 강함 — Node 전부 + OS 전부
RendererChromium content process — Blink + V8 + 일부 Web API기본은 웹 페이지 수준 — sandbox 켜면 Node 안 보임
PreloadRenderer마다 1개. 격리된 JS 컨텍스트. Node와 Web 둘 다 접근 가능좁고 명시적 — contextBridge로만 노출
GPUChromium GPU 프로세스GPU 가속·합성
Utility네트워킹·오디오·v22+의 사용자 정의 utilityMain이 위임한 만큼

이 분리는 Chromium의 멀티프로세스 아키텍처를 그대로 따른다. Chromium은 탭 하나가 죽어도 브라우저가 안 죽도록 Site Isolation을 만들었는데, Electron은 그 사이트 격리 모델을 데스크톱 창 격리 모델로 빌려 쓴다.

한 binary에 들어 있는 부품

electron 명령으로 실행할 때 그 binary가 들고 있는 것들.

부품역할출처
Chromium content shell렌더링·합성·네트워크·DevToolsupstream Chromium
V8JS 엔진 — Main과 Renderer가 서로 다른 isolate를 가짐upstream V8 (Chromium과 같은 버전)
Node.jsMain 프로세스의 런타임 (require·CommonJS·ESM·내장 모듈)upstream Node — Electron 버전마다 묶이는 버전이 정해짐
libuvNode의 이벤트 루프 — 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 버전이 고정 묶음으로 따라온다.

ElectronChromiumNodeV8출시
2812018.1812.02023-12
2912220.912.22024-02
3012420.1112.42024-04
3112620.1412.62024-06
3212820.1612.82024-08
3313020.1813.02024-10
3413220.1813.22025-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)
BrowserWindowOS 창 생성·이동·최대화
Menu · MenuItem네이티브 메뉴바
TrayOS 트레이/메뉴바 아이콘
dialog파일 열기·저장·alert 다이얼로그
shell파일 탐색기·기본 브라우저 열기
clipboardOS 클립보드
globalShortcut전역 단축키
NotificationOS 알림
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 — 이 런타임은 어디서 왔고, 지금 누가 쓰는가.