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) | 무엇이 들어있나 |
|---|---|---|
| Main | 30-50MB | Node.js 런타임 + 우리 main.js + 모든 BrowserWindow 객체 참조 |
| Renderer (창 1개) | 50-150MB | V8 isolate + DOM + 우리 React/Vue + 이미지 캐시 |
| GPU | 60-100MB | Skia + GL 컨텍스트 + 텍스처 캐시 |
| Utility (networkService) | 20-50MB | 분리된 network stack (보안 격리) |
| Helper (Plugin) | 20-30MB | iframe sandbox, plugin |
| Zygote (Linux) | 5-15MB | fork용 템플릿 프로세스 |
창을 한 개 더 띄우면 +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(); // 끝나면 죽이기 → 메모리 회수
});③ 닫힌 창의 참조 해제 (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 가능
});④ 이미지·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 — 시간·메모리에 이어 세 번째 비용 축, 디스크 크기.