Last Updated: 2023-12-19

cover

Electron의 IPC 개념은 Electron에서 자주 사용되는 기술이다.

ipcMainipcRenderer를 사용하여 Main 프로세스Renderer 프로세스 간에 통신을 할 수 있기 때문이다.

이런 개념을 빗대어 보자면 웹 기술에서 Back-End와 Front-End 간 통신을 하기 위해서 그에 맞는 특정 모듈과 기술이 필요하다. Electron에서도 이러한 개념을 담당하는 것이 IPC 모듈이다. 다른 예로 Vue를 다뤄본 사람이라면 Event Bus 개념이나 Props 개념을 알고 있을 텐데 이때 @event('eventName')으로 이벤트를 수신하고 $emit('eventName')으로 이벤트를 송신하는 것을 볼 수 있는데 이와 비슷한 개념으로 볼 수 있다.

Electron 공식 문서의 용어 정리 부분을 보면 IPC에 대하여 이렇게 기록하고 있다.


IPC는 프로세스 간 통신을 의미한다.

Electron은 IPC를 사용하여 Main 프로세스와 Renderer 프로세스 간에 직력화 된 JSON 메시지는 동기적 또는 비동기적으로 통신한다.

- - -

통신이란 의미는 글로만 봐서는 파악하기 힘들다.

이번 포스트에서는 이 IPC 모듈에 관한 내용을 알아보고 실제로 간단한 앱을 만들면서 파악해보도록 하자.


IPC 모듈

IPC는 위에서 설명했듯이 프로세스 간 통신을 의미한다. 여기서 프로세스란 Main 프로세스Renderer 프로세스를 의미한다.

예를 들어 Main 프로세스는 서버로 생각하고 Renderer 프로세스는 브라우저라고 생각한다면 서버와 브라우저 간 통신을 하기 위한 규약이 필요하다. 이를 Electron에서는 IPC가 처리하고 있다.

IPC는 on을 통해 메시지 또는 이벤트를 수신하고 send를 통해 메시지 또는 이벤트를 전달한다. 이 점을 기억하도록 하자.

electron-ipc-process

ipcMain에서 on을 통한 이벤트 수신

ipcMain.on('CHANNEL_NAME', (evt, payload) => { })

ipcRenderer에서 send을 통한 이벤트 송신

ipcRenderer.send('CHANNEL_NAME', 'message')

여기서 Main 프로세스Renderer 프로세스의 정의만 잠깐 짚고 넘어가자.

Main Processor

Main 프로세스는 일반적으로 우리가 Electron으로 개발할 때 생성하는 main.js로 볼 수 있다.
이는 Electron 앱의 진입점이기도 하며, 앱의 Life Cycle을 관리한다. 뿐만 아니라 애플리케이션에서 사용되는 메뉴, Dock, 트레이와 같은 네이티브 요소를 관리하며 Main 프로세스는 앱에서 각각의 새로운 Renderer 프로세스를 생성한다.

Chromium에서 이러한 개념을 Browser 프로세스라 하며, Electron에서는 Renderer 프로세스와 혼동을 피하고자 Main 프로세스라 명명한다. 즉, Main 프로세스는 Electron 시스템 내 동작하는 애플리케이션을 제어하는 프로세스로 볼 수 있다.

현재 Electron 11.x 버전에서는 아래와 같은 Main 프로세스 모듈이 존재한다.

  • app
  • autoUpdater
  • BrowserView
  • contentTracing
  • dialog
  • globalShortcut
  • inAppPurchase
  • ipcMain
  • Menu
  • MenuItem
  • net
  • netLog
  • nativeTheme
  • Notification
  • powerMonitor
  • powerSaveBlocker
  • protocol
  • screen
  • session
  • systemPreferences
  • TouchBar
  • Tray
  • webContents
  • webFrameMain

Renderer Processor

Renderer 프로세스는 Electron 앱의 Chromium 기반의 브라우저 창을 관리하는 모듈이다.

Main 프로세스와 달리 Renderer 프로세스는 여러 개가 존재할 수 있으며, Main 프로세스와는 1:N의 관계를 맺는다.

현재 Electron 11.x 버전에서는 아래와 같은 Main 프로세스 모듈이 존재한다.

  • desktopCapturer
  • ipcRenderer
  • remote
  • webFrame

이제 다시 돌아와서 IPC에 대해 알아보자.

위에서 설명했듯이 IPCipcMainipcRenderer 모듈 두 가지로 구분된다.

ipcMain

ipcMan 모듈은 Renderer 프로세스(웹 페이지)가 보내는 메시지 또는 이벤트를 동기적 혹은 비동기적으로 처리한다. 즉, 웹 페이지를 처리하는 프로세스인 Renderer 프로세스가 보내는 메시지를 Main 프로세스는 수신하여 처리하게 된다.

아래에서 ipcRenderer를 보면 알겠지만 보통 ipcMain에서는 IPC 통신을 할 때 수신만 할 수 있다.
위에서 이벤트 수신은 on이며 송신은 send라 하였지만, ipcMain에서는 on으로 송신을 하며 send가 아닌 reply로 회신하는 것이다.

아래 예제를 보자.

ipcMain에서 on을 통한 이벤트 수신

const { ipcMain } = require('electron')

// ipcMain에서의 이벤트 수신
ipcMain.on('CHANNEL_NAME', (evt, payload) => {
console.log(payload)

evt.reply('IPC_RENDERER_CHANNEL_NAME', 'message')
})

위 예제에서 CHANNEL_NAME은 메시지를 송신할 이름을 말하며 channel이라고 한다. 어느 ipcRenderer 프로세스에서 CHANNEL_NAME으로 메시지를 수신하였고 ipMain 프로세스는 ‘CHANNEL_NAME’로 송신하였다. 그리고 다시 reply를 통하여 송신 후 다시 응답하였다.

이 응답의 channel 이름은 IPC_RENDERER_CHANNEL_NAME이다. 이렇게 응답을 하였기에 어느 ipcRenderer 프로세서는 on을 통하여 IPC_RENDERER_CHANNEL_NAME 채널로 수신할 것이다.

기능 측면에서 send의 개념도 가능하다. 하지만 ipcMain 프로세스는 ipcRenderer와 같은 send 메소드를 가지고 있진 않다. send를 하기 위해서는 webContents.send를 이용해야 한다.

webContents.send를 이용한 send 방식

const { app, BrowserWindow } = require('electron')

app.whenReady().then(() => {
const win = new BrowserWindow({
// options
})

win.webContents.send('IPC_RENDERER_CHANNEL_NAME', 'message')
})

ipcMain이 될 수 있는 main.js에서 IPC_RENDERER_CHANNEL_NAME 채널명으로 수신하고 있다. 이는 어느 ipcRenderer에서 IPC_RENDERER_CHANNEL_NAME 채널명으로 송신을 받고 이후의 처리를 할 수 있을 것이다.

이렇게 되면 ipcMain에서도 ipcRenderer와 동일하게 메시지를 송신할 수 있다.

ipcRenderer

ipcRenderer 모듈은 Renderer 프로세스(웹 페이지)에서 Main 프로세스로 동기 또는 비동기 메시지를 보낼 수 있다. Main 프로세스에서 webContents.send 로 메시지를 보냈다면 수신 역시 가능하다.

보통 웹 페이지 딴에서 HTTP 통신을 통해 받아온 데이터를 보내거나 Main 프로세스를 호출할 경우 사용되며, send를 통해 송신하고 on을 통해 수신한다.

ipcRenderer에서 send을 통한 이벤트 송신

const { ipcRenderer } = require('electron')

const payload = 'message'

// ipcRenderer에서의 이벤트 송신
ipcRenderer.send('CHANNEL_NAME', payload)

지금까지 IPC 통신 방법 중 수신이 가능한 on과 송신이 가능한 send에 대해서 설명만 했지만, 이외에도 IPC와 관련된 API들은 많다. 이것에 대해서는 Electron API 문서 - ipcMainElectron API 문서 - ipcRenderer를 참고하도록 하자.

예제 만들기

이제 실제로 예제를 만들면서 ipcMainipcRenderer 모듈이 어떻게 동작하는지 알아보도록 하자.

예제의 동작은 최초 Electron이 구동되면 WenContent.send를 통해서 Renderer 프로세스(웹 페이지)에 정보를 보낼 것이다. 이후 웹 페이지에서는 값을 입력하고 버튼을 클릭하면 입력된 값을 다시 Main 프로세스로 전달할 것이다. 이렇게 전달받은 값은 다시 변형하여 Renderer 프로세스로 전달하는 과정을 만들 것이다.

환경 구축

가장 빠른 방법. electron-quick-start boilerplate를 통해서 구축하자.

git clone https://github.com/electron/electron-quick-start

cd electron-quick-start
npm install
npm run start

코드 수정

먼저 index.html을 열어 아래와 같이 수정하자.

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<script src="./renderer.js"></script>

<div>
<input
id="text-input"
type="text"
>

<button id="btn">Click</button>
</div>

<div>
<span>computed input value => </span>
<span
id="text-box"
style="color: red"
></span>
</div>
</body>
</html>

간단하게 input과 button 그리고 텍스트를 출력할 수 있는 영역을 두었다.

그다음 renderer.js를 수정하자.

renderer.js

const { ipcRenderer } = require('electron')

window.onload = () => {
const btnEl = document.getElementById('btn')

btnEl.addEventListener('click', (evt) => {
const inputValue = document.getElementById('text-input').value

// onInputValue 이벤트 송신
ipcRenderer.send('onInputValue', inputValue)
})

// replyInputValue에 대한 응답 수신
ipcRenderer.on('replyInputValue', (evt, payload) => {
document.getElementById('text-box').textContent = payload
})

// onWebcontentsValue에 대한 이벤트 수신
ipcRenderer.on('onWebcontentsValue', (evt, payload) => {
document.getElementById('text-box').textContent = payload
})
}

최초 페이지가 로드되면 onWebcontentsValue를 통해서 텍스트를 설정할 것이다. 이후 input에 값을 입력하고 버튼을 클릭하면 해당 값이 onInputValue를 통해서 Main 프로세스로 송신될 것이다. 이렇게 송신된 이벤트는 Main 프로세스에서 값을 가공하여 replyInputValue를 통해 응답할 것이고 Renderer 프로세스에서는 이를 받아 텍스트를 출력할 것이다.

이제 Main 프로세스에서 송신과 수신을 지정하자.

main.js

const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')

function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true
}
})

mainWindow.loadFile('index.html')

// 웹 페이지 로드 완료
mainWindow.webContents.on('did-finish-load', (evt) => {
// onWebcontentsValue 이벤트 송신
mainWindow.webContents.send('onWebcontentsValue', 'on load...')
})
}

app.whenReady().then(() => {
createWindow()

app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})

// onInputValue 이벤트 수신
ipcMain.on('onInputValue', (evt, payload) => {
console.log('on ipcMain event:: ', payload)

const computedPayload = payload + '(computed)'

// replyInputValue 송신 또는 응답
evt.reply('replyInputValue', computedPayload)
})
})

app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})

위와 같이 수정 후 아래 코드를 보자.

mainWindow.webContents.on('did-finish-load', (evt) => {
// onWebcontentsValue 이벤트 송신
mainWindow.webContents.send('onWebcontentsValue', 'on load...')
})

이 코드는 Renderer 프로세스에서 웹 페이지가 완료 후에 호출되는 이벤트이다. 이때 Main 프로세스에서는 webcontents.send를 통해서 최초에 ipc 통신을 하는 것이다.
이와 같이 Main 프로세스에서도 reply가 아닌 send를 통해서 ipc 통신 시 송신이 가능하다.

위 코드에서 BrowserWindow객체를 생성 시 nodeIntegration: true를 지정한 이유는 renderer.js에서 require를 통해 electron 모듈을 호출하기 위해서이다.

실행

main.js를 수정하였으므로 npm run start를 통해 다시 구동해보자.

electron-run

webcontent.send를 통해서 최초에 값을 전달 했기 때문에 on load가 출력되는 것을 볼 수 있다.

이제 input 박스에 값을 입력하고 버튼을 클릭해 보자.

electron-input-run

예제에서는 test를 입력하고 버튼을 클릭하니 결과값은 test(computed)로 출력되었다. 이는 Main 프로세스에서 수신받은 payload를 가공하여 다시 송신하였기 때문이다.

수신받은 payload를 가공하여 응답

// onInputValue 이벤트 수신
ipcMain.on('onInputValue', (evt, payload) => {
console.log('on ipcMain event:: ', payload)

const computedPayload = payload + '(computed)'

// replyInputValue 송신 또는 응답
evt.reply('replyInputValue', computedPayload)
})

여기서 console.log를 남겼기 때문에 콘솔을 확인해보자.

computed-data

콘솔 로그를 보면 입력된 값이 그대로 출력되는 것을 확인할 수 있다.


이렇게 Electron에서 IPC 통신에 대해서 개념과 예제를 통해서 알아보았다.

정리를 하면 IPC는 Electron에서 프로세스 간의 통신 방법을 말하며, 모듈에는 icpMainRenderer 프로세스가가 있다. 서로 간의 이벤트 수신은 on이며, 송신은 send이다. 이외에 webContents.send가 있다.

IPC 통신은 프로세스 간 통신 방법이기에 많이 사용된다. 하지만 중요한 건 Electron은 웹 애플리에션에서 하나의 애플리케이션에 REST API가 같이 있는 격이다. 파일별 분리. API의 설계. 등 구조적으로 잘 관리가 되지 않으면 복잡해질 수 있다.
정리가 되지 않고 구조적이지 않으면 이후에 수만은 IPC 모듈에서 수신 및 송신의 코드 추적이 어려워진다.