Skip to main content

프로세스 모델

Electron은 크로미움(Chromium)의 멀티 프로세스 아키텍처를 상속받는다. 이로 인해 Electron의 구조는 현대 웹 브라우저와 매우 유사하다. 이 가이드는 튜토리얼에서 다룬 개념을 더 깊이 있게 설명한다.

왜 싱글 프로세스를 사용하지 않을까?​

웹 브라우저는 매우 복잡한 애플리케이션이다. 웹 콘텐츠를 표시하는 기본 기능 외에도, 여러 윈도우(또는 탭)를 관리하고 서드파티 확장 프로그램을 로드하는 등 다양한 부가적인 책임을 지닌다.

과거에는 브라우저가 이러한 모든 기능을 싱글 프로세스로 처리하는 경우가 많았다. 이 방식은 열린 각 탭마다 오버헤드가 적다는 장점이 있지만, 한 웹사이트가 충돌하거나 멈출 경우 전체 브라우저에 영향을 미친다는 단점이 있다.

멀티 프로세스 모델​

이 문제를 해결하기 위해 Chrome 팀은 각 탭이 별도의 프로세스에서 렌더링되도록 결정했다. 이렇게 하면 웹 페이지의 버그나 악성 코드가 전체 애플리케이션에 미치는 피해를 제한할 수 있다. 단일 브라우저 프로세스가 이러한 프로세스들을 제어하며, 전체 애플리케이션의 라이프사이클도 관리한다. 아래 Chrome Comic에서 발췌한 다이어그램은 이 모델을 시각적으로 보여준다:

Chrome의 멀티 프로세스 아키텍처

Electron 애플리케이션도 매우 유사한 구조를 가진다. 앱 개발자는 두 가지 타입의 프로세스를 관리한다: 메인 프로세스와 렌더러 프로세스. 이들은 위에서 설명한 Chrome의 브라우저 프로세스와 렌더러 프로세스와 유사하다.

메인 프로세스​

각 Electron 앱은 단일 메인 프로세스를 가지고 있으며, 이는 애플리케이션의 진입점 역할을 한다. 메인 프로세스는 Node.js 환경에서 실행되기 때문에, require를 통해 모듈을 불러올 수 있고 Node.js의 모든 API를 사용할 수 있다.

윈도우 관리​

메인 프로세스의 주요 목적은 BrowserWindow 모듈을 사용해 애플리케이션 윈도우를 생성하고 관리하는 것이다.

BrowserWindow 클래스의 각 인스턴스는 별도의 렌더러 프로세스에서 웹 페이지를 로드하는 애플리케이션 윈도우를 생성한다. 윈도우의 webContents 객체를 통해 메인 프로세스에서 이 웹 콘텐츠와 상호작용할 수 있다.

main.js
const { BrowserWindow } = require('electron')

const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')

const contents = win.webContents
console.log(contents)

참고: BrowserView 모듈과 같은 웹 임베드를 위해 렌더러 프로세스도 생성된다. 임베드된 웹 콘텐츠의 경우에도 webContents 객체에 접근할 수 있다.

BrowserWindow 모듈은 EventEmitter이기 때문에, 윈도우를 최소화하거나 최대화하는 것과 같은 다양한 사용자 이벤트에 대한 핸들러를 추가할 수도 있다.

BrowserWindow 인스턴스가 파괴되면, 해당 렌더러 프로세스도 함께 종료된다.

애플리케이션 생명주기​

메인 프로세스는 Electron의 app 모듈을 통해 애플리케이션의 생명주기를 제어한다. 이 모듈은 다양한 이벤트와 메서드를 제공하며, 이를 활용해 커스텀 애플리케이션 동작을 추가할 수 있다. 예를 들어, 프로그램적으로 애플리케이션을 종료하거나, 애플리케이션 도크를 수정하거나, About 패널을 표시하는 등의 작업이 가능하다.

실제 예제로, 튜토리얼 시작 코드에 나온 애플리케이션은 app API를 사용해 더 네이티브한 애플리케이션 윈도우 경험을 제공한다.

main.js
// macOS가 아닌 플랫폼에서 모든 윈도우가 닫히면 앱을 종료
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})

네이티브 API​

Electron은 웹 콘텐츠를 위한 Chromium 래퍼 이상의 기능을 제공하기 위해 메인 프로세스에 사용자 운영체제와 상호작용할 수 있는 커스텀 API를 추가한다. Electron은 메뉴, 대화상자, 트레이 아이콘과 같은 네이티브 데스크톱 기능을 제어하는 다양한 모듈을 제공한다.

Electron의 메인 프로세스 모듈 전체 목록은 API 문서에서 확인할 수 있다.

렌더러 프로세스​

각 Electron 앱은 열린 BrowserWindow마다 별도의 렌더러 프로세스를 생성한다. 이름에서 알 수 있듯, 렌더러는 웹 콘텐츠를 렌더링 하는 역할을 담당한다. 렌더러 프로세스에서 실행되는 코드는 웹 표준에 따라 동작해야 한다(적어도 Chromium이 지원하는 범위 내에서).

따라서 단일 브라우저 윈도우 내의 모든 사용자 인터페이스와 앱 기능은 웹에서 사용하는 도구와 패러다임을 그대로 적용해 작성해야 한다.

모든 웹 사양을 설명하는 것은 이 가이드의 범위를 벗어나지만, 최소한 이해해야 할 기본 사항은 다음과 같다:

  • HTML 파일이 렌더러 프로세스의 시작점이다.
  • UI 스타일은 CSS(캐스케이딩 스타일 시트)를 통해 추가한다.
  • 실행 가능한 JavaScript 코드는 <script> 엘리먼트를 통해 추가할 수 있다.

또한, 렌더러는 require나 다른 Node.js API에 직접 접근할 수 없다. 렌더러에서 NPM 모듈을 직접 포함하려면 웹에서 사용하는 번들러 도구 체인(예: webpack 또는 parcel)을 사용해야 한다.

warning

개발 편의를 위해 렌더러 프로세스를 완전한 Node.js 환경으로 실행할 수 있다. 과거에는 이 기능이 기본적으로 활성화되어 있었지만, 보안상의 이유로 비활성화되었다.

이 시점에서 여러분은 렌더러 프로세스의 사용자 인터페이스가 어떻게 Node.js 및 Electron의 네이티브 데스크톱 기능과 상호작용할 수 있는지 궁금할 수 있다. 특히 이러한 기능이 메인 프로세스에서만 접근 가능하다면 말이다. 사실, Electron의 콘텐츠 스크립트를 직접 가져오는 방법은 없다.

사전 로드 스크립트​

사전 로드 스크립트는 렌더러 프로세스에서 웹 콘텐츠가 로드되기 전에 실행되는 코드를 포함한다. 이 스크립트는 렌더러 컨텍스트 내에서 실행되지만, Node.js API에 접근할 수 있는 더 많은 권한을 가진다.

사전 로드 스크립트는 BrowserWindow 생성자의 webPreferences 옵션을 통해 메인 프로세스에 연결할 수 있다.

main.js
const { BrowserWindow } = require('electron')
// ...
const win = new BrowserWindow({
webPreferences: {
preload: 'path/to/preload.js'
}
})
// ...

사전 로드 스크립트는 렌더러와 동일한 전역 Window 인터페이스를 공유하며 Node.js API에 접근할 수 있기 때문에, window 전역에 임의의 API를 노출시켜 웹 콘텐츠가 이를 사용할 수 있도록 하는 역할을 한다.

하지만 사전 로드 스크립트가 렌더러와 동일한 window 전역을 공유하더라도, contextIsolation이 기본적으로 활성화되어 있기 때문에 사전 로드 스크립트의 변수를 직접 window에 연결할 수 없다.

preload.js
window.myAPI = {
desktop: true
}
renderer.js
console.log(window.myAPI)
// => undefined

컨텍스트 격리(Context Isolation)는 사전 로드 스크립트가 렌더러의 메인 세계와 분리되어 있어, 권한이 있는 API가 웹 콘텐츠의 코드로 유출되는 것을 방지한다.

대신, contextBridge 모듈을 사용해 이를 안전하게 수행할 수 있다:

preload.js
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myAPI', {
desktop: true
})
renderer.js
console.log(window.myAPI)
// => { desktop: true }

이 기능은 주로 두 가지 목적으로 매우 유용하다:

  • ipcRenderer 헬퍼를 렌더러에 노출시켜, 렌더러에서 메인 프로세스의 작업을 트리거할 수 있는 프로세스 간 통신(IPC)을 사용할 수 있다(그 반대도 가능).
  • 원격 URL에 호스팅된 기존 웹 앱을 위한 Electron 래퍼를 개발하는 경우, 렌더러의 window 전역에 커스텀 속성을 추가해 웹 클라이언트 측에서 데스크톱 전용 로직을 사용할 수 있다.

유틸리티 프로세스​

Electron 앱은 메인 프로세스에서 UtilityProcess API를 사용해 여러 자식 프로세스를 생성할 수 있다. 유틸리티 프로세스는 Node.js 환경에서 실행되며, 이는 require를 통해 모듈을 불러오고 Node.js의 모든 API를 사용할 수 있다는 의미다. 유틸리티 프로세스는 신뢰할 수 없는 서비스, CPU 집약적인 작업, 또는 크래시가 발생하기 쉬운 컴포넌트를 호스팅하는 데 사용할 수 있다. 이전에는 이러한 작업들이 메인 프로세스나 Node.js의 child_process.fork API로 생성된 프로세스에서 실행되었다.

유틸리티 프로세스와 Node.js의 child_process 모듈로 생성된 프로세스의 주요 차이점은, 유틸리티 프로세스가 MessagePort를 사용해 렌더러 프로세스와 통신 채널을 설정할 수 있다는 점이다. Electron 앱은 메인 프로세스에서 자식 프로세스를 생성해야 할 때, Node.js의 child_process.fork API 대신 UtilityProcess API를 사용하는 것이 더 적합하다.

프로세스별 모듈 별칭 (TypeScript)​

Electron의 npm 패키지는 Electron의 TypeScript 타입 정의 일부를 포함하는 하위 경로도 제공한다.

  • electron/main은 메인 프로세스 모듈에 대한 타입을 포함한다.
  • electron/renderer는 렌더러 프로세스 모듈에 대한 타입을 포함한다.
  • electron/common은 메인과 렌더러 프로세스 모두에서 실행될 수 있는 모듈에 대한 타입을 포함한다.

이 별칭들은 런타임에 영향을 미치지 않지만, 타입 체크와 자동 완성에 사용할 수 있다.

사용 예제
const { app } = require('electron/main')
const { shell } = require('electron/common')