IPC 심화 — 렌더러 간 통신과 MessagePort
"기본 IPC만으로는 복잡한 앱을 만들기 어렵다" — 렌더러 간 통신, 스트리밍 데이터, 대용량 전송에는 심화 패턴이 필요합니다.
렌더러 간 통신 문제
기본 IPC에서는 렌더러끼리 직접 통신할 수 없습니다. 반드시 메인 프로세스를 경유해야 합니다.
렌더러 A ──── IPC ────► 메인 프로세스 ──── IPC ────► 렌더러 B
메인 프로세스 중계 패턴
// main.js — 렌더러 A의 메시지를 렌더러 B로 전달
const windows = new Map();
function createEditorWindow(id) {
const win = new BrowserWindow({
webPreferences: { preload: path.join(__dirname, 'preload.js') },
});
windows.set(id, win);
win.on('closed', () => windows.delete(id));
return win;
}
// 렌더러 A가 렌더러 B에게 메시지 전달 요청
ipcMain.on('relay:message', (event, targetId, channel, data) => {
const targetWin = windows.get(targetId);
if (targetWin && !targetWin.isDestroyed()) {
targetWin.webContents.send(channel, data);
}
});
MessagePort를 활용한 직접 통신
Electron 13+에서는 MessagePort를 사용하여 렌더러 간 직접 통신 채널을 만들 수 있습니다.
// main.js — MessagePort 쌍을 생성하여 두 렌더러에 전달
const { MessageChannelMain } = require('electron');
function connectRenderers(win1, win2) {
// 포트 쌍 생성
const { port1, port2 } = new MessageChannelMain();
// 각 렌더러에 포트 전달
win1.webContents.postMessage('port:connect', { partner: 'window2' }, [port1]);
win2.webContents.postMessage('port:connect', { partner: 'window1' }, [port2]);
}
app.whenReady().then(() => {
const editor = createEditorWindow('editor');
const preview = createEditorWindow('preview');
// 두 창이 모두 준비되면 연결
Promise.all([
new Promise(r => editor.webContents.once('did-finish-load', r)),
new Promise(r => preview.webContents.once('did-finish-load', r)),
]).then(() => {
connectRenderers(editor, preview);
});
});
// preload.js — MessagePort 수신
const { ipcRenderer } = require('electron');
ipcRenderer.on('port:connect', (event) => {
const port = event.ports[0];
// contextBridge로는 MessagePort를 직접 전달할 수 없으므로
// 이벤트 기반으로 래핑합니다
port.onmessage = (msgEvent) => {
window.dispatchEvent(
new CustomEvent('partner-message', { detail: msgEvent.data })
);
};
// 전송 함수를 전역에 노출
window.__sendToPartner = (data) => {
port.postMessage(data);
};
port.start();
});
// renderer.js (에디터 창)
// 텍스트 변경 시 프리뷰 창에 전송
editor.addEventListener('input', () => {
window.__sendToPartner({
type: 'content-update',
content: editor.value,
});
});
// renderer.js (프리뷰 창)
window.addEventListener('partner-message', (e) => {
if (e.detail.type === 'content-update') {
renderPreview(e.detail.content);
}
});
ipcRenderer.invoke 에러 핸들링 심화
// main.js — 에러를 구조화하여 전달
ipcMain.handle('api:request', async (_event, endpoint, options) => {
try {
const response = await fetch(endpoint, options);
if (!response.ok) {
return {
success: false,
error: {
code: 'HTTP_ERROR',
status: response.status,
message: response.statusText,
},
};
}
const data = await response.json();
return { success: true, data };
} catch (error) {
return {
success: false,
error: {
code: 'NETWORK_ERROR',
message: error.message,
},
};
}
});
IPC 미들웨어 패턴
// main.js — IPC 핸들러에 미들웨어 적용
function withLogging(handler) {
return async (event, ...args) => {
const start = Date.now();
const channel = handler.name || 'unknown';
console.log(`[IPC] ${channel} 호출`, args);
try {
const result = await handler(event, ...args);
console.log(`[IPC] ${channel} 완료 (${Date.now() - start}ms)`);
return result;
} catch (error) {
console.error(`[IPC] ${channel} 에러:`, error);
throw error;
}
};
}
function withAuth(handler) {
return async (event, ...args) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (!isAuthenticated(win)) {
throw new Error('인증이 필요합니다');
}
return handler(event, ...args);
};
}
// 미들웨어 적용
ipcMain.handle(
'secure:getData',
withLogging(withAuth(async (_event, key) => {
return await database.get(key);
}))
);
대용량 데이터 전송
IPC로 큰 데이터를 보내면 직렬화/역직렬화 비용이 발생합니다.
// 대용량 데이터는 파일을 통해 전달하는 패턴
ipcMain.handle('data:exportLarge', async (_event, query) => {
const data = await database.query(query);
const tempPath = path.join(app.getPath('temp'), `export-${Date.now()}.json`);
await fs.promises.writeFile(tempPath, JSON.stringify(data));
// 파일 경로만 전달 (직렬화 비용 절감)
return { filePath: tempPath, count: data.length };
});
// 렌더러에서는 경로를 받아 필요한 부분만 읽기
const { filePath } = await window.electronAPI.exportData(query);
이벤트 버스 패턴
// main.js — 중앙 이벤트 버스
class IPCEventBus {
constructor() {
this.subscribers = new Map();
}
subscribe(channel, webContents) {
if (!this.subscribers.has(channel)) {
this.subscribers.set(channel, new Set());
}
this.subscribers.get(channel).add(webContents);
// 윈도우가 닫히면 자동 구독 해제
webContents.on('destroyed', () => {
this.subscribers.get(channel)?.delete(webContents);
});
}
publish(channel, data, excludeSender) {
const subs = this.subscribers.get(channel);
if (!subs) return;
for (const wc of subs) {
if (wc !== excludeSender && !wc.isDestroyed()) {
wc.send(channel, data);
}
}
}
}
const bus = new IPCEventBus();
ipcMain.on('bus:subscribe', (event, channel) => {
bus.subscribe(channel, event.sender);
});
ipcMain.on('bus:publish', (event, channel, data) => {
bus.publish(channel, data, event.sender);
});
면접 포인트 정리
- 기본 IPC에서 렌더러 간 통신은 메인 프로세스 중계 필요
MessageChannelMain으로 렌더러 간 직접 통신 채널 생성 가능- 대용량 데이터는 IPC 대신 파일 경로를 전달하여 직렬화 비용 절감
- IPC 미들웨어 패턴으로 로깅, 인증 등 횡단 관심사 처리
- 이벤트 버스 패턴으로 pub/sub 구현 가능
IPC 심화를 다뤘으면, 다음은 webContents API를 깊이 파봅시다.
댓글 로딩 중...