
Electron에서 preload.ts는 main 프로세스와 renderer 프로세스 사이를 중개함. preload 스크립트는 브라우저 환경에서 실행되지만, Node.js API 접근 권한을 가지며 보안 경계(Sandboxing)를 제공하여 이를 통해, window 객체에 안전하게 커스텀 API를 노출이 가능함.
Electron의 ipcRenderer와 ipcMain을 사용하면 서로 다른 프로세스 간 메시지를 주고받을 수 있으나, 이 구조는 제대로 관리하지 않으면 복잡해질 가능성이 큼.
기존 방식의 문제점
비직관적인 코드 구조: IPC 메시지의 이벤트 이름과 로직이 분리되어 파악하기 어려움.
스케일링 한계: 메시지가 많아질수록 관리가 힘들어짐.
보안 취약점: renderer 프로세스에서 직접 ipcRenderer를 사용하는 경우 위험성이 증가.
preload.ts를 사용해 IPC 메시지를 모듈화하면 코드의 유지보수성과 보안성을 향상
contextBridge로 안전한 API 노출관련 작성한 샘플
src하위에 common 디렉토리를 추가하여 ipcChannels.ts 생성
// ipcChannels.ts
export const IPC_CHANNELS = {
GO_MAIN: 'go-main',
RESPONSE_DATA: 'response-data',
};
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
import { IPC_CHANNELS } from './ipcChannels';
const electronHandler = {
ipcRenderer: {
sendMessage: (channel: string, data: [] | {}) => {
if (Object.values(IPC_CHANNELS).includes(channel)) {
ipcRenderer.send(channel, data);
} else {
console.warn(`Invalid channel: ${channel}`);
}
},
on: (
channel: string,
callback: (event: Electron.IpcRendererEvent, args: [] | {}) => void,
) => {
if (Object.values(IPC_CHANNELS).includes(channel)) {
ipcRenderer.on(channel, callback);
} else {
console.warn(`Invalid channel: ${channel}`);
}
},
once: (
channel: string,
callback: (event: Electron.IpcRendererEvent, args: [] | {}) => void,
) => {
if (Object.values(IPC_CHANNELS).includes(channel)) {
ipcRenderer.once(channel, callback);
} else {
console.warn(`Invalid channel: ${channel}`);
}
},
},
};
//renderer -> App.tsx
const goMain = () => {
/*ipc 메세지 전송 시 보낼 데이터가 없을 경우에도 null or undefined을 사용하지 않는 이유
IPC 메시지 데이터로 null or undefined 전송 시 오류가 발생함.
Electron의 ipcRenderer.send 메서드가 기본적으로 JSON.stringify를 사용하여 데이터를 직렬화함.
null은 직렬화가 가능한 값이지만, 오류를 발생시킬 가능성이 생김.
그러므로 데이터가 없을 경우에도 빈 배열 []을 보내거나, 대신 명시적으로 빈 객체 {}를 사용하는 것을 추천.
*/
window.electron.ipcRenderer.sendMessage(IPC_CHANNELS.GO_MAIN, []);
};
///...생략
메세지 전송 시 null를 사용하게되면 아래와 같이 에러가 발생함

//main -> main.ts
ipcMain.on(IPC_CHANNELS.GO_MAIN, async (event, arg) => {
//...생략
console.log(IPC_CHANNELS.GO_MAIN, arg);
});

//main -> main.ts
ipcMain.on(IPC_CHANNELS.GET_DATA, async (event, arg) => {
if (arg) {
console.log(IPC_CHANNELS.GET_DATA, arg);
event.sender.send(IPC_CHANNELS.RESPONSE_DATA, getUserData(arg.id));
return;
}
console.log('No Data');
});
function getUserData(userId: number) {
// 예제 데이터
const mockUsers = [
{ id: 123, name: 'MINKI', email: 'minki@example.com' },
{ id: 456, name: 'BOB', email: 'bob@example.com' },
];
return (
mockUsers.find((user) => user.id === userId) || { error: 'User not found' }
);
}
//renderer -> Main.tsx
function SampleMain() {
const [userInfo, setUserInfo] = useState<{ name: string; email: string } | undefined>();
window.electron.ipcRenderer.on(
IPC_CHANNELS.RESPONSE_DATA,
(event, data: any) => {
console.log('User data received:', data); // 성공적으로 사용자 데이터 수신
setUserInfo(data);
},
);
const onSendData = () => {
console.log('onSendData');
window.electron.ipcRenderer.sendMessage(IPC_CHANNELS.GET_DATA, {
id: 123,
});
};
//...생략
}
위 샘플을 통해 user 정보를 가져옴

해당 소스 패턴은 데이터 관리와 화면 단 사용을 분리하여 Electron에서는 Main 프로세스가 애플리케이션의 핵심 데이터를 관리하고, Renderer 프로세스는 화면을 그리는 역할을 담당하는 것으로 가장 보편적으로 Electron IPC 통신의 패턴으로 사용됨
이런 아키텍처에서는 IPC (Inter-Process Communication)를 통해 Renderer 프로세스가 데이터를 요청하고, Main 프로세스가 해당 데이터를 반환하며 이 방식은 보안, 성능, 유지보수성 측면에서 유리함.
보안성: 중요한 데이터와 로직은 Main 프로세스에서만 관리되므로, 렌더러 프로세스에서 데이터 노출을 최소화.
성능: 렌더러 프로세스는 화면을 그리는 데 집중하고, 데이터 처리와 관리에 대한 부담을 Main 프로세스에 분리함으로써 성능 최적화.
유지보수성: 데이터 관리와 화면 표시를 분리하여 코드의 가독성을 높이고, 각 프로세스에 맞는 역할을 명확히 하여 유지보수가 용이함.