본문 바로가기
공부/etc

[Electron] input이 focus되지 않는 이슈 해결하기 (+ IPC 통신을 곁들여서)

by Piva 2024. 5. 6.
  • 최근에 잠깐 Electron을 써볼 기회가 있었는데, 그 때 접한 이슈 해결방법에 대해 기록한다.

  최근 Electron을 사용해 개발을 하던 중 이상한 이슈에 부딪혔다. 그것은 돌연히 input 태그가 먹통이 되면서 아무런 입력이 되지 않는 문제였다. 이 현상은 Electron에서 크롬 개발자 모드를 열거나 잠시 Electron 프로그램 창을 최소화 후 여는 것으로 해결이 되었지만, 매번 이와 같은 문제가 일어났을 때 개발자 모드를 열 수도 없는 노릇이었기에... 원인을 찾아 나섰다.

 

원인 - window.alert()의 호출

  알고 보니 이 이슈는 꽤 오래된 이슈였다.

(관련 GitHub 이슈)

 

the input-box lose focus after call window.alert('...') · Issue #19977 · electron/electron

Preflight Checklist I have read the Contributing Guidelines for this project. I agree to follow the Code of Conduct that this project adheres to. I have searched the issue tracker for an issue that...

github.com

 

  'Electron에서 input태그가 focus되지 않음'으로 서치하니 원인이 나왔는데, 바로 Web API 중 하나인 window.alert 함수였다. 간단히 알아보니, window.alert 함수를 호출하면 Electron 앱 자체가 포커스를 잃어버려서라는 듯 하다. 참고로, MacOS에서는 발생하지 않는다(오직 Window 환경에서만 발생한다!).

 

  위 링크를 보면 알겠지만 이런 저런 해결방법이 올라와있는데, 개인적으로 느낀 가장 간단한 방법은 Electron에서 제공하는 API를 사용하는 방법이라, 이를 통해 해결했다.

 

 

dialog 사용하기 ( + IPC 통신)

  dialog를 사용하면 Electron 앱 내부에서도 alert와 비슷한 알림 창을 띄우는 것이 가능하다.

 

dialog | Electron

Display native system dialogs for opening and saving files, alerting, etc.

www.electronjs.org

 

  위의 문서에서도 적혀있듯, Main 프로세스에서 사용가능하기에 React 단에서 사용하려면 IPC 통신을 통해 해결해야 한다.

 

* IPC(Inter-Process Communication) 통신이란?

  Electron에는 Main 프로세스와 Renderer 프로세스의 두 가지 프로세스가 존재한다.

 

Main process

  Electron 앱의 엔트리 포인트로서 node.js 환경에서 동작한다. 그렇기에 require를 통해 모듈을 불러오는 것도, node.js API를 사용하는 것도 가능하다. 

  Main 프로세스의 주요 목적은 BrowserWindow 모듈을 통해 어플리케이션 윈도우를 생성하고 관리하는 것. 또한 어플리케이션의 생명 주기를 관리하는 역할도 수행한다.

 

 

Renderer process

  각각 BrowserWindow에 대해 생성되는 프로세스로, 웹 컨텐츠를 '렌더링'하는 역할을 갖는다(개인적으로 이해하기로는, 일반적은 React 관련 코드가 이쪽에 속한다고 봐도 무방한 것 같다...). 따라서 Main 프로세스와는 달리 Node.js API를 사용하는 것이 불가능하다.

 

 

  위에 언급된 바와 같이, 렌더러 프로세스는 메인과 달리 어플리케이션의 네이티브한 기능에 접근하는 것이 불가능하다. 따라서, 렌더러 단에서 이쪽 기능(ex. 메인 프로세스만이 가능한 기능들, 즉 어플리케이션을 직접 제어하는 기능)에 접근하려면 별도의 방법이 필요하다. 그것이 바로 IPC 통신이다.

 

IPC 통신 예시

  IPC 통신의 일례로, 렌더러와 메인 프로세스는 서로에게서 보내진 메시지를 받을 수 있는 리스너를 등록하는 것이 가능하다. 위에서 언급한 것처럼 렌더러가 메인 프로세스의 기능을 사용하고 싶을 때는 렌더러에서 신호를 보내고, 메인이 이를 받아 기능을 대신 수행하는 방식으로 구현할 수 있다.

 

// main.ts

import { ipcMain, dialog } from 'electron';

let mainWindow;

/* Electron main window setting code... */

ipcMain.on('showDialog', async (event, arg) => {
  const options = { message: 'Sample message', buttons: ['Yes', 'No'] };
  dialog.showMessageBox(mainWindow, options);
});
  • 메인 프로세스의 코드. ipcMain.on을 통해 'showDialog'라는 이름의 채널로 메시지가 도착하면 등록된 리스너를 통해 정해진 동작을 수행한다.
  • 위 코드에서는 dialog를 통해 전달받은 인자를 넣은 메시지 창을 띄우고 있다.

 

// preload.ts

import { contextBridge, ipcRenderer } from 'electron';

const electronHandler = {
  ipcRenderer: {
    sendMessage(channel, ...args) {
      ipcRenderer.send(channel, ...args);
    },
  },
};

contextBridge.exposeInMainWorld('electron', electronHandler);
  • preload 스크립트를 사용하여 메인과 렌더러 프로세스를 연결한다. preload 스크립트는 렌더러 프로세스 내부에서도 Node.js API를 사용할 수 있게끔 렌더러와 메인을 연결하는 역할을 한다고 생각하면 된다.
  • 렌더러에서 호출할 수 있는 함수(sendMessage)를 정의하고, 그 내부에서는 메인 프로세스에 채널 메시지를 보낼 수 있도록 send 를 사용한다.
  • 이렇게 렌더러에서 사용할 수 있는 API를 정의한 후, contextBridge의 exposeInMainWorld 를 통해 렌더러에서 호출할 수 있도록 한다. 렌더러(즉, 리액트 쪽)에서는 위에서 정의한 API를 window.electron.ipcRenderer의 형태로 호출할 수 있게 된다.

 

// 위에서 정의한 API를 호출한다.
window.electron.ipcRenderer.sendMessage(
  'showDialog',
  'Item is successfully removed!',
);
  • 실제 리액트 단에서 Electron API를 호출하는 모습이다.
  • 위에서 정의한 showDialog 라는 채널로 메시지를 보냄으로써, 등록해둔 dialog함수가 메인에서 실행될 수 있게 한다.

잘 작동하는 모습!

 


  • 원래는 트러블 슈팅에 관해서만 쓸 예정이었는데, 쓰다보니 IPC 통신까지 건드리며 엄청 길어졌다(!). IPC 통신은 Electron에서 중요한 개념이기도 해서, 언젠가 좀더 제대로 이에 대해 다룰 수 있으면 좋을 것 같다. 나중에 시간이 되면 간단한 프로젝트를 빌드까지 해서 과정을 남겨두고 싶다.
  • 사실 실제로 input이 먹통이 되는 걸 올리려 했는데, Mac에서 재현이 안 된다는 걸 열심히 예제 만들면서 알았다...!