Electron에서 파일을 입출력하려면 반드시 NodeJS를 통해야 한다.
사용자의 입력은 Renderer Process가 담당하기 때문에 NodeJS를 기반으로 동작하는 Main Porcess와 통신이 필수적인데, 이 때 IPC 통신을 사용한다.
통신에 주로 사용되는 API는 ipcMain과 ipcRenderer이다.
ipcRenderer는 Main Process에 무언가 요청하거나 데이터를 전달할 때 사용한다.
ipcMain은 Renderer Process에서 전송한 데이터를 수신하고 응답하는데 사용한다.
파일 입출력이 필요한 Logger를 만들어 보겠다.
import { getLog, setLog } from './ipcModules';
// 이전에 작성한 코드 생략
app
.whenReady()
.then(createWindow)
.then((window: Electron.BrowserWindow) => {
// 메뉴 만들기
Menu.setApplicationMenu(
Menu.buildFromTemplate([
...(isMac ? [{ label: 'TimeTimer' }] : []),
{
label: 'File',
submenu: [
{
label: 'logger',
click: () => {
window.webContents.send(IPC.OPEN_LOGGER, 'open'); // 메뉴 클릭 했을때 Renderer Process에 이벤트 전달
},
},
],
},
]),
);
});
ipcMain.handle('get-timer-log', async () => {
return await getLog();
});
ipcMain.handle(
'set-timer-log',
(_: Electron.IpcMainInvokeEvent, type: 'ON/OFF' | 'USAGE', log: string) => {
setLog(type, log);
},
);
Main Process에서는 Renderer Process의 요청을 수신하는 방법은
ipcMain.on
, ipcMain.handle
두 가지가 있다.
이 둘의 가장 큰 차이는 Renderer에 데이터를 전달하는 방식이다.
ipcMain.on
의 경우
event.reply('asynchronous-reply', 'pong')
(비동기)
event.returnValue = 'pong'
(동기)
방식으로 전달할 수 있다.
ipcMain.handle
의 경우
return 'pong'
으로 값을 전달하는데, Promise도 전달이 가능하다.
따라서 Renderer Process에서 await으로 동기화 처리를 할 수 있다.
여기에선 Promise 처리가 가능한 ipcMain.handle
을 사용하겠다.
ipcMain.handle
의 첫 번째 매개변수로 이벤트 채널명을, 두 번째 매개변수에는 이벤트 수신시 호출할 함수를 전달한다.
호출할 함수가 리턴하는 값은 Renderer Process에 전달된다.
일반적으로 웹 프로그래밍에선 서버가 먼저 클라이언트에 데이터를 전송하진 않는다.
Electron도 그렇지만, Main Process에서 Renderer Process로 이벤트를 발생시킬 수 있다.
createWindow를 통해 생성한 window 객체를 통해 window.webContents.send
를 호출하는 것이다.
이 방법을 이용해 사용자가 메뉴의 logger를 클릭했을 때 Renderer에 이벤트를 발생시키도록 만들었다.
async function getLog() {
try {
const buffer = fs.readFileSync(path.join(app.getPath('logs'), 'log.json'));
const stringBuffer = buffer.toString();
const logData = JSON.parse(stringBuffer);
return logData.logList;
} catch (e) {
if (e.message.includes('no such file')) {
const newLogDatas = { logList: [] };
fs.writeFileSync(
path.join(app.getPath('logs'), 'log.json'),
JSON.stringify(newLogDatas),
);
return newLogDatas.logList;
}
throw new Error(e);
}
}
async function setLog(type: 'ON/OFF' | 'USAGE', log: string) {
try {
const logList = await getLog();
const newLog = {
type,
log,
time: new Date().toLocaleString('ko-KR', {
timeZone: 'Asia/Seoul',
}),
};
const newLogDatas = { logList: [...logList, newLog] };
fs.writeFileSync(
path.join(app.getPath('logs'), 'log.json'),
JSON.stringify(newLogDatas),
);
} catch (e) {
throw new Error(e);
}
}
export { getLog, setLog };
log를 저장할 파일 입출력 함수들을 모아뒀다.
여기서 중요한 점은 로그 파일의 위치이다.
Electron은 Desktop app이기 때문에 클라이언트 PC에 데이터가 저장된다.
따라서 항상 일정한 경로가 필요한데, 여기에선 app.getPath('logs')
를 이용하여
Electron App이 기본적으로 사용하는 로그 폴더 경로를 읽어왔다.
만약 '../log.json'
과 같은 상대 경로를 사용한다면 npm start
에서는 동작하지만
배포시에 파일을 찾을 수 없게 되니 주의해야 한다.
async function setLog(type: 'ON/OFF' | 'USAGE', log: string) {
try {
await ipcRenderer.invoke(IPC.SET_LOG, type, log);
} catch (e) {
throw new Error(e);
}
}
async function getLog() {
try {
return await ipcRenderer.invoke(IPC.GET_LOG);
} catch (e) {
throw new Error(e);
}
}
이제 Renderer Process의 코드를 보자.
Renderer Process에서 특정 요청을 보낼 때는 ipcRenderer.on
과 ipcRenderer.invoke
를 사용할 수 있다.
ipcRenderer.on
-> ipcMain.on
ipcRenderer.invoke
-> ipcMain.handle
각각 위와 같이 Main Process 에서 처리하는 함수가 다르다.
main.ts
에서 ipcMain.handle
를 사용했기 때문에 ipcRenderer.invoke
를 사용하여 작성했다.
Promise 처리가 가능하여 asyn, await를 사용하여 동기화 처리를 했다.