프로세스 모델
Electron은 크로미움(Chromium)의 멀티 프로세스 아키텍처를 상속받는다. 이로 인해 Electron의 구조는 현대 웹 브라우저와 매우 유사하다. 이 가이드는 튜토리얼에서 다룬 개념을 더 깊이 있게 설명한다.
왜 싱글 프로세스를 사용하지 않을까?
웹 브라우저는 매우 복잡한 애플리케이션이다. 웹 콘텐츠를 표시하는 기본 기능 외에도, 여러 윈도우(또는 탭)를 관리하고 서드파티 확장 프로그램을 로드하는 등 다양한 부가적인 책임을 지닌다.
과거에는 브라우저가 이러한 모든 기능을 싱글 프로세스로 처리하는 경우가 많았다. 이 방식은 열린 각 탭마다 오버헤드가 적다는 장점이 있지만, 한 웹사이트가 충돌하거나 멈출 경우 전체 브라우저에 영향을 미친다는 단점이 있다.
멀티 프로세스 모델
이 문제를 해결하기 위해 Chrome 팀은 각 탭이 별도의 프로세스에서 렌더링되도록 결정했다. 이렇게 하면 웹 페이지의 버그나 악성 코드가 전체 애플리케이션에 미치는 피해를 제한할 수 있다. 단일 브라우저 프로세스가 이러한 프로세스들을 제어하며, 전체 애플리케이션의 라이프사이클도 관리한다. 아래 Chrome Comic에서 발췌한 다이어그램은 이 모델을 시각적으로 보여준다:
Electron 애플리케이션도 매우 유사한 구조를 가진다. 앱 개발자는 두 가지 타입의 프로세스를 관리한다: 메인 프로세스와 렌더러 프로세스. 이들은 위에서 설명한 Chrome의 브라우저 프로세스와 렌더러 프로세스와 유사하다.
메인 프로세스
각 Electron 앱은 단일 메인 프로세스를 가지고 있으며, 이는 애플리케이션의 진입점 역할을 한다. 메인 프로세스는 Node.js 환경에서 실행되기 때문에, require
를 통해 모듈을 불러올 수 있고 Node.js의 모든 API를 사용할 수 있다.
윈도우 관리
메인 프로세스의 주요 목적은 BrowserWindow
모듈을 사용해 애플리케이션 윈도우를 생성하고 관리하는 것이다.
BrowserWindow
클래스의 각 인스턴스는 별도의 렌더러 프로세스에서 웹 페이지를 로드하는 애플리케이션 윈도우를 생성한다. 윈도우의 webContents
객체를 통해 메인 프로세스에서 이 웹 콘텐츠와 상호작용할 수 있다.
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를 사용해 더 네이티브한 애플리케이션 윈도우 경험을 제공한다.
// 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
)을 사용해야 한다.
개발 편의를 위해 렌더러 프로세스를 완전한 Node.js 환경으로 실행할 수 있다. 과거에는 이 기능이 기본적으로 활성화되어 있었지만, 보안상의 이유로 비활성화되었다.
이 시점에서 여러분은 렌더러 프로세스의 사용자 인터페이스가 어떻게 Node.js 및 Electron의 네이티브 데스크톱 기능과 상호작용할 수 있는지 궁금할 수 있다. 특히 이러한 기능이 메인 프로세스에서만 접근 가능하다면 말이다. 사실, Electron의 콘텐츠 스크립트를 직접 가져오는 방법은 없다.
사전 로드 스크립트
사전 로드 스크립트는 렌더러 프로세스에서 웹 콘텐츠가 로드되기 전에 실행되는 코드를 포함한다. 이 스크립트는 렌더러 컨텍스트 내에서 실행되지만, Node.js API에 접근할 수 있는 더 많은 권한을 가진다.
사전 로드 스크립트는 BrowserWindow
생성자의 webPreferences
옵션을 통해 메인 프로세스에 연결할 수 있다.
const { BrowserWindow } = require('electron')
// ...
const win = new BrowserWindow({
webPreferences: {
preload: 'path/to/preload.js'
}
})
// ...
사전 로드 스크립트는 렌더러와 동일한 전역 Window
인터페이스를 공유하며 Node.js API에 접근할 수 있기 때문에, window
전역에 임의의 API를 노출시켜 웹 콘텐츠가 이를 사용할 수 있도록 하는 역할을 한다.
하지만 사전 로드 스크립트가 렌더러와 동일한 window
전역을 공유하더라도, contextIsolation
이 기본적으로 활성화되어 있기 때문에 사전 로드 스크립트의 변수를 직접 window
에 연결할 수 없다.
window.myAPI = {
desktop: true
}
console.log(window.myAPI)
// => undefined
컨텍스트 격리(Context Isolation)는 사전 로드 스크립트가 렌더러의 메인 세계와 분리되어 있어, 권한이 있는 API가 웹 콘텐츠의 코드로 유출되는 것을 방지한다.
대신, contextBridge
모듈을 사용해 이를 안전하게 수행할 수 있다:
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
desktop: true
})
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')