"IPC는 Electron의 혈관" — 메인과 렌더러가 분리되어 있으니, 데이터를 주고받으려면 반드시 IPC를 거쳐야 합니다.

면접에서 Electron의 통신 방식을 물어보면, invokesend의 차이를 명확히 설명할 수 있어야 합니다.


IPC 통신의 세 가지 패턴

패턴방향반환값용도
invoke / handle렌더러 → 메인 → 렌더러Promise요청-응답 (가장 권장)
send / on렌더러 → 메인없음단방향 알림
webContents.send메인 → 렌더러없음메인에서 푸시

패턴 1: invoke / handle (양방향)

가장 많이 쓰이는 패턴입니다. 렌더러가 메인에 요청하고 결과를 받습니다.

JAVASCRIPT
// main.js — 핸들러 등록
const { ipcMain, dialog } = require('electron');

ipcMain.handle('dialog:openFile', async (_event, options) => {
  const result = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: options?.filters || [],
  });
  return result.filePaths[0] || null;
});

ipcMain.handle('app:getVersion', () => {
  return app.getVersion();
});
JAVASCRIPT
// preload.js — 브릿지 설정
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  openFile: (options) => ipcRenderer.invoke('dialog:openFile', options),
  getVersion: () => ipcRenderer.invoke('app:getVersion'),
});
JAVASCRIPT
// renderer.js — 렌더러에서 사용
async function handleOpenFile() {
  const filePath = await window.electronAPI.openFile({
    filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
  });
  if (filePath) {
    console.log('선택한 파일:', filePath);
  }
}

invoke가 권장되는 이유

  • **Promise 기반 **: async/await로 깔끔하게 처리 가능
  • ** 에러 전파 **: 메인에서 throw하면 렌더러에서 catch 가능
  • ** 타이밍 보장 **: 응답이 올 때까지 기다릴 수 있음

패턴 2: send / on (렌더러 → 메인, 단방향)

응답이 필요 없는 단방향 메시지에 사용합니다.

JAVASCRIPT
// main.js — 수신
const { ipcMain } = require('electron');

ipcMain.on('log:write', (_event, level, message) => {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] [${level}] ${message}`);
});

ipcMain.on('analytics:track', (_event, eventName, data) => {
  // 분석 데이터 전송 — 응답 불필요
  sendToAnalytics(eventName, data);
});
JAVASCRIPT
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  log: (level, message) => ipcRenderer.send('log:write', level, message),
  track: (event, data) => ipcRenderer.send('analytics:track', event, data),
});
JAVASCRIPT
// renderer.js
window.electronAPI.log('info', '페이지 로드 완료');
window.electronAPI.track('button_click', { buttonId: 'submit' });

패턴 3: webContents.send (메인 → 렌더러)

메인 프로세스에서 렌더러로 이벤트를 ** 푸시 **합니다.

JAVASCRIPT
// main.js — 메인에서 렌더러로 메시지 전송
function startDownload(win, url) {
  const download = new DownloadManager(url);

  download.on('progress', (percent) => {
    // 렌더러에 진행률 알림
    win.webContents.send('download:progress', percent);
  });

  download.on('complete', (filePath) => {
    win.webContents.send('download:complete', filePath);
  });

  download.on('error', (error) => {
    win.webContents.send('download:error', error.message);
  });
}
JAVASCRIPT
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  onDownloadProgress: (callback) => {
    const handler = (_event, percent) => callback(percent);
    ipcRenderer.on('download:progress', handler);
    return () => ipcRenderer.removeListener('download:progress', handler);
  },
  onDownloadComplete: (callback) => {
    ipcRenderer.once('download:complete', (_event, path) => callback(path));
  },
  onDownloadError: (callback) => {
    ipcRenderer.once('download:error', (_event, msg) => callback(msg));
  },
});
JAVASCRIPT
// renderer.js
const removeProgressListener = window.electronAPI.onDownloadProgress((percent) => {
  progressBar.style.width = `${percent}%`;
  progressText.textContent = `${percent}% 완료`;
});

window.electronAPI.onDownloadComplete((path) => {
  removeProgressListener();  // 리스너 정리
  alert(`다운로드 완료: ${path}`);
});

에러 처리

invoke의 에러 전파

JAVASCRIPT
// main.js
ipcMain.handle('file:read', async (_event, filePath) => {
  try {
    const content = await fs.promises.readFile(filePath, 'utf-8');
    return { success: true, data: content };
  } catch (error) {
    // 방법 1: 에러를 throw → 렌더러에서 catch
    throw new Error(`파일을 읽을 수 없습니다: ${error.message}`);

    // 방법 2: 에러 객체를 반환 (권장)
    // return { success: false, error: error.message };
  }
});
JAVASCRIPT
// renderer.js — 에러 처리
try {
  const result = await window.electronAPI.readFile('/some/path');
  if (result.success) {
    displayContent(result.data);
  } else {
    showError(result.error);
  }
} catch (error) {
  // invoke에서 throw된 에러가 여기로 옵니다
  showError(error.message);
}

sendSync — 가급적 사용하지 마세요

ipcRenderer.sendSync는 동기적으로 메인에 요청하고 응답을 기다립니다.

JAVASCRIPT
// ⚠️ 권장하지 않음 — 렌더러가 블로킹됨
ipcMain.on('sync:getConfig', (event) => {
  event.returnValue = { theme: 'dark', language: 'ko' };
});

// 렌더러에서
const config = ipcRenderer.sendSync('sync:getConfig');
// 메인이 응답할 때까지 렌더러가 멈춤!

** 사용하지 않는 것이 좋은 이유:**

  • 렌더러 프로세스가 ** 완전히 블로킹 **됨
  • 메인 프로세스가 바쁘면 UI가 얼어버림
  • invoke로 대체 가능

IPC 데이터 직렬화

IPC로 전달되는 데이터는 ** 구조화된 복제 알고리즘 **(Structured Clone)으로 직렬화됩니다.

JAVASCRIPT
// ✅ 전달 가능한 데이터
ipcRenderer.invoke('save', {
  name: '문서',           // 문자열
  count: 42,              // 숫자
  tags: ['a', 'b'],       // 배열
  metadata: { key: 'v' }, // 중첩 객체
  created: new Date(),    // Date 객체
  buffer: Buffer.from('hello'), // Buffer
});

// ❌ 전달 불가능한 데이터
ipcRenderer.invoke('save', {
  callback: () => {},     // 함수는 직렬화 불가
  symbol: Symbol('x'),    // Symbol 불가
  element: document.body, // DOM 요소 불가
});

실전 패턴: IPC 채널 관리

프로젝트가 커지면 IPC 채널을 체계적으로 관리해야 합니다.

JAVASCRIPT
// shared/ipc-channels.js — 채널 상수 관리
const IPC_CHANNELS = {
  FILE: {
    READ: 'file:read',
    WRITE: 'file:write',
    DELETE: 'file:delete',
  },
  APP: {
    GET_VERSION: 'app:getVersion',
    QUIT: 'app:quit',
  },
  DIALOG: {
    OPEN_FILE: 'dialog:openFile',
    SAVE_FILE: 'dialog:saveFile',
  },
};

module.exports = { IPC_CHANNELS };

면접 포인트 정리

  • invoke/handle: 양방향, Promise 반환, 가장 권장되는 패턴
  • send/on: 단방향(렌더러→메인), 응답 불필요할 때
  • webContents.send: 메인→렌더러 푸시 (진행률, 알림 등)
  • sendSync는 렌더러를 블로킹하므로 사용 자제
  • IPC 데이터는 Structured Clone으로 직렬화 (함수 전달 불가)
  • 채널명은 상수로 관리하면 유지보수가 편함

IPC 기초를 익혔으면, 다음은 메뉴와 트레이 아이콘 구현을 알아봅시다.

댓글 로딩 중...