MessagePorts의 기본 개념
MessageChannel은 두 개의 연결된 MessagePort 객체를 생성합니다. 이 두 포트는 서로 메시지를 주고받을 수 있으며, 포트는 웹 표준의 window.postMessage와 유사한 방식으로 작동합니다. Electron에서는 ipcRenderer.postMessage와 WebContents.postMessage 메서드를 통해 MessagePorts를 전송할 수 있습니다.
MessagePort는 postMessage로 데이터를 보내고, onmessage로 수신하며, 일반 IPC와 달리 채널 자체를 전달해 지속적인 통신이 가능합니다.
렌더러에서 메인으로 MessagePort 전달
렌더러에서 MessagePort를 생성해 메인 프로세스에 전달하는 예제입니다. 메인에서 사용자 입력을 받아 렌더러에 실시간으로 반영합니다.
// preload.js
const { ipcRenderer } = require('electron');
// MessageChannel 생성: portA와 portB 쌍을 생성합니다.
const channel = new MessageChannel();
const portA = channel.port1;
const portB = channel.port2;
// 포트B를 사용해 미리 메시지를 전송할 수 있습니다.
portB.postMessage({ info: '초기 데이터' });
// 포트A를 메인 프로세스로 전송 (배열 형태로 전달)
ipcRenderer.postMessage('transfer-port', null, [portA]);
// 나중에 portB를 사용해 메인에서 오는 메시지를 수신합니다.
portB.onmessage = (event) => {
console.log('렌더러 수신:', event.data);
};
// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 900,
height: 700,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
win.loadFile('index.html');
}
app.whenReady().then(() => {
// 'transfer-port' 채널에서 MessagePort를 수신합니다.
ipcMain.on('transfer-port', (event) => {
// 이벤트의 ports 배열에서 전송받은 포트를 추출합니다.
const receivedPort = event.ports[0];
// 메인 프로세스에서는 MessagePortMain API를 사용합니다.
receivedPort.on('message', (msgEvent) => {
console.log('메인 수신:', msgEvent.data);
// 예: 받은 데이터를 처리 후 응답 전송
receivedPort.postMessage({ reply: '메인에서 응답합니다.' });
});
// 포트의 메시지 큐를 시작합니다.
receivedPort.start();
});
createWindow();
});
두 렌더러 간 MessageChannel 연결
메인 프로세스를 중계자로 활용하여 두 렌더러 간 직접 통신을 구현할 수 있습니다. 아래 예제는 메인에서 MessageChannel을 생성하고, 각 포트를 서로 다른 렌더러에 전달하는 방식입니다.
// main.js
const { app, BrowserWindow, MessageChannelMain } = require('electron');
const path = require('path');
let windowA, windowB;
app.whenReady().then(() => {
windowA = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preloadA.js'),
contextIsolation: true
}
});
windowB = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preloadB.js'),
contextIsolation: true
}
});
windowA.loadFile('indexA.html');
windowB.loadFile('indexB.html');
// MessageChannelMain 생성: 두 포트를 각각 창에 전달
const { port1, port2 } = new MessageChannelMain();
windowA.webContents.once('did-finish-load', () => {
windowA.webContents.postMessage('connect-channel', null, [port1]);
});
windowB.webContents.once('did-finish-load', () => {
windowB.webContents.postMessage('connect-channel', null, [port2]);
});
});
// preloadA.js (Renderer A)
const { ipcRenderer } = require('electron');
ipcRenderer.on('connect-channel', (event) => {
window.sharedPort = event.ports[0];
window.sharedPort.onmessage = (e) => {
console.log('Renderer A received:', e.data);
};
window.sharedPort.start();
});
// preloadB.js (Renderer B)
const { ipcRenderer } = require('electron');
ipcRenderer.on('connect-channel', (event) => {
window.sharedPort = event.ports[0];
window.sharedPort.onmessage = (e) => {
console.log('Renderer B received:', e.data);
};
window.sharedPort.start();
});
// indexA.html 내의 renderer script (rendererA.js)
window.sharedPort.postMessage({ msg: '안녕하세요, Renderer B!' });
Renderer B에서는 이를 수신하여 콘솔에 출력할 것입니다.
Worker와의 MessagePort 통신
Electron에서는 별도의 작업을 수행하기 위해 숨겨진 BrowserWindow(Worker)를 사용하여, 메인 창과 직접 통신할 수 있습니다. 아래 예제는 메인 창이 Worker와 MessageChannel을 통해 데이터를 주고받는 방식입니다.
// main.js
const { app, BrowserWindow, MessageChannelMain } = require('electron');
const path = require('path');
let mainWindow, workerWindow;
app.whenReady().then(async () => {
workerWindow = new BrowserWindow({
show: false,
webPreferences: {
preload: path.join(__dirname, "worker.js"),
nodeIntegration: true // 워커는 풀 Node 환경 사용
}
});
await workerWindow.loadFile('worker.html');
mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, "app.js"),
nodeIntegration: true
}
});
mainWindow.loadFile('app.html');
// MessageChannel 생성하여 두 창에 각각 전달
const { port1, port2 } = new MessageChannelMain();
mainWindow.webContents.postMessage('init-worker-channel', null, [port1]);
workerWindow.webContents.postMessage('init-worker-channel', null, [port2]);
});
<!-- worker.js -->
const { ipcRenderer } = require('electron');
ipcRenderer.on('init-worker-channel', (event) => {
const [port] = event.ports;
port.onmessage = (e) => {
// CPU 집약적인 작업 처리 예시
const result = e.data * 2;
port.postMessage(result);
};
port.start();
});
const { ipcRenderer } = require('electron');
ipcRenderer.on('init-worker-channel', (event) => {
const [port] = event.ports;
port.onmessage = (e) => {
console.log('Worker 결과:', e.data);
};
port.start();
// 워커에 작업 요청 전송 (예: 숫자 21)
port.postMessage(21);
});
메인 창이 워커 창과 직접 통신할 수 있도록 MessageChannel을 설정합니다. 워커는 전달받은 메시지를 처리하여 결과를 다시 메인 창에 전송합니다.
직접 메인 월드와 통신하기
Context Isolation이 활성화된 렌더러에서는 메인 세계(Main World)와 격리되어 있지만, 때로는 메인 세계와 직접 통신이 필요할 수 있습니다. 이때 MessagePort를 활용하면 격리된 환경과 메인 세계 간에 안전하게 메시지를 전달할 수 있습니다.
// main.js
const { app, BrowserWindow, MessageChannelMain } = require('electron');
const path = require('node:path');
app.whenReady().then(() => {
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});
win.loadFile('index.html');
// MessageChannelMain 생성 및 한쪽 포트를 전달
const { port1, port2 } = new MessageChannelMain();
port2.postMessage({ test: 'Hello from main process' });
port2.start();
// 메인 프로세스에서 생성한 포트를 렌더러의 메인 세계로 전달
win.webContents.postMessage('main-world-port', null, [port1]);
});
// preload.js
const { ipcRenderer } = require('electron');
ipcRenderer.on('main-world-port', (event) => {
// 메인 세계에 포트를 전달하기 위해 window.postMessage 사용
window.postMessage('main-world-port', '*', event.ports);
});
<!-- index.html -->
<script>
window.addEventListener('message', (event) => {
if (event.source === window && event.data === 'main-world-port') {
const [port] = event.ports;
port.onmessage = (e) => {
console.log('메인 세계로부터 수신:', e.data);
// 응답 보내기
port.postMessage({ reply: '안녕하세요, 메인 월드!' });
};
port.start();
}
});
</script>
'Electron' 카테고리의 다른 글
[Electron] 프로세스 샌드박싱: 보안을 위한 실행 격리 (0) | 2025.03.24 |
---|---|
[Electron] IPC: 프로세스 간 통신으로 앱 기능 확장하기 (0) | 2025.03.24 |
[Electron] 컨텍스트 격리(Context Isolation): 보안과 TypeScript로 더 안전하게 (0) | 2025.03.24 |
[Electron] 프로세스 모델: 메인과 렌더러의 조화로운 협력 (0) | 2025.03.24 |
[Electron] 튜토리얼 Part 6: 앱 배포 및 자동 업데이트 (0) | 2025.03.24 |