일렉트론에서 데이터 통신

예은·2022년 2월 25일
1

일렉트론과 리액트 사이의 데이터 통신

일렉트론에서 active-win으로 얻은 정보를 클라이언트로 보내야 하므로 둘 사이의 데이터 통신 방법에 대해 알아보니 IPC 통신으로 가능했다.

프로세스간 통신을 IPC라고 하며 일렉트론에서 IPC를 위한 모듈은 ipcMainipcRenderer가 있다.


우선 일렉트론에서의 프로세스 개념을 간단히 정리하면

메인 프로세스렌더러 프로세스로 구분한다.
메인 프로세스는 앱의 라이프사이클을 관리하는 프로세스, 렌더러 프로세스는 브라우저 환경을 담당하는 프로세스이다. 즉 웹 페이지를 구성하는 프로세스다.

그래서 서버를 메인, 클라이언트를 렌더러로 보기로 한다.

ipc 통신을 하기 위한 두 가지 모듈(ipcMain, ipcRenderer)

모듈이 두가지인 이유는 메인 프로세스에서 사용할 모듈과 렌더러 프로세스에서 사용할 모듈이 각각이기 때문이다.
렌더러 프로세스에서 메인 프로세스로 데이터를 전송하는 모듈이 ipcRenderer이고 메인 프로세스에서 렌더러 프로세스의 데이터를 수신하는 모듈이 ipcMain이다.

REST API로 서버에 요청할 때에도 특정 url로 보낸다. 여기서는 채널명으로 어디서 어디로 요청을 보내는지를 알 수 있다. 클라이언트에서는 원하는 요청의 채널명을 전송하고 서버에서는 그걸 받아서 동작하는 것이다.

ipcRenderer

메인 프로세스로 메세지 또는 데이터를 보낸다. 웹 페이지에서 HTTP 통신으로 받은 데이터를 보내거나 혹은 서버에 요청해야 하는 경우에 사용한다.
요청을 보내고 응답을 받기 위한 sendon이다.

ipcRenderer.send(channel, ...args)

  • channel: 메인 프로세스로 보낼 채널명
  • ...args : 메인 프로세스로 함께 보내고자 하는 데이터
const { ipcRenderer } = require("electron");

ipcRenderer.send("MAIN_CHANNER_NAME", "SEND_MESSAGE");

메인 프로세스에 MAIN_CHANNER_NAME라는 채널로 요청을 보내겠다는 의미다.

ipcRenderer.on(channel, listener)

  • channel : 메인 프로세스에서 응답으로 보낸 채널명
  • listener : 동작시킬 이벤트
const { ipcRenderer } = require("electron");

ipcRenderer.on("REPLY_FROM_MAIN_CHANNER_NAME", (event, args) => {
  console.log(args);
});

메인 프로세스로 요청을 보내고 난 뒤 응답을 받을 경우에 사용한다. 서버에서 돌아온 응답이 REPLY_FROM_MAIN_CHANNER_NAME일 때 동작할 리스너를 추가하면 된다. 서버에서 받은 데이터(여기서는 payload)를 확인해서 화면에 보여주거나 이 외 연산을 처리할 수 있다.


ipcMain

렌더러 프로세스에서 송신한 메세지를 수신하여 받은 채널명에 맞는 이벤트가 동작한다. 그리고 렌더러 프로세스로 다시 응답을 보낼 수 있다. 수신할 때는 on 메서드를 사용한다. 응답을 보낼 때에는 리스터 이벤트에 reply메서드에 송신할 채널 이름과 함께 파라미터를 보낸다.

ipcMain.on(channel, listener)

  • channel : 렌더러로부터 요청받은 채널명
  • listener : 요청받으면 실행할 이벤트
const { ipcMain } = require("electron");

ipcMain.on("MAIN_CHANNER_NAME", (event, args) => {
  const replyArgs = `${args} + ' reply'`;
  event.reply("REPLY_CHANNER_NAME", replyArgs);
});

렌더러에서 MAIN_CHANNER_NAME라는 요청을 전송한 경우 두번째 파라미터인 이벤트가 실행된다. 렌더러에서 함께 보낸 데이터(args)도 확인할 수 있다. 요청 처리 후 렌더러로 응답을 보낼 경우에는 event.reply로 채널명과 함께 보낼 데이터를 송신하면 된다.

프로젝트에 적용하기

프로젝트의 서버는 일렉트론이고 클라이언트는 리액트를 사용하였다.

특정 컴포넌트에서 서버로 active-win 정보를 요청하고 해당 요청이 성공하면 콘솔에 출력하기로 했다.

  1. 리액트 컴포넌트에서 ipcRenderer로 일렉트론 서버로 요청을 보낸다.
  2. 서버에서 해당 채널에 맞는 이벤트를 실행한다.
  3. 이벤트 실행 후 해당 이벤트에 대한 응답을 리액트로 보낸다.
  4. 리액트는 응답을 받아 서버가 반환해준 데이터를 브라우저 콘솔에 출력한다.

1. 리액트 컴포넌트에서 ipcRenderer로 일렉트론 서버로 요청을 보낸다.

app.whenReady().then(() => {
  let win = new BrowserWindow({
    show: false,
    webPreferences: {
      enableRemoteModule: true,
      preload: `${__dirname}/preload.js`,
      nodeIntegration: true,
      contextIsolation: false,
    },
  });
});

리액트에서 ipc통신을 하기 위해서는 일렉트론의 browserWindow를 생성하는 시점에서 nodeIntegration 속성을 true로 지정해줘야 한다. 그래야 클라이언트에서 서버사이드 라이브러리를 이해할 수 있다.
contextIsolation 속성은 false로 지정한다. 이것은 웹 사이트가 일렉트론 내부 또는 preload 스크립트에 접근할 수 있도록 해준다. 기본값은 true로 접근할 수 없도록 하지만 ipcRenderer를 리액트에서 사용하기 위해서 false로 지정해준다.

하지만 보안에 취약하기 때문에 권장하지는 않는다고 한다. contextBridge를 사용해서 ipcRenderer를 넘기는 방법으로 바꿔야 한다. (더 알아보기!!! 🔥)

이전에는 active-win 호출 자체를 setInterval로 받아왔지만 이번에는 클라이언트단에서 interval로 주기적으로 받아오도록 했다.
componentDidMount에서 받아오도록 하고 state값으로 interval를 관리하였다. 그러고 ipcRenderer.send로 서버를 요청을 보냈다.(*1) 이 때 ACTIVE_WINDOW 라는 채널로 송신한다. 두번째 파라미터로 서버에 전달할 객체를 넣으면 되지만 이 경우에는 보낼 것이 없으므로 빈 텍스트를 보냈다.

//Toolbar.js
componentDidMount = () => {
  console.log("ipcRender start");
  this.setState({ interval2: setInterval(this.electronActiveWin, 1000) });
};

electronActiveWin = () => {
  const electron = window.require("electron");
  electron.ipcRenderer.send("ACTIVE_WINDOW", ""); // *1
  electron.ipcRenderer.on("REPLY_ACTIVE_WINDOW", (event, payload) => {
    console.log("payload : ", payload);
  }); //*4
};

2. 일렉트론에서 active-win 정보를 구한다.

//electron.js(main entry)
app.whenReady().then(() => {
  let win = new BrowserWindow({
    //...
  });
  ipcMain.on("ACTIVE_WINDOW", async (event, payload) => {
    const activeWindow = require("active-win");
    const active = await activeWindow({ screenRecordingPermission: true }); // *2
    console.log("ipcMain on : ", active);
    event.reply("REPLY_ACTIVE_WINDOW", active); // *3
  });
});

BrowserWindow가 생성되는 시점인 whenReady에서 있다 ACTIVE_WINDOW라는 채널로 요청이 온 경우를 처리하였다. 요청이 들어오면 active-win를 호출해서 활성화중인 윈도우의 정보를 받아낸다.(*2)

3. 구한 값을 리액트로 전달한다.

event.reply로 클라이언트로 값을 전달한다.(*3) 이 때 채널명은 클라이언트가 요청을 위해서 호출했던 채널명(ACTIVE_WINDOW)가 아니라 응답을 받았을 때 처리하기로 한 채널명(REPLY_ACTIVE_WINDOW)을 보내야한다.

4. 일렉트론이 반환해준 데이터를 브라우저 콘솔에 출력한다.

메인에서 REPLY_ACTIVE_WINDOW라는 채널명으로 회신을 하였기 때문에 *4에서 데이터를 받아올 수 있다. 받아온 payload를 브라우저 콘솔에 출력하였다.

setInterval로 반복했기 때문에 2초마다 콘솔에 찍히는 것을 확인할 수 있다.

고쳐야 할 것

2초마다 한 번씩 active-win을 호출했는데 왜 여러 줄이 찍히는지 모르겠다..😰 렌더러에서 메인으로 setInterval로 요청해서 그런거 같은데 더 찾아봐야겠다.

(수정)
서버의 응답을 처리하는 REPLY_ACTIVE_WINDOW의 이벤트 리스너가 쌓이고 있었다. active-win의 정보를 2초마다 받아오기 위해서 setInterval로 요청을 보내고 있는데 응답 이벤트까지 그 안에서 선언해버려서 요청을 보낼때마다 똑같은 리스너가 계속 쌓였던 것이다. 그래서 반복될수록 로그가 한줄씩 증가했던 것이다.

electronActiveWin = () => {
  const electron = window.require("electron");
  electron.ipcRenderer.send("ACTIVE_WINDOW", "");
  /// 🤦‍♀️ 2초마다 실행되는 메서드에 이벤트 리스너를 등록하였다.
####   electron.ipcRenderer.on("REPLY_ACTIVE_WINDOW", (event, payload) => {
    console.log("payload : ", payload);
  });
};

해결방법은 두 가지이다.

1. ipcRenderer에 once 메서드를 사용한다.

ipcRenderer.once(channel, listener)

  • channel : 메인 프로세스에서 응답으로 보낸 채널명
  • listener : 동작시킬 이벤트

응답에 대한 이벤트 리스너를 동작하는 것은 on과 동일하지만 다른 점은 리스너가 호출되면 삭제된다. 즉 한 번만 사용되고 없애야 할 리스너를 등록할 때 사용하면 좋은 메서드다. 때문에 setInterval로 반복해도 등록 -> 호출 -> 삭제가 반복되므로 리스너가 쌓일 일은 없다.

2. on으로 리스너를 추가하되 setInterval 밖에서 선언하는 것이다.

interval마다 생성과 삭제를 반복하지 않고 interval 외부에서 한 번만 선언 후 사용하기로 했다. 응답에 대한 이벤트 리스너는 화면이 그려질 때 등록되고 서버로의 요청은 Toolbar 컴포넌트가 렌더링되면 interval로 전송된다.

const electron = window.require('electron');
electron.ipcRenderer.on('REPLY_ACTIVE_WINDOW', (event, payload) => {
  console.log('payload : ', payload);
});

class Toolbar extends React.Component{
  componentDidMount = () => {
    setInterval(this.electronActiveWin, 2000);
  };

  electronActiveWin = () => {
    electron.ipcRenderer.send('ACTIVE_WINDOW', '');
  };
}

한 번에 한 줄씩만 가져오는 방법을 찾아보고 콘솔이 아닌 다른 컴포넌트에 전달해서 화면에 목록을 출력해야겠다.

References

profile
Don't only learn by watching. Learn by building. Learn by doing.

0개의 댓글