IPC 통신
IPC(Inter-Process Communication)는 프로세스 간 통신을 의미합니다.
Electron에서는 메인 프로세스와 렌더러 프로세스 간의 통신에 사용됩니다.
Electron의 IPC 통신 특징
- 비동기적 통신 지원
- 양방향 통신 가능
- 보안성 강화를 위한 컨텍스트 격리
ipcMain과 ipcRenderer
ipcMain은 메인 프로세스에서, ipcRenderer는 렌더러 프로세스에서 사용되는 모듈입니다.
메인 프로세스
- 애플리케이션의 생명주기 관리
- 시스템 리소스 접근 (파일 시스템, 네트워크 등)
- 네이티브 API 사용 (메뉴, 트레이 아이콘 등)
- 새 렌더러 프로세스 생성 및 관리
렌더러 프로세스
- 웹 페이지 렌더링 및 사용자 인터페이스 표시
- DOM 조작 및 웹 API 사용
- 제한된 Node.js API 접근 (contextIsolation 설정에 따라)
- 사용자 입력 처리 및 UI 이벤트 관리
Electron IPC 통신의 기본 패턴
가장 기본적인 형태는 아래와 같은 구성으로 핸들러를 등록하고 사용하는 형태입니다.
// src/ipcMethods.ts
import { ipcMain } from 'electron'
ipcMain.handle( 'foo', ( e, arg1: string ) => {
console.log( 'bar!!!', arg1 )
return 100
} )
ipcMain.handle( 'alpha', async ( e, arg1: number, arg2: number, arg3: number ) => {
// 1초 딜레이
await new Promise<void>( ( res ) => {
setTimeout( () => res(), 1000 )
} )
console.log( 'bravo!!!', arg1, arg2, arg3 )
return arg1 + arg2 === arg3
} )
// src/main.ts
import './ipcMethods'
import { app, BrowserWindow } from 'electron'
...
// src/react/some.tsx
const { ipcRenderer } = window.require( 'electron/renderer' )
const Some = () => {
const action = async () => {
const a = await ipcRenderer.invoke( 'foo', 33 )
const b = await ipcRenderer.invoke( 'alpha', 'hi', 'hello', 'hihello' )
console.log( a, b )
}
return <button onClick={action}>click</button>
}
export default Some
불편한 사용성
ipcMain과 ipcRenderder 함수를 그대로 사용하게 되면 개발 과정에서 여러 불편한 점이 있습니다.
- ipcRenderer는 어떤 채널이 등록되어 있는지 알 수 없다.
- ipcRenderer는 지금 통신하려는 채널의 전달인자가 몇 개 있을지, 어떤 타입인지 알 수 없다.
- ipcRenderer는 지금 통신하려는 채널의 반환 값이 어떤 타입인지 알 수 없다.
실제로 위 예시를 자세히 보면 ‘foo’ 채널에서는 첫 번째 전달인자로 string
타입을 원하고 있지만 number
타입을 입력하고 있습니다. 하지만 invoke함수에서 모든 전달인자의 타입은 any
이기 때문에 ts서버는 오류를 띄우지 않습니다. 반환값의 타입 또한 알 수 없겠죠.
그러다 보니 alpha채널의 통신은 전달인자들 에게number
타입을 원하였고 이중 일부를 더한(숫자 연산)값이 한 값과 일치하는 지를 돌려주는데도 불구하고,
string
타입을 넣어 문자열 연산으로 계산해 일치 여부를 돌려주는 의도와 다른 (이 경우에는 심지어 에러조차 나지 않으니 발견이 늦어질 수 있습니다) 결과를 받게 될 수 있습니다.
Electron IPC 통신 사용성 향상 방법
저는 위와 같은 상황을 타개하기 위해 아래의 방법을 사용했습니다.
ipcHandler
타입을 만들어 채널과 함수에 대해 정의ipcMain
과ipcRenderer
의 커스텀
ipcHandler
타입 정의
채널명을 key로 하는 type을 export가능하게 생성해 줍니다.
// src/ipcMethods.ts
export type IpcHandler = {
foo: ( arg1: string ) => number
alpha: ( arg1: number, arg2: number, arg3: number ) => Promise<boolean>
}
ipcMain
과 ipcRenderer
의 커스텀
이제 ipcHandler
타입을 받아와 핸들을 등록하거나 invoke
로 통신할 때에 활용할 수 있도록 구성해줍시다.
ipcMain
먼저 Electron.IpcMain
를 상속 하는 customIpcMain
이라는 인터페이스를 생성합니다.
이 때, 인터페이스와 핸들에 제네릭을 지정하여 ipcHandler와 연계할 수 있도록 구성했습니다.
// src/ipcMethods.ts
interface CustomIpcMain<T extends Record<string, ( ...args: unknown[] ) => unknown>> extends Electron.IpcMain {
handle: {
<J extends keyof T>(
channel: J,
fn: ( ...args: [e: Electron.IpcMainInvokeEvent, ...Parameters<T[J]>] ) => ReturnType<T[J]>
): void
}
}
먼저 상속할 IpcMain의 handle에는 callback 함수의 첫 번째 전달인자로 이벤트가, 이후에는 각 핸들러 함수의 전달인자들이 들어올 수 있으니 이에 대응하여 구성해 둡니다.
ipcRenderer
// src/react/hooks/useIpcRenderer.ts
import type { IpcHandler } from 'src/ipcMethods'
interface CustomIpcRenderer<T extends Record<string, ( ...args: unknown[] ) => unknown>> extends Electron.IpcRenderer {
invoke: {
<J extends keyof T>( channel: J, ...args: Parameters<T[J]> ): Promise<Awaited<ReturnType<T[J]>>>
}
}
렌더러에서 역시 IpcRenderer의 invoke함수에 맞도록 맞춰줘야 하는데,Promise<Awaited<ReturnType<T[J]>>>
구문에서 알 수 있듯이 invoke함수는 항상 Promise를 돌려줍니다.
다만 핸들러 함수가 비동기 함수이든 일반 함수 이든 대응할 수 있도록 Awaited
유틸 타입을 이용했습니다. (Promise<Promise<any>>
와 같은 형태는 존재해선 안되니까요..!
실제 코드와 연결 및 활용 이점
// src/ipcMethods.ts
// 다른 변수명으로 import 해둔다
import { ipcMain as _ipcMain } from 'electron'
export type IpcHandler = {
foo: ( arg1: string ) => number
alpha: ( arg1: number, arg2: number, arg3: number ) => Promise<boolean>
}
interface CustomIpcMain<T extends Record<string, ( ...args: unknown[] ) => unknown>> extends Electron.IpcMain {
handle: {
<J extends keyof T>(
channel: J,
fn: ( ...args: [e: Electron.IpcMainInvokeEvent, ...Parameters<T[J]>] ) => ReturnType<T[J]>
): void
}
}
// 원래 할당하려던 변수에 커스텀 인터페이스 및 핸들러 타입 연결
const ipcMain = _ipcMain as CustomIpcMain<IpcHandler>
ipcMain.handle( 'foo', ( e, arg1 ) => {
console.log( 'bar!!!', arg1 )
return 100
} )
ipcMain.handle( 'alpha', async ( e, arg1, arg2, arg3 ) => {
// 1초 딜레이
await new Promise<void>( ( res ) => {
setTimeout( () => res(), 1000 )
} )
console.log( 'bravo!!!', arg1, arg2, arg3 )
return arg1 + arg2 === arg3
} )
이젠 ipcMain
에서 핸들에 등록 할 때에 어떤 채널이 있는지, 해당 채널엔 어떤 유형의 callback함수가 필요한 지 알 수 있습니다.
// src/react/hooks/useIpcRenderer.ts
import type { IpcHandler } from 'src/ipcMethods'
interface CustomIpcRenderer<T extends Record<string, ( ...args: unknown[] ) => unknown>> extends Electron.IpcRenderer {
invoke: {
<J extends keyof T>( channel: J, ...args: Parameters<T[J]> ): Promise<Awaited<ReturnType<T[J]>>>
}
}
// 이 hooks는 react hooks의 기본 기능을 사용하지 않아 다른 위치에서도 호출이 가능하다.
const ipcRendererLazy = {} as {
// 커스텀 인터페이스 및 핸들러 타입 연결
ipcRenderer: CustomIpcRenderer<IpcHandler>
}
export function useIpcRenderer() {
if ( !ipcRendererLazy.ipcRenderer ) {
const { ipcRenderer } = window.require( 'electron/renderer' )
ipcRendererLazy.ipcRenderer = ipcRenderer
}
return { ipcRenderer: ipcRendererLazy.ipcRenderer }
}
// src/react/some.tsx
import { useIpcRenderer } from './hooks/useIpcRenderer'
const Some = () => {
const { ipcRenderer } = useIpcRenderer()
const action = async () => {
const a = await ipcRenderer.invoke( 'foo', 33 )
const b = await ipcRenderer.invoke( 'alpha', 'hi', 'hello', 'hihello' )
console.log( a, b )
}
return <button onClick={action}>click</button>
}
export default Some
ipcRenderer
에서도 invoke
할 때, 잘못된 채널로 통신을 시도하거나, 해당 채널에 잘못된 배개변수를 넣지 못하도록 오류를 띄울 수 있게 되었습니다.
또한, ipcMain
에서 처리한 리턴값의 타입을 알 수 있게 되었습니다.
결론
이러한 방식으로 Electron의 IPC 통신을 개선함으로써 아래와 같은 이점을 얻을 수 있습니다:
- 타입 안정성:
잘못된 채널명이나 매개변수 사용을 컴파일 시점에 방지할 수 있다. - 코드 자동완성
IDE에서 채널명과 매개변수에 대한 자동완성 기능을 활용할 수 있어 개발 생산성이 향상된다. - 가독성 향상
코드의 의도를 명확히 파악할 수 있어 유지보수가 용이해진다. - 런타임 오류 감소
타입 검사로 인해 런타임에 발생할 수 있는 오류를 사전에 방지할 수 있습니다.
결과적으로, 이러한 접근 방식은 Electron 애플리케이션의 안정성을 높이고 개발 과정을 더욱 효율적으로 만들어줄 수 있습니다. TypeScript
의 강력한 타입 시스템을 활용하여 IPC 통신의 안정성과 사용성을 크게 향상시킬 수 있으며, 이는 대규모 Electron 프로젝트에서 특히 유용할 것 같습니다.
'JavaScript | 자바스크립트 > Electron' 카테고리의 다른 글
Electron + NextUI / focus error (0) | 2024.11.09 |
---|
댓글