Electron(일렉트론) 앱을 만들 때, 메인 프로세스와 렌더러 프로세스 간의 통신은 핵심입니다.
이 통신을 담당하는 것이 바로 IPC (Inter-Process Communication) 입니다.
하지만 ipcRenderer
, ipcMain
, invoke
, send
, on
, handle
, reply
등등
메서드가 많고 헷갈려서 겉핥기식으로 넘어가면 반드시 후회하게 됩니다.
이 글에서는 개념 → 흐름 → 메서드별 심화까지
입문자도 고급 개발자도 이해할 수 있도록 완전히 정리합니다.
Electron에서 IPC란?
IPC (Inter-Process Communication)는 서로 다른 프로세스 간에 데이터를 주고받는 방법입니다.
Electron은 메인 프로세스와 렌더러 프로세스로 나뉘는데,
서로 다른 역할을 하기에 자연스럽게 통신이 필요합니다.
- 메인 프로세스: 시스템 리소스 접근 (파일 읽기, OS 정보, 앱 제어)
- 렌더러 프로세스: UI 담당 (HTML, CSS, JS)
따라서 렌더러에서 메인에 요청하고, 메인이 응답하는 구조가 빈번하게 발생합니다.
메인 프로세스 vs 렌더러 프로세스
메인 프로세스 (Main) | 렌더러 프로세스 (Renderer) |
Node.js 가능 | DOM 접근 가능 |
하나만 존재 | 여러 개 가능 (BrowserWindow) |
시스템 API 접근 (파일, 네트워크) | 사용자 UI 렌더링 |
ipcMain 모듈 사용 | ipcRenderer 모듈 사용 |
IPC 통신의 기본 흐름
렌더러 → 메인
요청을 보내서 무언가 수행하거나, 데이터를 받아오는 경우.
메인 → 렌더러
작업 완료 알림, 강제 업데이트, 데이터 푸시 등.
통신 방향에 따라 사용하는 메서드가 달라집니다.
IPC 메서드 상세 정리
ipcRenderer.invoke ↔ ipcMain.handle
비동기 요청-응답 통신 (Promise 기반)
ipcRenderer.invoke(channel, ...args)
→ 렌더러가 메인에 요청을 보내고, 결과를 Promise로 기다림ipcMain.handle(channel, listener)
→ 메인이 요청을 받고 처리한 후 결과 반환
예시
렌더러
const result = await ipcRenderer.invoke('get-user-data', userId);
console.log(result);
메인
ipcMain.handle('get-user-data', async (event, userId) => {
const data = await readUserData(userId);
return data;
});
특징
invoke
는 자동으로 응답을 기다립니다.handle
은 등록 시 단 하나만 존재 가능 (중복 등록하면 마지막만 적용)
ipcRenderer.send ↔ ipcMain.on
단방향 통신
ipcRenderer.send(channel, ...args)
→ 렌더러가 메인에 단순 메시지를 보냄 (응답 기다리지 않음)ipcMain.on(channel, listener)
→ 메인이 메시지를 수신
예시
렌더러
ipcRenderer.send('log-message', 'User clicked button');
메인
ipcMain.on('log-message', (event, message) => {
console.log('Received log:', message);
});
특징
- 요청만 보내고 응답은 기본적으로 없음.
- 필요하면 수동으로
event.reply
를 통해 응답할 수 있음.
event.reply
send 통신의 응답 메커니즘
event.reply(channel, ...args)
→ 메인에서 렌더러로 다시 응답을 보내는 방법입니다.
예시
메인
ipcMain.on('ping', (event) => {
event.reply('pong', 'Pong from main!');
});
렌더러
ipcRenderer.on('pong', (event, message) => {
console.log(message); // "Pong from main!"
});
특징
reply
는send
기반 통신에서만 사용합니다.invoke/handle
기반에서는 필요 없습니다.
ipcMain.emit
메인 프로세스 내 이벤트 강제 발생
ipcMain.emit(channel, ...args)
→ 메인 안에서 수동으로 이벤트 발생.
예시
ipcMain.emit('some-channel', {}, 'Manual Event Triggered');
특징
- 테스트나 특별한 처리에 사용.
- 실제 IPC 통신이 아님 (렌더러는 받지 못함).
ipcMain.handleOnce
딱 한 번만 처리하는 핸들러 등록
ipcMain.handleOnce(channel, listener)
→ 한번 요청받고 자동으로 해제됩니다.
예시
ipcMain.handleOnce('fetch-once', async (event, data) => {
return 'One time fetch';
});
특징
- 보안 이슈가 있는 경우나 단발성 요청에 유용.
- 이후 같은 채널로 invoke하면 핸들러가 없기 때문에 에러 발생.
ipcMain.once
단발성 수신 리스너 등록
ipcMain.once(channel, listener)
→ 첫 번째 send를 받고 나서 리스너 자동 해제.
예시
ipcMain.once('first-click', (event, data) => {
console.log('Received first click:', data);
});
특징
- 사용자 동작 중 딱 한번만 받고 싶은 경우.
- 이벤트 중복 수신 방지 가능.
ipcRenderer.on
렌더러에서 메인으로부터 수신하기
ipcRenderer.on(channel, listener)
→ 메인이reply
하거나send
할 때 수신.
예시
ipcRenderer.on('update-available', (event, version) => {
console.log('Update available:', version);
});
특징
- 다수의 이벤트를 구독할 수 있음.
- 메모리 누수 방지를 위해 나중에
removeListener
필요할 수 있음.
메서드 조합 패턴
상황 | 메서드 조합 |
비동기 요청/응답 | invoke + handle |
단순 메시지 송신 | send + on |
한 번만 요청/응답 | invoke + handleOnce |
단발성 이벤트 수신 | send + once |
메인 내부 이벤트 발생 | emit + on |
예제: 파일 읽기 요청
렌더러
async function loadFile(filePath) {
const content = await ipcRenderer.invoke('read-file', filePath);
console.log(content);
}
메인
const fs = require('fs').promises;
ipcMain.handle('read-file', async (event, filePath) => {
const data = await fs.readFile(filePath, 'utf8');
return data;
});
정리
- invoke/handle: 요청과 결과를 깔끔히 처리하는 기본 통신.
- send/on: 단순 통지나 알림용.
- reply: send에 대한 수동 응답.
- handleOnce/once: 단발성 통신.
- emit: 메인 프로세스 내부에서 강제 이벤트 발생.
Electron IPC를 잘 다루는 것은 안정성, 보안, 유지보수성을 높이는 핵심입니다.
특히 파일 접근, 설정 저장, 업데이트 시스템 구축 시 반드시 정확한 통신 설계가 필요합니다.