Electron - IPC 통신

Seongkyun Yu·2021년 3월 5일
2

TIL-Electron

목록 보기
3/4
post-thumbnail

Electron에서 파일을 입출력하려면 반드시 NodeJS를 통해야 한다.

사용자의 입력은 Renderer Process가 담당하기 때문에 NodeJS를 기반으로 동작하는 Main Porcess와 통신이 필수적인데, 이 때 IPC 통신을 사용한다.

ipcMain, ipcRenderer

통신에 주로 사용되는 API는 ipcMain과 ipcRenderer이다.

ipcRenderer는 Main Process에 무언가 요청하거나 데이터를 전달할 때 사용한다.

ipcMain은 Renderer Process에서 전송한 데이터를 수신하고 응답하는데 사용한다.

Logger 구현하기

파일 입출력이 필요한 Logger를 만들어 보겠다.

main.ts

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에 이벤트를 발생시키도록 만들었다.


ipcModules.ts

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에서는 동작하지만

배포시에 파일을 찾을 수 없게 되니 주의해야 한다.

index.ts

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.onipcRenderer.invoke를 사용할 수 있다.

ipcRenderer.on -> ipcMain.on
ipcRenderer.invoke -> ipcMain.handle

각각 위와 같이 Main Process 에서 처리하는 함수가 다르다.

main.ts에서 ipcMain.handle를 사용했기 때문에 ipcRenderer.invoke를 사용하여 작성했다.

Promise 처리가 가능하여 asyn, await를 사용하여 동기화 처리를 했다.

profile
FrontEnd Developer

0개의 댓글