[Electron] 성능 최적화: 리소스 효율성과 반응성을 높이는 전략

모듈 로드 최적화

문제점

불필요하거나 무거운 Node.js 모듈을 초기 로드하면 메모리와 초기화 시간이 증가합니다.

전략

  • 필요한 모듈만 선택적으로 로드
  • 동적 임포트(dynamic import)로 필요 시점에 모듈을 가져옴
// Bad Example: 큰 모듈을 전역으로 require
const heavyModule = require('heavy-module'); // 이 모듈은 수백 MB의 데이터를 포함함

// Good Example: 필요할 때 동적으로 로드 (Lazy Loading)
async function loadHeavyModule() {
  return await import('heavy-module');
}

// 사용 시점에 모듈 로드
loadHeavyModule().then(module => {
  module.doHeavyTask();
});

초기화 지연 처리

문제점

앱 시작 시 모든 작업을 동시에 실행하면 초기 로딩 속도가 느려집니다.

전략

  • 필수 작업만 초기화하고, 나머지는 지연 로드(lazy loading)
  • 사용자 인터랙션에 따라 필요한 리소스를 로드
// parser.js: 앱 시작 시 바로 파일 목록을 읽지 않고, 함수 호출 시 읽도록 수정
const fs = require('fs');

class Parser {
  async getFiles() {
    if (!this.files) {
      this.files = await fs.promises.readdir('./data');
    }
    return this.files;
  }

  async parseFiles() {
    const files = await this.getFiles();
    // 여기서 동적으로 heavy parsing 모듈을 로드합니다.
    const parserModule = await import('light-parser');
    return parserModule.parse(files);
  }
}

module.exports = new Parser();

메인 프로세스 차단 방지

문제점

메인 프로세스(컨트롤 타워)는 사용자 입력, 창 관리, IPC 등 핵심 작업을 수행합니다. 긴 작업이나 동기 작업이 이 프로세스를 차단하면 앱 전체가 멈추게 됩니다.

전략

  • 무거운 작업을 워커 스레드나 별도 프로세스로 분리
  • 비동기 API 사용, 동기 IPC 회피
// CPU 집약적 작업은 Worker Thread로 처리
const { Worker } = require('worker_threads');

ipcMain.on("start-heavy-task", (event, data) => {
    const worker = new Worker(
        path.join(__dirname, "worker.js"),
        {
            workerData: data,
        },
    );

    worker.on("message", (result) => {
        event.sender.send("heavy-task-result", result);
    });

    worker.on("error", (error) => {
        event.sender.send("heavy-task-error", error.message);
    });
});

// worker.js (별도 파일)
const { workerData, parentPort } = require('worker_threads');

// 무거운 계산 작업 수행
const result = workerData.map(num => num * 2); 
parentPort?.postMessage(result);

// renderer.js
const { ipcRenderer } = require("electron");

document.getElementById("start-button").addEventListener("click", () => {
    const data = [1, 2, 3, 4, 5];
    ipcRenderer.send("start-heavy-task", data);
});

ipcRenderer.on("heavy-task-result", (event, result) => {
    document.getElementById("result").textContent = `결과: ${result}`;
});

렌더러 프로세스 최적화

문제점

렌더러 프로세스에서는 UI 업데이트, 애니메이션, 스크롤 등 사용자가 직접 경험하는 부분이 동작합니다. 이곳에 긴 작업이 들어가면 앱이 멈추거나 느려집니다.

전략

  • requestIdleCallback(): 브라우저의 idle 상태에 호출될 함수를 대기열에 넣습니다.
  • requestAnimationFrame(): 브라우저에게 수행하기를 원하는 애니메이션을 알립니다.
  • Web Workers: 무거운 계산이나 데이터 처리 작업은 별도의 워커 스레드로 분리합니다.
// requestIdleCallback 사용
// renderer.js
function lowPriorityTask() {
  console.log('낮은 우선순위 작업 실행');
  // 작업 내용
}

if (window.requestIdleCallback) {
  requestIdleCallback(lowPriorityTask);
} else {
  // 지원하지 않는 경우 setTimeout 사용
  setTimeout(lowPriorityTask, 200);
}
// Web Worker 사용
// worker.js
self.onmessage = (e) => {
  const result = e.data.map(item => item * 2); // 예시 작업
  self.postMessage(result);
};
// renderer.js
const worker = new Worker('worker.js');
worker.onmessage = (e) => {
  console.log('Worker 결과:', e.data);
};
worker.postMessage([1, 2, 3, 4, 5]);

네트워크 및 리소스 관리

문제점

불필요한 네트워크 요청이나 폴리필은 초기 로드 시간을 늘립니다.

전략

  • 앱 번들에 포함할 수 있는 리소스는 미리 다운로드하여 포함합니다.
  • 필요 없는 폴리필은 제거하고, 최신 ECMAScript를 사용합니다.
  • 네트워크 요청이 필수적이라면 캐싱 전략을 도입합니다.

코드 번들링으로 로드 속도 개선

문제점

여러 파일로 나누어 require() 호출을 반복하면, 로딩 시간과 메모리 사용량이 증가할 수 있습니다.

전략

Webpack, Parcel, Rollup.js와 같은 번들러를 사용하여 모든 JavaScript 코드를 하나의 파일로 번들링하면 초기 require 호출 오버헤드를 줄일 수 있습니다.

// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/main.js',
  target: 'electron-main',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'production'
};

불필요한 기본 메뉴 제거

Electron은 기본 메뉴를 생성하는데, 필요하지 않은 경우 이를 제거하면 약간의 성능 향상을 기대할 수 있습니다.

// main.js
const { app, BrowserWindow, Menu } = require('electron');

app.whenReady().then(() => {
  const win = new BrowserWindow({
    width: 800,
    height: 600
  });
  // 불필요한 기본 메뉴 제거
  Menu.setApplicationMenu(null);
  win.loadFile('index.html');
});