⚡ Electron7. 성능 & 메모리02 · Memory footprint — 프로세스 수 × 단가

02 · Memory footprint — 프로세스 수 × 단가

질문: Activity Monitor에서 Slack Helper가 8개 뜨는 건 정상인가? 메모리 500MB는 누수인가? 한 줄 답: Electron 메모리 = 프로세스 수 × 한 프로세스 비용. Slack Helper 8개는 정상이고(창·utility·GPU·zygote), 500MB도 그 수의 곱이라 누수가 아닐 수 있다. 줄이는 첫 자리는 창을 늘리지 말고 재사용, 둘째는 utility로 무거운 일 분리, 셋째는 닫힌 창의 webContents 참조 해제.


Pyramid Top

Electron은 한 프로세스로 동작하지 않는다 — Main 1 + Renderer N + Preload N(같은 프로세스에 살음) + GPU 1 + Utility k + Zygote(Linux). 메모리는 프로세스 마다 separate RSS이고 Activity Monitor가 그것을 모두 더해서 보여준다.

“우리 앱이 메모리 500MB를 먹는다”는 말은 프로세스 수와 단가를 같이 봐야 의미를 갖는다 — 7개 프로세스 × 70MB = 490MB는 디자인대로 작동 중이고, 1개 프로세스가 500MB면 진짜 누수다.


사고 흐름


Why — 프로세스 단위가 메모리 단위인 이유

Chromium은 site isolation 보안 모델 위에 있다 — origin마다 다른 V8 isolate, 다른 메모리 공간. Electron은 그 모델을 창 단위로 확장했고, 한 창은 한 Renderer 프로세스다 (기본).

프로세스메모리 단가 (RSS)무엇이 들어있나
Main30-50MBNode.js 런타임 + 우리 main.js + 모든 BrowserWindow 객체 참조
Renderer (창 1개)50-150MBV8 isolate + DOM + 우리 React/Vue + 이미지 캐시
GPU60-100MBSkia + GL 컨텍스트 + 텍스처 캐시
Utility (networkService)20-50MB분리된 network stack (보안 격리)
Helper (Plugin)20-30MBiframe sandbox, plugin
Zygote (Linux)5-15MBfork용 템플릿 프로세스

창을 한 개 더 띄우면 +80MB가 더 붙는다. 이건 디자인이다 — 한 창이 죽어도 다른 창이 살기 위한 격리 비용.


RSS vs PSS — 두 개의 메모리 단위

Activity Monitor·Task Manager가 어떤 수치를 보여주는지가 중요하다.

단위무엇함정
RSS (Resident Set Size)이 프로세스가 물리 메모리에 올린 페이지의 합공유 라이브러리가 여러 프로세스에 중복 계산됨
PSS (Proportional Set Size)공유 페이지를 프로세스 수로 나눠 분담Linux /proc/[pid]/smaps로만
Private Memory이 프로세스만의 메모리 (공유 제외)진짜 내가 차지한 것
Working Set (Windows)최근 사용된 페이지윈도우 Task Manager 기본값

macOS Activity Monitor의 “Memory” 열compressed + footprint라 RSS와 다르다. Chromium 8개 helper가 각각 80MB로 보이면 — 실제 물리 메모리는 그것의 절반쯤이다 (Electron 코드가 공유 매핑).

과대평가 패턴: “Slack이 2GB를 먹어요!” → 보통 RSS 합. 실제 PSS는 1.2GB 정도. 그래도 많지만, “2GB”는 부풀려졌다.


How — 측정 코드

Main 프로세스에서 모든 자식의 메모리 한 번에

const { app } = require('electron');
 
setInterval(async () => {
  const metrics = app.getAppMetrics();
  // metrics: [{ pid, type, memory: { workingSetSize, peakWorkingSetSize } }, ...]
  console.table(metrics.map(m => ({
    pid: m.pid,
    type: m.type,           // 'Browser' | 'Tab' | 'Utility' | 'GPU'
    name: m.name,
    workingSetKB: m.memory.workingSetSize,
    peakKB: m.memory.peakWorkingSetSize,
  })));
}, 5000);

Renderer/Main 안에서 자기 메모리

// Node 표준 — Main 또는 preload에서 가능
const m = process.memoryUsage();
// { rss, heapTotal, heapUsed, external, arrayBuffers }
console.log('Heap used:', (m.heapUsed / 1024 / 1024).toFixed(1), 'MB');
console.log('RSS:', (m.rss / 1024 / 1024).toFixed(1), 'MB');

webContents.getProcessMemoryInfo() (deprecated → app.getAppMetrics 사용)

이전 API. 현재는 app.getAppMetrics()가 통합 진입점.


What — 줄이는 4가지 자리

① 창을 늘리지 마라 — 가장 큰 자리

// 나쁨: 클릭마다 새 창
ipcMain.handle('open-doc', (_, docId) => {
  const win = new BrowserWindow({ /* ... */ });  // +80MB
  win.loadURL(`...?id=${docId}`);
});
 
// 좋음: 한 창 안에서 라우팅
ipcMain.handle('open-doc', (_, docId) => {
  mainWindow.webContents.send('navigate', `/doc/${docId}`);
});

근거: VS Code는 수십 개 파일을 열어도 창 1개다 — 각 파일을 으로. Slack도 워크스페이스는 탭으로 (이전엔 창 분리였고 메모리 폭증 사고가 있었다).

② 무거운 일은 UtilityProcess로 분리

// Main이 zip 압축을 직접 하면 → Main 메모리 200MB+
const { utilityProcess } = require('electron');
 
const child = utilityProcess.fork(path.join(__dirname, 'zip-worker.js'));
child.postMessage({ paths });
child.on('message', (result) => {
  child.kill(); // 끝나면 죽이기 → 메모리 회수
});

자세히: 01-process-model/05-utility-and-gpu-process

③ 닫힌 창의 참조 해제 (leak 1순위)

// 나쁨: closures가 win을 잡고 있음
let win = new BrowserWindow();
someService.on('event', () => win.webContents.send('x'));
win.on('closed', () => { /* win이 globals에 남음 */ });
 
// 좋음
let win = new BrowserWindow();
const onEvent = () => win?.webContents.send('x');
someService.on('event', onEvent);
win.on('closed', () => {
  someService.off('event', onEvent);
  win = null;  // GC 가능
});

자세히: 06-leaks-and-profiling

④ 이미지·blob 캐시 통제

const win = new BrowserWindow({
  webPreferences: {
    backgroundThrottling: true, // 백그라운드 창은 메모리 적게
  },
});
 
// 주기적 정리
win.webContents.session.clearCache();  // 디스크 캐시 (자주 호출 X)

What-if — 메모리 무관심의 결과

시나리오결과
사용자가 우리 앱 + Slack + Discord + VS Code 같이 켰는데, 우리만 2GB사용자가 우리 앱 먼저 끈다. 평판: “이 앱은 메모리 먹는 괴물”
8GB RAM 노트북에서 전체 메모리의 30%OS가 swap → 디스크 IO → 전 앱 freeze. 사용자는 우리를 의심.
백그라운드에서 메모리 계속 증가사용자가 주에 한 번 앱 재시작하는 운영 부담. backend 앱만 그래야 정상.
창 5개 띄우는 워크플로우 → 합 600MB회사 보안팀이 “리소스 과다 사용”으로 전사 차단. (실제 사례 다수)

흥미로운 이야기

“Electron uses 8 helpers because that’s how Chromium works”

2019년 Hacker News에서 Slack helper 프로세스 8개를 두고 거대한 논쟁이 있었다. 사용자들은 “왜 메신저 하나가 helper를 8개나 띄우냐”고 분노했고, Slack 엔지니어가 직접 댓글로 — “4개는 워크스페이스마다 하나, 2개는 utility(network·storage 분리), 1개는 GPU, 1개는 zygote다. 이건 Chromium의 multi-process 모델이고, Slack이 만든 것이 아니다” 라고 답했다. 같은 시기 Discord도 비슷한 비난을 받았고, 그들의 답은 helper의 수를 줄이지 않고 단가를 줄이는 쪽이었다 — 워크스페이스 간 Service Worker 공유로 코드 중복 제거, backgroundThrottling으로 비활성 창 메모리 압축. 교훈: 사용자는 helper 개수를 보고 평가하지만, 실제 비용은 RSS 합이다. UI(개수)가 진짜 문제(RSS)보다 먼저 인식된다는 게 데스크톱 앱 운영의 잔혹한 진실이다.


Insight — 메모리는 N × 단가

대부분의 팀이 단가 깎기(이미지 압축, 코드 줄이기)에 시간을 쓰지만 — 진짜 효과N 줄이기(창 재사용)에 있다. 한 자리 결정이 고정 비용 80MB를 자른다.


한 단락 요약

Electron 메모리는 프로세스 수 × 단가다. 창 1개 ~80MB라 5창 ~400MB가 정상 — 누수가 아니라 디자인 비용. 줄이는 자리는 4개: 창 재사용(가장 큼) · utility 분리 · 닫힌 창 참조 해제(leak 1순위) · 이미지 캐시 통제. RSS와 PSS는 다르고, Activity Monitor 수치는 공유 메모리를 중복 카운트부풀려져 보인다는 점을 팀 외부에 설명할 줄 알아야 한다. 다음: 03-bundle-size — 시간·메모리에 이어 세 번째 비용 축, 디스크 크기.