IPC (Inter-Process Communication) 개념
IPC란?
**IPC (Inter-Process Communication, 프로세스 간 통신)**은 여러 프로세스가 서로 데이터를 주고받는 기술입니다.
Electron에서는 메인 프로세스(Main Process)와 렌더러 프로세스(Renderer Process)가 분리되어 있기 때문에,
두 프로세스가 서로 통신하려면 IPC를 활용해야 합니다.
IPC는 메인 프로세스 ↔ 렌더러 프로세스 간 데이터를 주고받는 방법
Electron에서는 ipcMain(메인)과 ipcRenderer(렌더러) 모듈을 사용하여 IPC 구현
contextBridge를 활용하면 보안을 유지하면서 IPC 사용 가능
즉, Electron에서 IPC는 "메인 프로세스와 렌더러 프로세스를 연결하는 다리" 역할을 합니다!
1. 왜 IPC가 필요할까?
Electron은 보안을 위해 메인 프로세스와 렌더러 프로세스를 분리합니다.
렌더러 프로세스에서 파일 시스템을 접근하려면? → 메인 프로세스에 요청해야 함 (IPC 필요!)
렌더러 프로세스에서 시스템 트레이를 조작하려면? → 메인 프로세스에 요청해야 함 (IPC 필요!)
2. IPC 기본 개념
Electron의 IPC는 메인 프로세스 ↔ 렌더러 프로세스 간 데이터를 주고받는 기술입니다.
주요 개념은 다음과 같습니다.
| ipcMain | 메인 프로세스에서 메시지 수신 (on, handle) |
| ipcRenderer | 렌더러 프로세스에서 메시지 송신 (send, invoke) |
| contextBridge | 보안 강화를 위해 렌더러 프로세스에 IPC API를 안전하게 노출 |
즉, ipcRenderer.send()로 메인 프로세스에 요청하고, ipcMain.on()에서 응답을 받을 수 있습니다.
3. 기본적인 IPC 통신 예제
1️⃣ main.js (메인 프로세스)
const { app, BrowserWindow, ipcMain } = require("electron");
let mainWindow;
app.whenReady().then(() => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: __dirname + "/preload.js",
contextIsolation: true,
nodeIntegration: false
}
});
mainWindow.loadFile("index.html");
});
// 렌더러에서 받은 메시지 출력
ipcMain.on("messageToMain", (event, message) => {
console.log("렌더러에서 받은 메시지:", message);
event.reply("messageFromMain", "메인 프로세스 응답!");
});
렌더러 프로세스에서 보낸 메시지를 받아서 응답을 보냅니다.
2️⃣ preload.js (렌더러 프로세스에 안전한 API 제공)
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
sendMessage: (message) => ipcRenderer.send("messageToMain", message),
receiveMessage: (callback) => ipcRenderer.on("messageFromMain", (event, data) => callback(data))
});
보안을 위해 contextBridge를 사용하여 IPC 기능을 안전하게 제공해야 합니다.
3️⃣ index.js (렌더러 프로세스)
document.getElementById("sendMessage").addEventListener("click", () => {
window.electronAPI.sendMessage("렌더러에서 보냄!");
});
window.electronAPI.receiveMessage((message) => {
document.getElementById("responseMessage").textContent = "응답: " + message;
});
렌더러 프로세스에서 버튼을 클릭하면 메인 프로세스에 메시지를 전송합니다.
4️⃣ index.html (렌더러 UI)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Electron IPC 예제</title>
</head>
<body>
<h1>IPC 통신 예제</h1>
<button id="sendMessage">메시지 보내기</button>
<p id="responseMessage">응답 대기 중...</p>
<script src="index.js"></script>
</body>
</html>
이제 버튼을 클릭하면 메인 프로세스와 통신할 수 있습니다!
4. invoke를 사용한 요청-응답 방식
ipcRenderer.send()는 단방향 통신이므로, 메인 프로세스에서 데이터를 반환하려면 event.reply()를 사용해야 합니다.
하지만 ipcRenderer.invoke()와 ipcMain.handle()을 사용하면 비동기 요청-응답 방식을 구현할 수 있습니다.
1️⃣ main.js (메인 프로세스)
ipcMain.handle("getData", async () => {
return "메인 프로세스에서 보낸 데이터!";
});
2️⃣ preload.js (보안 설정)
contextBridge.exposeInMainWorld("electronAPI", {
getData: () => ipcRenderer.invoke("getData")
});
3️⃣ index.js (렌더러 프로세스)
document.getElementById("getData").addEventListener("click", async () => {
const data = await window.electronAPI.getData();
document.getElementById("responseMessage").textContent = "응답: " + data;
});
이제 버튼을 클릭하면 메인 프로세스에서 데이터를 받아올 수 있습니다!
5. IPC를 활용한 파일 읽기 예제
렌더러 프로세스에서 파일 시스템(fs)에 직접 접근할 수 없지만, IPC를 사용하면 가능합니다.
1️⃣ main.js (파일 읽기 처리)
const fs = require("fs");
ipcMain.handle("readFile", async (event, filePath) => {
return fs.readFileSync(filePath, "utf8");
});
2️⃣ preload.js (렌더러에서 파일 읽기 요청)
contextBridge.exposeInMainWorld("electronAPI", {
readFile: (filePath) => ipcRenderer.invoke("readFile", filePath)
});
3️⃣ index.js (파일 읽기 실행)
document.getElementById("readFile").addEventListener("click", async () => {
const content = await window.electronAPI.readFile("test.txt");
console.log("파일 내용:", content);
});
이제 렌더러 프로세스에서 안전하게 파일을 읽을 수 있습니다!
ipcMain과 ipcRenderer의 차이점
ipcMain vs ipcRenderer
Electron에서 메인 프로세스(Main Process)와 렌더러 프로세스(Renderer Process)는 서로 직접 접근할 수 없기 때문에, IPC (Inter-Process Communication) 통신을 사용해야 합니다.
IPC를 위해 Electron은 두 가지 주요 모듈을 제공합니다:
- ipcMain → 메인 프로세스에서 사용 (수신 및 응답). 렌더러 → 메인
- ipcRenderer → 렌더러 프로세스에서 사용 (메시지 전송 및 수신). 렌더러 ↔ 메인 (양방향 가능)
즉, ipcRenderer는 데이터를 보내고, ipcMain은 데이터를 받아 처리하는 역할을 합니다!
1. 기본적인 ipcMain과 ipcRenderer (단방향)
1️⃣ main.js (메인 프로세스)
const { app, BrowserWindow, ipcMain } = require("electron");
let mainWindow;
app.whenReady().then(() => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: __dirname + "/preload.js",
contextIsolation: true,
nodeIntegration: false
}
});
mainWindow.loadFile("index.html");
});
// 렌더러 프로세스에서 메시지를 받음
ipcMain.on("messageToMain", (event, message) => {
console.log("렌더러에서 받은 메시지:", message);
});
렌더러 프로세스에서 messageToMain 이벤트로 메시지를 보내면 메인 프로세스가 이를 받습니다.
2️⃣ preload.js (보안 설정 + IPC 브릿지)
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
sendMessage: (message) => ipcRenderer.send("messageToMain", message)
});
렌더러 프로세스에서 window.electronAPI.sendMessage("메시지")를 통해 IPC를 사용할 수 있도록 설정합니다.
3️⃣ index.js (렌더러 프로세스)
document.getElementById("sendMessage").addEventListener("click", () => {
window.electronAPI.sendMessage("렌더러에서 보낸 메시지!");
});
버튼 클릭 시 sendMessage()를 통해 메인 프로세스로 메시지를 보냅니다.
4️⃣ index.html (렌더러 UI)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Electron IPC 예제</title>
</head>
<body>
<h1>IPC 통신 예제</h1>
<button id="sendMessage">메시지 보내기</button>
<script src="index.js"></script>
</body>
</html>
이제 버튼을 클릭하면 메인 프로세스로 메시지가 전달됩니다!
2. invoke와 handle을 사용한 요청-응답 방식
일반적으로 ipcRenderer.send()와 ipcMain.on()을 사용하면 단방향 통신이므로, 메인 프로세스에서 응답을 주려면 event.reply()를 사용해야 합니다.
하지만 비동기 응답이 필요할 경우, ipcRenderer.invoke()와 ipcMain.handle()을 사용하면 보다 직관적인 요청-응답 방식을 구현할 수 있습니다.
1️⃣ main.js (메인 프로세스)
ipcMain.handle("getData", async () => {
return "메인 프로세스에서 보낸 데이터!";
});
렌더러 프로세스에서 invoke("getData")를 호출하면 데이터를 반환합니다.
2️⃣ preload.js (보안 설정)
contextBridge.exposeInMainWorld("electronAPI", {
getData: () => ipcRenderer.invoke("getData")
});
렌더러 프로세스에서 window.electronAPI.getData()를 통해 데이터를 요청할 수 있습니다.
3️⃣ index.js (렌더러 프로세스)
document.getElementById("getData").addEventListener("click", async () => {
const data = await window.electronAPI.getData();
console.log("메인 프로세스 응답:", data);
});
이제 버튼을 클릭하면 메인 프로세스에서 데이터를 받아옵니다!
4️⃣ index.html (렌더러 UI)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Electron IPC 요청-응답</title>
</head>
<body>
<h1>IPC 요청-응답 방식</h1>
<button id="getData">데이터 요청</button>
<script src="index.js"></script>
</body>
</html>
버튼을 클릭하면 메인 프로세스에서 데이터를 가져올 수 있습니다.
3. 보안 강화: contextBridge를 반드시 사용해야 하는 이유
Electron에서 보안을 위해 nodeIntegration: false를 설정하면 렌더러 프로세스에서 require("electron")을 사용할 수 없습니다.
따라서 렌더러 프로세스에서 IPC를 사용하려면 반드시 preload.js에서 contextBridge를 통해 안전한 API를 제공해야 합니다.
보안 강화를 위한 webPreferences 설정
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false, // Node.js API 차단 (보안 강화)
contextIsolation: true, // `window` 객체 격리 (보안 강화)
preload: __dirname + "/preload.js", // 안전한 API 제공
}
});
보안을 위해 nodeIntegration: false, contextIsolation: true 설정을 반드시 추가해야 합니다.
ipcMain.handle() vs ipcMain.on() 차이점
ipcMain.handle() vs ipcMain.on() 비교
Electron에서 **렌더러 프로세스(Renderer Process)**는 **메인 프로세스(Main Process)**와 직접 통신할 수 없기 때문에 **IPC (Inter-Process Communication, 프로세스 간 통신)**을 사용해야 합니다.
ipcMain.on() → 단방향 이벤트 기반 통신 (렌더러 → 메인, 이벤트 리스너 방식)
ipcMain.handle() → 요청-응답 기반 비동기 통신 (렌더러 ↔ 메인, await 사용 가능)
즉, ipcMain.on()은 단순히 메시지를 받는 이벤트 리스너 방식이며, ipcMain.handle()은 비동기 함수로 값을 반환하는 요청-응답 방식입니다.
1. ipcMain.on() - 단방향 메시지 전달 (이벤트 리스너)
ipcMain.on()은 렌더러 프로세스에서 보낸 메시지를 메인 프로세스에서 수신하여 처리하는 단방향 이벤트 리스너 방식입니다.
1️⃣ main.js (메인 프로세스)
const { app, BrowserWindow, ipcMain } = require("electron");
let mainWindow;
app.whenReady().then(() => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: __dirname + "/preload.js",
contextIsolation: true,
nodeIntegration: false
}
});
mainWindow.loadFile("index.html");
});
// 렌더러 프로세스에서 메시지를 받는 이벤트 리스너
ipcMain.on("messageToMain", (event, message) => {
console.log("렌더러에서 받은 메시지:", message);
event.reply("messageFromMain", "메인 프로세스에서 응답!");
});
2️⃣ preload.js (렌더러에서 안전한 API 제공)
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
sendMessage: (message) => ipcRenderer.send("messageToMain", message),
receiveMessage: (callback) => ipcRenderer.on("messageFromMain", (event, data) => callback(data))
});
3️⃣ index.js (렌더러 프로세스)
document.getElementById("sendMessage").addEventListener("click", () => {
window.electronAPI.sendMessage("렌더러에서 보낸 메시지!");
});
// 메인 프로세스에서 응답을 받음
window.electronAPI.receiveMessage((message) => {
document.getElementById("responseMessage").textContent = "응답: " + message;
});
4️⃣ index.html (렌더러 UI)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Electron IPC 예제</title>
</head>
<body>
<h1>IPC 단방향 메시지 예제</h1>
<button id="sendMessage">메시지 보내기</button>
<p id="responseMessage">응답 대기 중...</p>
<script src="index.js"></script>
</body>
</html>
2. ipcMain.handle() - 요청-응답 방식 (비동기)
ipcMain.handle()은 렌더러 프로세스에서 데이터를 요청하고, 메인 프로세스에서 해당 요청을 처리한 후 값을 반환하는 방식입니다.
비동기 await를 사용할 수 있어 API 호출과 같은 작업에 적합합니다.
1️⃣ main.js (메인 프로세스)
ipcMain.handle("getData", async () => {
return "메인 프로세스에서 보낸 데이터!";
});
2️⃣ preload.js (보안 설정)
contextBridge.exposeInMainWorld("electronAPI", {
getData: () => ipcRenderer.invoke("getData")
});
3️⃣ index.js (렌더러 프로세스)
document.getElementById("getData").addEventListener("click", async () => {
const data = await window.electronAPI.getData();
document.getElementById("responseMessage").textContent = "응답: " + data;
});
4️⃣ index.html (렌더러 UI)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Electron IPC 요청-응답</title>
</head>
<body>
<h1>IPC 요청-응답 방식</h1>
<button id="getData">데이터 요청</button>
<p id="responseMessage">응답 대기 중...</p>
<script src="index.js"></script>
</body>
</html>
3. ipcMain.on() vs ipcMain.handle() 차이점
| ipcMain.on() | ipcMain.handle() | |
| 렌더러 → 메인 | ipcRenderer.send(event, data) | ipcRenderer.invoke(event, data) |
| 메인 → 렌더러 응답 | ipcRenderer.invoke(event, data) | return 값을 자동 반환 |
| 비동기 지원 | ❌ (콜백 사용) | ✅ async/await 지원 |
| 사용 예시 | UI 이벤트 (버튼 클릭 등) | API 요청, 데이터 조회 |
| 통신 방식 | 단방향 (이벤트 리스너) | 요청-응답 (비동기 방식) |
즉, ipcMain.on()은 단방향 이벤트 기반이며, ipcMain.handle()은 요청-응답 방식으로 await를 사용할 수 있습니다!
4. ipcMain.on()과 ipcMain.handle() 언제 사용할까?
- 버튼 클릭 후 이벤트 전달 ipcMain.on() 단순 이벤트 리스너 방식이 적합
- 로그 기록 (console.log) ipcMain.on() 단순 메시지 전달 방식이면 충분
- 파일 읽기 요청 (fs.readFile) ipcMain.handle() 데이터를 반환해야 하므로 await 필요
- API 요청 (fetch, DB 조회) ipcMain.handle() 데이터 응답이 필요하므로 await 사용
즉, 데이터 요청과 응답이 필요하면 ipcMain.handle(), 단순 메시지 전달이면 ipcMain.on()을 사용하면 됩니다!
ipcRenderer.invoke() vs ipcRenderer.send() 차이점
ipcRenderer.invoke() vs ipcRenderer.send() 비교
Electron에서 **렌더러 프로세스(Renderer Process)**와 **메인 프로세스(Main Process)**는 서로 직접 접근할 수 없기 때문에 **IPC (Inter-Process Communication)**를 사용하여 데이터를 주고받습니다.
Electron의 IPC 방식은 크게 두 가지가 있습니다:
- ipcRenderer.send() → 단방향 이벤트 기반 메시지 전송
- ipcRenderer.invoke() → 요청-응답 기반 비동기 데이터 반환
즉, ipcRenderer.send()는 단순한 이벤트 전달, ipcRenderer.invoke()는 데이터를 반환해야 하는 경우 사용됩니다!
1. ipcRenderer.send() - 단방향 이벤트 기반 메시지 전달
ipcRenderer.send()는 렌더러 프로세스에서 메인 프로세스로 데이터를 보낼 때 사용하는 단방향 방식입니다.
즉, 메인 프로세스에서 응답을 보내려면 event.reply() 를 별도로 호출해야 합니다.
1️⃣ main.js (메인 프로세스)
const { app, BrowserWindow, ipcMain } = require("electron");
let mainWindow;
app.whenReady().then(() => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: __dirname + "/preload.js",
contextIsolation: true,
nodeIntegration: false
}
});
mainWindow.loadFile("index.html");
});
// 렌더러 프로세스에서 메시지를 받음
ipcMain.on("messageToMain", (event, message) => {
console.log("렌더러에서 받은 메시지:", message);
event.reply("messageFromMain", "메인 프로세스에서 응답!");
});
2️⃣ preload.js (보안 설정)
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
sendMessage: (message) => ipcRenderer.send("messageToMain", message),
receiveMessage: (callback) => ipcRenderer.on("messageFromMain", (event, data) => callback(data))
});
3️⃣ index.js (렌더러 프로세스)
document.getElementById("sendMessage").addEventListener("click", () => {
window.electronAPI.sendMessage("렌더러에서 보낸 메시지!");
});
// 메인 프로세스에서 응답을 받음
window.electronAPI.receiveMessage((message) => {
document.getElementById("responseMessage").textContent = "응답: " + message;
});
4️⃣ index.html (렌더러 UI)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Electron IPC 단방향 예제</title>
</head>
<body>
<h1>IPC 단방향 메시지</h1>
<button id="sendMessage">메시지 보내기</button>
<p id="responseMessage">응답 대기 중...</p>
<script src="index.js"></script>
</body>
</html>
이제 버튼을 클릭하면 메인 프로세스로 메시지가 전송되고, 메인 프로세스에서 event.reply()로 응답을 보냅니다!
2. ipcRenderer.invoke() - 요청-응답 방식 (비동기)
ipcRenderer.invoke()는 렌더러 프로세스에서 요청을 보내고, 메인 프로세스에서 데이터를 반환할 수 있는 방식입니다.
비동기 await 를 사용할 수 있어 파일 읽기, API 요청, DB 조회 등 데이터 반환이 필요한 경우에 적합합니다.
1️⃣ main.js (메인 프로세스)
ipcMain.handle("getData", async () => {
return "메인 프로세스에서 보낸 데이터!";
});
2️⃣ preload.js (보안 설정)
contextBridge.exposeInMainWorld("electronAPI", {
getData: () => ipcRenderer.invoke("getData")
});
3️⃣ index.js (렌더러 프로세스)
document.getElementById("getData").addEventListener("click", async () => {
const data = await window.electronAPI.getData();
document.getElementById("responseMessage").textContent = "응답: " + data;
});
4️⃣ index.html (렌더러 UI)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Electron IPC 요청-응답</title>
</head>
<body>
<h1>IPC 요청-응답 방식</h1>
<button id="getData">데이터 요청</button>
<p id="responseMessage">응답 대기 중...</p>
<script src="index.js"></script>
</body>
</html>
이제 버튼을 클릭하면 await을 통해 메인 프로세스에서 데이터를 받아올 수 있습니다!
3. ipcRenderer.send() vs ipcRenderer.invoke() 차이점
| ipcRenderer.send() | ipcRenderer.invoke() | |
| 통신 방식 | 단방향 (이벤트 리스너) | 요청-응답 (비동기 await 지원) |
| 메인 프로세스에서 응답 | event.reply()를 사용해야 함 | return을 사용하여 데이터 반환 가능 |
| 비동기 지원 | ❌ (콜백 방식) | ✅ await을 사용할 수 있음 |
| 사용 예시 | UI 이벤트 처리, 로그 기록 | API 요청, 파일 읽기, DB 조회 |
즉, 데이터 응답이 필요하면 ipcRenderer.invoke(), 단순 메시지 전달이면 ipcRenderer.send()를 사용하면 됩니다!
4. 언제 send()와 invoke()를 사용할까?
- 버튼 클릭 후 이벤트 전달 ipcRenderer.send() 단순 이벤트 전달 방식이 적합
- 로그 기록 (console.log) ipcRenderer.send() 단순 메시지 전달 방식이면 충분
- 파일 읽기 (fs.readFile) ipcRenderer.invoke() 데이터를 반환해야 하므로 await 필요
- API 요청 (fetch, DB 조회) ipcRenderer.invoke() 데이터 응답이 필요하므로 await 사용
즉, 데이터 요청과 응답이 필요하면 invoke(), 단순 메시지 전달이면 send() 사용이 적절합니다!
event.sender vs event.senderFrame 차이점
Electron에서 IPC 이벤트 핸들러를 사용할 때, 메인 프로세스에서 event 객체를 통해 렌더러 프로세스와 통신할 수 있습니다.
이때 event.sender와 event.senderFrame은 모두 렌더러 프로세스를 나타내지만, 사용 목적과 동작 방식이 다릅니다.
event.sender vs event.senderFrame 비교
| event.sender | event.senderFrame | |
| 설명 | 요청을 보낸 렌더러 프로세스를 나타냄 | 요청을 보낸 특정 iframe을 나타냄 |
| 객체 타입 | WebContents 객체 | WebFrameMain 객체 |
| 주요 용도 | 메인 창 전체에 대한 조작 (BrowserWindow) | 특정 iframe 또는 서브프레임 조작 |
| 예제 | event.sender.send("message", data) | event.senderFrame.url 사용 가능 |
| 사용 가능 속성 | loadURL(), send() 등 사용 가능 | url, executeJavaScript() 사용 가능 |
| iframe 대응 | ❌ 여러 iframe이 있어도 구분 불가능 | ✅ 요청한 iframe만 구분 가능 |
즉, event.sender는 창 전체(WebContents), event.senderFrame은 특정 프레임(WebFrameMain)을 나타냅니다.
1. event.sender - 렌더러 프로세스 전체를 나타냄
event.sender는 렌더러 프로세스의 WebContents를 나타내며, 전체 창을 조작할 때 사용됩니다.
렌더러 프로세스에서 ipcRenderer.send()를 호출하면, 메인 프로세스에서 event.sender를 통해 다시 응답을 보낼 수 있습니다.
1️⃣ main.js (메인 프로세스)
const { app, BrowserWindow, ipcMain } = require("electron");
let mainWindow;
app.whenReady().then(() => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: __dirname + "/preload.js",
contextIsolation: true,
nodeIntegration: false
}
});
mainWindow.loadFile("index.html");
});
// `event.sender`를 사용하여 렌더러 프로세스에 응답
ipcMain.on("messageToMain", (event, message) => {
console.log("렌더러에서 받은 메시지:", message);
event.sender.send("messageFromMain", "메인 프로세스에서 응답!");
});
2️⃣ preload.js (보안 설정)
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
sendMessage: (message) => ipcRenderer.send("messageToMain", message),
receiveMessage: (callback) => ipcRenderer.on("messageFromMain", (event, data) => callback(data))
});
3️⃣ index.js (렌더러 프로세스)
document.getElementById("sendMessage").addEventListener("click", () => {
window.electronAPI.sendMessage("렌더러에서 보낸 메시지!");
});
// 메인 프로세스에서 응답을 받음
window.electronAPI.receiveMessage((message) => {
document.getElementById("responseMessage").textContent = "응답: " + message;
});
이제 버튼을 클릭하면 event.sender.send()를 통해 메인 프로세스가 응답을 보낼 수 있습니다!
2. event.senderFrame - 특정 iframe을 나타냄
Electron에서는 렌더러 프로세스 내부에 iframe이 있을 경우, event.sender는 창 전체를 나타내므로 특정 iframe을 구분할 수 없습니다.
이때, event.senderFrame을 사용하면 특정 iframe에서 요청을 보냈는지 확인할 수 있습니다.
1️⃣ index.html (렌더러 UI - iframe 포함)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Electron IPC 예제</title>
</head>
<body>
<h1>메인 페이지</h1>
<iframe id="myFrame" src="iframe.html"></iframe>
<script>
const iframe = document.getElementById("myFrame");
iframe.onload = () => {
iframe.contentWindow.postMessage("Hello from main page!", "*");
};
</script>
</body>
</html>
2️⃣ iframe.html (iframe 내부)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Electron iframe</title>
</head>
<body>
<h1>iframe 페이지</h1>
<script>
window.addEventListener("message", (event) => {
console.log("부모 페이지에서 받은 메시지:", event.data);
});
</script>
</body>
</html>
3️⃣ main.js (메인 프로세스)
ipcMain.on("messageToMain", (event, message) => {
console.log("렌더러에서 받은 메시지:", message);
// iframe에서 요청을 보냈는지 확인
if (event.senderFrame.url.includes("iframe.html")) {
console.log("iframe에서 메시지를 보냈습니다.");
} else {
console.log("메인 페이지에서 메시지를 보냈습니다.");
}
});
이제 event.senderFrame.url을 사용하여 메시지를 보낸 곳이 iframe인지 확인할 수 있습니다!
3. 언제 event.sender와 event.senderFrame을 사용할까?
- 메인 창 전체에 응답을 보낼 때 - event.sender.send() - BrowserWindow 전체 조작 가능
- iframe에서 요청한 경우만 처리할 때 - event.senderFrame.url 확인 - 특정 iframe에서 보낸 요청을 구분 가능
- iframe에서 실행되는 코드에만 접근할 때 - event.senderFrame.executeJavaScript() - 특정 iframe 내에서 코드 실행 가능
즉, 창 전체를 제어할 때는 event.sender, 특정 iframe을 조작할 때는 event.senderFrame을 사용하면 됩니다!
보안을 고려한 안전한 IPC 사용법 (Electron)
왜 안전한 IPC 사용이 중요한가?
Electron은 메인 프로세스(Main Process)와 렌더러 프로세스(Renderer Process)를 분리하여 실행합니다.
하지만 렌더러 프로세스는 웹 기술(HTML, CSS, JavaScript) 기반으로 실행되기 때문에 XSS(크로스 사이트 스크립팅) 공격에 취약할 수 있습니다.
또한, 렌더러 프로세스에서 직접 Node.js API를 실행할 수 있다면, 시스템 파일을 조작하는 등의 보안 위험이 발생할 수 있습니다.
렌더러 프로세스가 직접 Node.js API를 실행하지 못하도록 차단해야 함
IPC(Inter-Process Communication) 통신을 안전하게 설정해야 함
렌더러에서 임의의 API를 호출하지 못하도록 제한해야 함
메인 프로세스와의 데이터 교환을 검증해야 함
1. 보안 강화를 위한 webPreferences 설정
Electron에서 렌더러 프로세스의 보안을 강화하려면, webPreferences 설정을 반드시 조정해야 합니다.
1️⃣ nodeIntegration: false 설정 (Node.js API 차단)
렌더러 프로세스에서 require("fs") 같은 Node.js API를 직접 실행하지 못하도록 차단해야 합니다.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false, // 렌더러 프로세스에서 Node.js API 실행 차단
contextIsolation: true, // `window` 객체 보호 (보안 강화)
enableRemoteModule: false, // remote 모듈 비활성화
sandbox: true, // 렌더러 프로세스를 샌드박스로 실행 (보안 강화)
preload: __dirname + "/preload.js", // 안전한 API 제공을 위한 Preload 설정
}
});
이제 렌더러 프로세스에서 require("fs"), require("os") 등을 사용할 수 없습니다.
2. preload.js를 통한 안전한 API 제공
렌더러 프로세스가 직접 Node.js API에 접근하는 것을 차단하고, 필요한 기능만 preload.js를 통해 제공해야 합니다.
1️⃣ 안전한 API 제공 (contextBridge)
preload.js에서 contextBridge를 사용하여 메인 프로세스와의 통신을 제한적으로 허용합니다.
const { contextBridge, ipcRenderer } = require("electron");
// 안전한 API만 제공
contextBridge.exposeInMainWorld("electronAPI", {
sendMessage: (message) => ipcRenderer.send("messageToMain", message),
receiveMessage: (callback) => ipcRenderer.on("messageFromMain", (event, data) => callback(data)),
getData: (key) => ipcRenderer.invoke("getData", key), // 요청-응답 방식으로 데이터 가져오기
});
이제 렌더러 프로세스에서 window.electronAPI.sendMessage() 같은 제한된 API만 호출할 수 있습니다.
3. 메인 프로세스에서 데이터 검증 (ipcMain.handle)
렌더러 프로세스에서 메인 프로세스로 데이터를 보낼 때, 메인 프로세스에서 반드시 데이터를 검증해야 합니다.
악성 데이터가 전달될 경우, 메인 프로세스에서 직접 실행되지 않도록 해야 합니다.
1️⃣ ipcMain.handle()에서 데이터 검증
const { ipcMain } = require("electron");
// 📌 안전한 데이터 검증
ipcMain.handle("getData", async (event, key) => {
if (typeof key !== "string") {
throw new Error("Invalid key type"); // ❌ 잘못된 데이터 차단
}
// 데이터 반환
return `요청한 데이터: ${key}`;
});
이제 숫자나 객체가 key로 전달되면 요청이 차단됩니다!
4. IPC 통신에서 올바른 응답 처리
렌더러 프로세스가 메인 프로세스에서 데이터를 요청할 때, 무작위로 다른 IPC 이벤트를 호출하지 못하도록 제한해야 합니다.
1️⃣ preload.js에서 허용된 API만 제공
contextBridge.exposeInMainWorld("electronAPI", {
getSafeData: (key) => ipcRenderer.invoke("getSafeData", key),
});
2️⃣ main.js에서 데이터 요청을 검증
ipcMain.handle("getSafeData", async (event, key) => {
const allowedKeys = ["username", "email", "userID"];
if (!allowedKeys.includes(key)) {
throw new Error("Unauthorized key access!"); // ❌ 허용되지 않은 요청 차단
}
return `요청한 데이터: ${key}`;
});
이제 window.electronAPI.getSafeData("password") 같은 민감한 데이터 요청이 차단됩니다!
5. XSS 공격 방어 (DOMPurify 사용)
렌더러 프로세스에서 XSS(크로스 사이트 스크립팅) 공격을 방지하려면,
DOMPurify 같은 라이브러리를 활용하여 사용자 입력을 검증해야 합니다.
1️⃣ DOMPurify를 사용한 안전한 HTML 렌더링
import DOMPurify from "dompurify";
document.getElementById("message").innerHTML = DOMPurify.sanitize(userInput);
이제 <script>alert("해킹!")</script> 같은 악성 코드가 실행되지 않습니다!
6. 보안 강화를 위한 추가 설정
Electron의 기본 보안을 강화하려면, securityWarnings: true 설정을 추가해야 합니다.
1️⃣ 보안 설정 추가
app.commandLine.appendSwitch("disable-site-isolation-trials");
app.commandLine.appendSwitch("enable-features", "ElectronSecureDefaults");
이제 기본적으로 더 강력한 보안 설정이 적용됩니다.
7. remote 모듈 사용 금지
Electron의 remote 모듈은 보안 취약점이 많아, 최신 버전에서는 기본적으로 비활성화되었습니다.
1️⃣ remote 모듈을 완전히 차단
const mainWindow = new BrowserWindow({
webPreferences: {
enableRemoteModule: false, // ❌ remote 모듈 비활성화
}
});
이제 remote 모듈을 사용한 공격이 차단!
'JavaScript > Electron' 카테고리의 다른 글
| Electron의 보안 문제 (0) | 2025.03.18 |
|---|---|
| Electron의 주요 API (0) | 2025.03.18 |
| Electron 핵심 프로세스 - 렌더러 프로세스 (0) | 2025.03.17 |
| Electron 핵심 프로세스 - 메인 프로세스 (0) | 2025.03.17 |
| Electron의 기본 개념 (1) | 2025.03.17 |