[Electron] 6.프로세스와 IPC 통신 - 레시피 일렉트론 앱

개요

이미지 파일과 압축 파일 모두 DB에 삽입했으면, 이제 기능을 구현해야한다. 현재까지는 앱 실행시에 이미지 파일과 압축 파일을 DB에 넣는 클래스만 main.js(메인 프로세스)에 Import하여 사용했다. 어차피 DB 테이블 완성을 위한 1회성 클래스 및 함수였기 때문에, 구조를 생각하지 않고 메인 프로세스에서 한번만 돌아가면 됐었다.

 

하지만 이제부터는 몇가지 기능에 관련된 요청과 응답이 메인 프로세스(Main process)와 렌더러 프로세스(Renderer process) 사이에서 오가며, 그러한 통신들을 처리해줄 클래스들이 필요하다.

소스 구조 변경

사진1. 프로젝트 경로

 

우선 사진1과 같이 프로세스에 맞춰 프로젝트 경로를 재구성해주었다. main 폴더에는 각 메인 프로세스에서 돌아갈 스크립트들을 넣어주고, renderer 폴더에는 앱이 보여줄 각 페이지 별로 다시 폴더를 나누어 그 안에 GUI가 될 html 파일과 렌더러 프로세스가 될 renderer.js 스크립트를 넣어주었다.

Main process & Renderer process

electron docs: https://www.electronjs.org/docs/latest/tutorial/process-model

 

Process Model | Electron

Electron inherits its multi-process architecture from Chromium, which makes the framework architecturally very similar to a modern web browser. This guide will expand on the concepts applied in the tutorial.

www.electronjs.org

Electron 은 하나의 메인 프로세스와 여러개의 렌더러 프로세스로 이루어져있다.

 

메인 프로세스는 Node 기반으로 동작하며 앱의 window를 관리(생성, 삭제 등)하고 내부적인 일을 처리한다. 웹의 Back 단의 영역과 유사하다.

렌더러 프로세스는 익숙한 웹 개발 기반의 소스들(HTML, CSS, Javascript)로 동작하며 화면마다 각각 지니게 된다. 웹의 Front 단 영역과 유사하다.

 

우선 config 파일에서 메인 프로세스와 렌더러 프로세스의 기반이 될 파일 경로들을 매핑해줘야 한다. (메인 프로세스와 렌더러 프로세스 하나는 기본으로 설정되어 있다.)

ps. 프로젝트 환경에 따라 forge 프레임워크, webpack 플러그인을 사용하는 기준으로 기록한다.

//webpack.main.config.js
module.exports = {
  /**
   * This is the main entry point for your application, it's the first file
   * that runs in the main process.
   */
  entry: './src/main/main.js',
  // Put your normal webpack config below here
  module: {
    rules: require('./webpack.rules'),
  },
};
//forge.config.js
module.exports = {
  packagerConfig: {
    asar: true,
  },
  rebuildConfig: {},
  makers: [
		.
		.
		.
  ],
  plugins: [
    {
      name: '@electron-forge/plugin-auto-unpack-natives',
      config: {},
    },
    {
      name: '@electron-forge/plugin-webpack',
      config: {
        mainConfig: './webpack.main.config.js',
        renderer: {
          config: './webpack.renderer.config.js',
          entryPoints: [
            {
              name: 'main_window',
              html: './src/renderer/main_window/index.html',
              js: './src/renderer/main_window/renderer.js',
              preload: {
                js: './src/preload.js'
              }
            },
            {
              name: 'search_result',
              html: './src/renderer/search_result/index.html',
              js: './src/renderer/search_result/renderer.js',
              preload: {
                js: './src/preload.js'
              }
            },            
          ],
        },
      },
    },
  ],
};

메인 프로세스 경로 설정: webpack.main.config.js 의 entry에 main.js 경로를 매핑한다. (변경 가능)

렌더러 프로세스 경로 설정: forge.config.js 의 plugins > webpack 관련 항목 > confing > renderer > entrypoints 내에 경로를 매핑한다.

IPC 통신

electron docs: https://www.electronjs.org/docs/latest/tutorial/ipc?origin-link-is-now-working

 

Inter-Process Communication | Electron

Use the ipcMain and ipcRenderer modules to communicate between Electron processes

www.electronjs.org

 

Electron의 로직은 보통 렌더러에서 요청을 하면 메인에서 응답을 보내주는 식으로 처리된다. 이때 프로세스 간 요청과 응

답을 처리해주는 것이 IPC 통신이다. 메인 프로세스에서는 ipcMain 모듈을 사용하고, 렌더러 프로세스에서는 ipcRenderer 모듈을 사용한다.

//main.js
import RcpIPC from './rcp_ipc';
.
.
.
let rcpIPC;
const createWindow = () => {
	.
	.
	.
  mainWindow.once('ready-to-show', () => {
		.
		.
		.
    rcpIPC = new RcpIPC(mainWindow, db);
    rcpIPC.registerIPC();
  });

};
//rcp_ipc.js
const {ipcMain} = require('electron');

export default class RcpIPC {
    mainWindow;
    db;

    constructor(_mainWindow, _db) {
        console.log('RcpIPC constructor');

        this.mainWindow = _mainWindow;
        this.db = _db;
    }
   
    addSearchRcpList() {
        ipcMain.on('req_searchRcpListAll', (event, keyword, pageNum) => {
            console.log('main: req_searchRcpListAll');
            try {
                keyword = keyword.trim();
                
                let offset = (pageNum > 1) ?  (pageNum-1)*10 : 0;

                let searchResult = this.db.selectAllByKeyword(keyword,offset);
                event.reply('resp_searchRcpListAll', searchResult);

            } catch (e) {
                console.log(e);
                event.reply('resp_searchRcpListAll', 'error');
            }

        });

    }

    registerIPC() {
        this.addSearchRcpList();     
    }

}
//preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('apis', {
    
    req_searchRcpListAll: (keyword, pageNum) => ipcRenderer.send('req_searchRcpListAll', keyword, pageNum),
    resp_searchRcpListAll: (searchResult) => ipcRenderer.on('resp_searchRcpListAll', searchResult),

  });
//renderer.js
window.$ = window.jQuery = require('jquery');

window.onload = () => {  
    //keyword setting for search & paging
    const urlParams = new URLSearchParams(window.location.search);
    let keyword = urlParams.get('keyword');
    window.apis.req_searchRcpListAll(keyword, 1);
}

window.apis.resp_searchRcpListAll((event, searchResult)=>{  
    console.log('renderer: resp_searchRcpListAll');
    if (searchResult.length == 0) {
      alert('No results were found for your search.');
  
    } else if (searchResult != 'error') { // search success
      setSearchResult(searchResult);
      console.log(searchResult);
    } else { // error or no results
        alert('else error');
    }
});

ipcMain

ipcMain 모듈은 RcpIPC 라는 클래스를 따로 작성하여 사용한다. main.js에서 RcpIPC 인스턴스를 생성하고 ipc 통신에 관한 처리를 모두 맡길 것이다. RcpIPC 내 에서는 ipc 채널 리스너를 담는 함수를 각각 만들고, 모든 함수를 한번에 호출하여 리스너를 등록하는 함수를 만들었다. main.js 에서는 인스턴스를 생성하고 registerIPC 함수만 한번 호출하면 모든 ipcMain 리스너를 한 번에 등록할 수 있다.

위 rcp_ipc.js 소스에서는 req_searchRcpListAll 채널에서 온 요청을 처리하고 resp_searchRcpListAll채널로 응답을 보내는 로직을 구현했다.

ipcRenderer

ipcRenderer 모듈은 preload.js 에서 사용한다. preload.js는 Node.js 모듈 활용, ipc 통신, 보안 강화 등을 위해 사전에 불러오는 스크립트 파일이다. 기본적으로 렌더러 프로세스는 Node.js 모듈에 접근할 수 없다. 따라서 preload.js를 통해 모듈에 접근하여 렌더러 프로세스가 전역변수로 ipcRenderer를 사용할 수 있도록 설정한다. ipcMain에서 정한 채널명과 파라미터를 맞추어서 전역변수에 할당한다. 할당한 이름으로 renderer.js에서 함수 혹은 리스너를 호출하여 사용할 수 있다.

이제 페이지나 필요한 ipc 채널이 생길 때마다 위 규칙에 맞게 추가해주면 된다.