"Electron은 왜 프로세스를 나누는가?" — Chromium이 멀티 프로세스 아키텍처를 쓰는 이유와 같습니다. 하나의 탭이 죽어도 다른 탭은 살아있어야 하니까요.

면접에서 Electron 아키텍처를 물어보면, 단순히 "메인과 렌더러가 있다"보다는 왜 나뉘었는지, 각각 어떤 API에 접근할 수 있는지를 설명할 수 있어야 합니다.


Chromium의 멀티 프로세스 모델

Electron은 Chromium의 멀티 프로세스 아키텍처를 그대로 사용합니다.

PLAINTEXT
┌─────────────────────────────────┐
│         메인 프로세스            │  ← Node.js 런타임
│  (Browser Process)              │  ← 앱 라이프사이클 관리
│                                 │  ← 시스템 API 접근
├─────────────────────────────────┤
│                                 │
│  ┌──────────┐  ┌──────────┐    │
│  │ 렌더러 1  │  │ 렌더러 2  │   │  ← 각 BrowserWindow
│  │(Renderer)│  │(Renderer)│    │  ← 웹 페이지 렌더링
│  └──────────┘  └──────────┘    │
│                                 │
└─────────────────────────────────┘

왜 프로세스를 나누는가

  1. **안정성 **: 하나의 렌더러가 크래시해도 메인 프로세스와 다른 렌더러에 영향 없음
  2. ** 보안 **: 렌더러에서 직접 시스템 API에 접근하지 못하게 격리
  3. ** 성능 **: 각 렌더러가 독립적으로 동작하므로 한 창의 무거운 작업이 다른 창에 영향을 주지 않음

메인 프로세스 (Main Process)

메인 프로세스는 Electron 앱에서 ** 단 하나만 존재 **합니다. package.jsonmain 필드가 가리키는 파일에서 시작됩니다.

메인 프로세스가 하는 일

JAVASCRIPT
const { app, BrowserWindow, Menu, dialog } = require('electron');

// 1. 앱 라이프사이클 관리
app.whenReady().then(() => {
  // 2. BrowserWindow 생성/관리
  const win = new BrowserWindow({ width: 800, height: 600 });

  // 3. 네이티브 메뉴 설정
  const menu = Menu.buildFromTemplate([
    { label: '파일', submenu: [{ role: 'quit' }] }
  ]);
  Menu.setApplicationMenu(menu);

  // 4. 시스템 다이얼로그
  // dialog.showOpenDialog(win, { properties: ['openFile'] });
});

메인 프로세스에서만 접근 가능한 모듈

모듈역할
app앱 라이프사이클
BrowserWindow윈도우 생성/관리
Menu / Tray네이티브 메뉴, 트레이 아이콘
dialog파일 열기/저장 다이얼로그
globalShortcut전역 키보드 단축키
autoUpdater자동 업데이트

렌더러 프로세스 (Renderer Process)

BrowserWindow는 자체 렌더러 프로세스를 가집니다. 렌더러 프로세스는 기본적으로 ** 브라우저 환경 **입니다.

JAVASCRIPT
// 렌더러에서 실행되는 코드 — 일반 웹 코드와 동일
document.getElementById('btn').addEventListener('click', () => {
  console.log('버튼이 클릭되었습니다');
  // Node.js API 직접 사용 불가 (보안상)
  // require('fs') ← 이런 코드는 동작하지 않음
});

렌더러 프로세스의 특징

  • 일반 웹 페이지와 동일한 환경 (DOM, Web API 사용 가능)
  • ** 기본적으로 Node.js API에 접근 불가** (보안을 위해)
  • 메인 프로세스와 통신하려면 IPC(Inter-Process Communication) 사용
  • 각 렌더러는 독립된 프로세스로 동작

Preload 스크립트

Preload 스크립트는 메인과 렌더러 사이의 ** 안전한 브릿지** 역할을 합니다.

JAVASCRIPT
// preload.js
const { contextBridge, ipcRenderer } = require('electron');

// contextBridge로 렌더러에 안전하게 API를 노출합니다
contextBridge.exposeInMainWorld('electronAPI', {
  // 파일 열기 요청을 메인 프로세스로 전달
  openFile: () => ipcRenderer.invoke('dialog:openFile'),

  // 앱 버전 가져오기
  getVersion: () => ipcRenderer.invoke('app:getVersion'),
});
JAVASCRIPT
// 렌더러에서 사용 — window.electronAPI로 접근 가능
const filePath = await window.electronAPI.openFile();

Preload 스크립트의 특성

  • Node.js API 접근 가능 (require, process 등)
  • DOM 접근 가능 (window, document 등)
  • 두 세계를 모두 접근할 수 있지만, contextBridge로 필요한 것만 노출하는 것이 보안 원칙
  • BrowserWindow 생성 시 webPreferences.preload로 지정

프로세스 간 통신 흐름

PLAINTEXT
렌더러                   Preload              메인 프로세스
  │                        │                      │
  │  window.electronAPI    │                      │
  │  .openFile()           │                      │
  │ ─────────────────────► │                      │
  │                        │  ipcRenderer         │
  │                        │  .invoke('open')     │
  │                        │ ───────────────────► │
  │                        │                      │  dialog
  │                        │                      │  .showOpenDialog()
  │                        │  결과 반환            │
  │                        │ ◄─────────────────── │
  │  Promise 해결           │                      │
  │ ◄───────────────────── │                      │

nodeIntegration은 왜 끄는가

Electron 초기에는 nodeIntegration: true로 설정하면 렌더러에서 직접 require('fs') 같은 Node.js API를 쓸 수 있었습니다.

JAVASCRIPT
// ⚠️ 위험한 설정 — 절대 프로덕션에서 사용하지 마세요
const win = new BrowserWindow({
  webPreferences: {
    nodeIntegration: true,     // 렌더러에서 Node.js 사용 가능
    contextIsolation: false,   // 컨텍스트 격리 해제
  },
});

이게 왜 위험한지:

  • 렌더러에서 외부 스크립트를 로드하면 그 스크립트가 파일 시스템에 접근 가능
  • XSS 공격이 시스템 전체를 장악할 수 있음
  • ** 반드시 contextIsolation: true (기본값)을 유지하고 preload를 사용해야 합니다**

유틸리티 프로세스

Electron 22부터는 UtilityProcess를 사용해 무거운 작업을 별도 프로세스에서 처리할 수 있습니다.

JAVASCRIPT
// main.js에서 유틸리티 프로세스 생성
const { utilityProcess } = require('electron');

const child = utilityProcess.fork(path.join(__dirname, 'heavy-task.js'));
child.postMessage({ type: 'start', data: largeData });

child.on('message', (result) => {
  console.log('작업 완료:', result);
});
JAVASCRIPT
// heavy-task.js — 별도 프로세스에서 실행
process.parentPort.on('message', (e) => {
  const { type, data } = e.data;
  // CPU 집약적인 작업 수행
  const result = processLargeData(data);
  process.parentPort.postMessage(result);
});

면접 포인트 정리

  • 메인 프로세스는 1개, 렌더러 프로세스는 BrowserWindow당 1개
  • 메인: 앱 라이프사이클, 시스템 API / 렌더러: UI 렌더링
  • preload 스크립트는 두 프로세스를 안전하게 연결하는 브릿지
  • contextIsolation: true + contextBridge가 현재 권장 보안 패턴
  • nodeIntegration: true는 보안 취약점이므로 사용 금지
  • 무거운 작업은 UtilityProcess로 분리 가능

프로세스 모델을 이해했으면, 다음은 BrowserWindow의 다양한 옵션을 알아볼 차례입니다.

댓글 로딩 중...