메모리 관리 — 누수 탐지와 최적화
"Electron 앱은 메모리를 많이 먹는다는 편견이 있지만, 관리를 안 하면 진짜 많이 먹는다" — 메모리 누수를 잡으면 사용자 경험이 크게 좋아집니다.
메모리 사용량 모니터링
// 메인 프로세스 메모리 확인
function logMemoryUsage() {
const usage = process.memoryUsage();
console.log({
rss: `${(usage.rss / 1024 / 1024).toFixed(1)} MB`, // 전체 메모리
heapUsed: `${(usage.heapUsed / 1024 / 1024).toFixed(1)} MB`, // 사용 중인 힙
heapTotal: `${(usage.heapTotal / 1024 / 1024).toFixed(1)} MB`, // 전체 힙
external: `${(usage.external / 1024 / 1024).toFixed(1)} MB`, // C++ 객체
});
}
// 주기적 모니터링
setInterval(logMemoryUsage, 30000);
렌더러 메모리 확인
// 모든 렌더러의 메모리 확인
async function getAllProcessMemory() {
const metrics = app.getAppMetrics();
return metrics.map(m => ({
pid: m.pid,
type: m.type,
memory: `${(m.memory.workingSetSize / 1024).toFixed(1)} MB`,
cpu: `${m.cpu.percentCPUUsage.toFixed(1)}%`,
}));
}
메모리 누수의 주요 원인
1. 이벤트 리스너 미정리
// ❌ 리스너가 계속 쌓이는 패턴
function addHandlers(win) {
win.webContents.on('did-finish-load', handleLoad);
ipcMain.on('some-event', handleEvent);
}
// ✅ 윈도우가 닫히면 리스너 정리
function addHandlers(win) {
const loadHandler = () => handleLoad();
const eventHandler = (e, ...args) => handleEvent(e, ...args);
win.webContents.on('did-finish-load', loadHandler);
ipcMain.on('some-event', eventHandler);
win.on('closed', () => {
ipcMain.removeListener('some-event', eventHandler);
});
}
2. 글로벌 참조 미해제
// ❌ Map에 참조가 계속 쌓임
const cache = new Map();
function processData(id, data) {
cache.set(id, data); // 삭제 로직이 없으면 메모리 누수
}
// ✅ WeakMap 사용 또는 캐시 크기 제한
const cache = new Map();
const MAX_CACHE = 100;
function processData(id, data) {
if (cache.size > MAX_CACHE) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(id, data);
}
3. 닫힌 BrowserWindow 참조
// ❌ 윈도우가 닫혀도 참조가 남아있음
let mainWindow = new BrowserWindow({});
// ✅ closed 이벤트에서 참조 해제
mainWindow.on('closed', () => {
mainWindow = null;
});
DevTools로 메모리 누수 탐지
// 렌더러에서 DevTools의 Memory 탭 활용
// 1. Memory 탭 → Heap snapshot 찍기
// 2. 의심 작업 수행
// 3. 다시 Heap snapshot 찍기
// 4. Comparison 뷰로 증가한 객체 확인
GC 강제 실행
// 메인 프로세스에서 GC 강제 실행 (디버그용)
// --expose-gc 플래그 필요
if (global.gc) {
global.gc();
}
// package.json
{
"scripts": {
"start:debug": "electron --expose-gc ."
}
}
대용량 데이터 처리
// ❌ 전체 파일을 메모리에 로드
const data = fs.readFileSync('large-file.json', 'utf-8');
const parsed = JSON.parse(data);
// ✅ 스트림으로 처리
const { createReadStream } = require('fs');
const { pipeline } = require('stream/promises');
async function processLargeFile(filePath) {
const stream = createReadStream(filePath, {
highWaterMark: 64 * 1024, // 64KB 청크
});
for await (const chunk of stream) {
processChunk(chunk);
}
}
면접 포인트 정리
process.memoryUsage()와app.getAppMetrics()로 메모리 모니터링- 이벤트 리스너 미정리, 글로벌 캐시 미제한이 주요 누수 원인
- 닫힌 BrowserWindow의 참조를
null로 설정 - DevTools의 Heap Snapshot으로 누수 탐지
- 대용량 파일은 스트림으로 처리하여 메모리 효율 확보
메모리 관리를 다뤘으면, 다음은 V8 Snapshot 최적화를 알아봅시다.
댓글 로딩 중...