"Electron 앱은 메모리를 많이 먹는다는 편견이 있지만, 관리를 안 하면 진짜 많이 먹는다" — 메모리 누수를 잡으면 사용자 경험이 크게 좋아집니다.


메모리 사용량 모니터링

JAVASCRIPT
// 메인 프로세스 메모리 확인
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);

렌더러 메모리 확인

JAVASCRIPT
// 모든 렌더러의 메모리 확인
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. 이벤트 리스너 미정리

JAVASCRIPT
// ❌ 리스너가 계속 쌓이는 패턴
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. 글로벌 참조 미해제

JAVASCRIPT
// ❌ 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 참조

JAVASCRIPT
// ❌ 윈도우가 닫혀도 참조가 남아있음
let mainWindow = new BrowserWindow({});

// ✅ closed 이벤트에서 참조 해제
mainWindow.on('closed', () => {
  mainWindow = null;
});

DevTools로 메모리 누수 탐지

JAVASCRIPT
// 렌더러에서 DevTools의 Memory 탭 활용
// 1. Memory 탭 → Heap snapshot 찍기
// 2. 의심 작업 수행
// 3. 다시 Heap snapshot 찍기
// 4. Comparison 뷰로 증가한 객체 확인

GC 강제 실행

JAVASCRIPT
// 메인 프로세스에서 GC 강제 실행 (디버그용)
// --expose-gc 플래그 필요
if (global.gc) {
  global.gc();
}

// package.json
{
  "scripts": {
    "start:debug": "electron --expose-gc ."
  }
}

대용량 데이터 처리

JAVASCRIPT
// ❌ 전체 파일을 메모리에 로드
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 최적화를 알아봅시다.

댓글 로딩 중...