첫 번째 앱 만들기
이것은 Electron 튜토리얼의 2편입니다.
- [필수 조건][prerequisites]
- [첫 번째 앱 만들기][building your first app]
- [프리로드 스크립트 사용하기][preload]
- [기능 추가하기][features]
- [애플리케이션 패키징][packaging]
- [배포 및 업데이트][updates]
학습 목표
이번 튜토리얼에서는 여러분이 Electron 프로젝트를 설정하고, 기본적인 시작 애플리케이션을 작성하는 방법을 배운다. 이 섹션을 마치면, 터미널에서 개발 모드로 동작하는 Electron 앱을 실행할 수 있게 된다.
프로젝트 설정하기
윈도우 환경에서 이 튜토리얼을 진행할 때는 [Windows Subsystem for Linux][wsl] (WSL)을 사용하지 않는 것이 좋다. WSL을 사용하면 애플리케이션 실행 시 문제가 발생할 수 있다.
npm 프로젝트 초기화
Electron 앱은 npm을 사용해 스캐폴딩하며, package.json
파일이 진입점 역할을 한다. 먼저 폴더를 생성하고 그 안에서 npm init
명령어로 npm 패키지를 초기화한다.
- npm
- Yarn
mkdir my-electron-app && cd my-electron-app
npm init
mkdir my-electron-app && cd my-electron-app
yarn init
이 명령어는 package.json
의 몇 가지 필드를 설정하도록 요청한다. 이 튜토리얼을 위해 몇 가지 규칙을 따라야 한다:
- **진입점(entry point)**은
main.js
로 설정한다 (이 파일은 곧 생성할 것이다). - 작성자(author), 라이선스(license), **설명(description)**은 어떤 값이든 상관없지만, 나중에 [패키징][packaging]할 때 필요하다.
그런 다음, Electron을 앱의 devDependencies에 설치한다. devDependencies는 개발 전용 외부 패키지 의존성 목록으로, 프로덕션 환경에서는 필요하지 않다.
프로덕션 코드에서 Electron API를 실행하는데도 이렇게 하는 것이 직관적이지 않게 보일 수 있다. 하지만 패키징된 앱은 Electron 바이너리와 함께 번들로 제공되므로, 프로덕션 의존성으로 지정할 필요가 없다.
- npm
- Yarn
npm install electron --save-dev
yarn add electron --dev
패키지를 초기화하고 Electron을 설치한 후 package.json
파일은 다음과 같이 보일 것이다. 또한 이제 Electron 실행 파일이 포함된 node_modules
폴더와, 설치할 정확한 의존성 버전을 지정하는 package-lock.json
파일이 생성되었을 것이다.
{
"name": "my-electron-app",
"version": "1.0.0",
"description": "Hello World!",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Jane Doe",
"license": "MIT",
"devDependencies": {
"electron": "23.1.3"
}
}
Electron을 직접 설치하는 데 실패한다면, [고급 설치][installation] 문서를 참고해 다운로드 미러, 프록시, 문제 해결 단계에 대한 지침을 확인할 수 있다.
.gitignore 파일 추가하기
[.gitignore
][gitignore] 파일은 Git이 추적하지 않을 파일과 디렉토리를 지정한다. 프로젝트의 node_modules
폴더가 커밋되지 않도록 하려면 [GitHub의 Node.js gitignore 템플릿][gitignore-template]을 프로젝트의 루트 폴더에 복사해 넣어야 한다.
Electron 앱 실행하기
Electron의 멀티 프로세스가 어떻게 동작하는지 더 깊이 이해하려면 [Electron의 프로세스 모델][process-model] 문서를 읽어보세요.
package.json
에 정의한 [main
][package-json-main] 스크립트는 모든 Electron 애플리케이션의 시작점이다. 이 스크립트는 메인 프로세스를 제어하며, Node.js 환경에서 실행된다. 메인 프로세스는 앱의 생명주기 관리, 네이티브 인터페이스 표시, 권한이 필요한 작업 수행, 그리고 렌더러 프로세스 관리(이에 대해서는 나중에 자세히 설명)를 담당한다.
첫 번째 Electron 앱을 만들기 전에, 메인 프로세스 시작점이 올바르게 설정되었는지 확인하기 위해 간단한 스크립트를 사용한다. 프로젝트 루트 폴더에 main.js
파일을 생성하고 다음 한 줄의 코드를 추가한다:
console.log('Hello from Electron 👋')
Electron의 메인 프로세스는 Node.js 런타임이므로, electron
명령어를 사용해 임의의 Node.js 코드를 실행할 수 있다([REPL][]로도 사용 가능). 이 스크립트를 실행하려면 package.json
의 [scripts
][package-scripts] 필드에 start
명령어로 electron .
을 추가한다. 이 명령어는 Electron 실행 파일에게 현재 디렉토리에서 메인 스크립트를 찾아 개발 모드로 실행하라고 지시한다.
{
"name": "my-electron-app",
"version": "1.0.0",
"description": "Hello World!",
"main": "main.js",
"scripts": {
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Jane Doe",
"license": "MIT",
"devDependencies": {
"electron": "23.1.3"
}
}
- npm
- Yarn
npm run start
yarn run start
터미널에 Hello from Electron 👋
이 출력될 것이다. 축하한다! 첫 번째 Electron 코드를 성공적으로 실행했다. 다음으로, HTML을 사용해 사용자 인터페이스를 만들고 이를 네이티브 윈도우에 로드하는 방법을 배울 것이다.
브라우저 윈도우에 웹 페이지 로드하기
Electron에서 각 윈도우는 로컬 HTML 파일이나 원격 웹 주소에서 로드된 웹 페이지를 표시한다. 이 예제에서는 로컬 파일을 로드한다. 프로젝트의 루트 폴더에 index.html
파일을 만들고 기본적인 웹 페이지를 작성한다:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>Hello from Electron renderer!</title>
</head>
<body>
<h1>Hello from Electron renderer!</h1>
<p>👋</p>
</body>
</html>
이제 웹 페이지가 준비되었으니, 이를 Electron의 [BrowserWindow][browser-window]에 로드할 수 있다. main.js
파일의 내용을 다음 코드로 대체한다. 각 하이라이트된 블록을 하나씩 설명할 것이다.
const { app, BrowserWindow } = require('electron')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
})
모듈 임포트
const { app, BrowserWindow } = require('electron')
첫 번째 줄에서는 CommonJS 모듈 문법을 사용해 두 개의 Electron 모듈을 임포트한다:
- [app][app]: 애플리케이션의 이벤트 생명주기를 제어하는 모듈
- [BrowserWindow][browser-window]: 애플리케이션 윈도우를 생성하고 관리하는 모듈
모듈 대소문자 규칙
app과 BrowserWindow 모듈의 대소문자 차이를 눈치챘을 것이다. Electron은 일반적인 자바스크립트 규칙을 따르는데, PascalCase 모듈은 인스턴스화 가능한 클래스 생성자(예: BrowserWindow, Tray, Notification)이고, camelCase 모듈은 인스턴스화할 수 없는 모듈(예: app, ipcRenderer, webContents)이다.
타입 임포트 별칭
TypeScript 코드를 작성할 때 더 나은 타입 체크를 위해 electron/main
에서 메인 프로세스 모듈을 임포트할 수 있다.
const { app, BrowserWindow } = require('electron/main')
자세한 내용은 Process Model 문서를 참고한다.
ECMAScript 모듈 (즉, import
를 사용해 모듈을 로드하는 방식)은 Electron 28부터 지원된다. Electron에서 ES 모듈의 상태와 이를 애플리케이션에서 사용하는 방법에 대한 자세한 내용은 ESM 가이드에서 확인할 수 있다.
재사용 가능한 윈도우 생성 함수 작성하기
createWindow()
함수는 웹 페이지를 새로운 BrowserWindow 인스턴스에 로드한다:
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadFile('index.html')
}
앱 준비 시점에 함수 호출하기
app.whenReady().then(() => {
createWindow()
})
Electron의 핵심 모듈 대부분은 Node.js의 비동기 이벤트 기반 아키텍처를 따르는 [이벤트 에미터][event emitters]다. app
모듈도 이러한 에미터 중 하나이다.
Electron에서 BrowserWindow
는 app
모듈의 [ready
][app-ready] 이벤트가 발생한 후에만 생성할 수 있다. 이 이벤트를 기다리기 위해 [app.whenReady()
][app-when-ready] API를 사용하고, Promise가 이행되면 createWindow()
를 호출한다.
일반적으로 Node.js 이벤트는 에미터의 .on
함수를 사용해 리스닝한다.
+ app.on('ready', () => {
- app.whenReady().then(() => {
createWindow()
})
하지만 Electron은 ready
이벤트를 직접 리스닝할 때 발생할 수 있는 미묘한 문제를 피하기 위해 app.whenReady()
라는 헬퍼를 제공한다. 자세한 내용은 electron/electron#21972를 참고한다.
이 시점에서 Electron 애플리케이션의 start
커맨드를 실행하면 웹 페이지를 표시하는 윈도우가 성공적으로 열린다!
앱이 윈도우에 표시하는 각 웹 페이지는 별도의 프로세스에서 실행된다. 이 프로세스를 렌더러 프로세스(또는 간단히 렌더러)라고 부른다. 렌더러 프로세스는 일반적인 프론트엔드 웹 개발에서 사용하는 JavaScript API와 도구에 접근할 수 있다. 예를 들어 [webpack][]으로 코드를 번들링하고 최소화하거나 [React][react]로 사용자 인터페이스를 구축할 수 있다.
앱 윈도우 생명주기 관리
애플리케이션 윈도우는 운영체제마다 다르게 동작한다. Electron은 기본적으로 이러한 관례를 강제하지 않고, 개발자가 원할 경우 앱 코드에서 직접 구현할 수 있도록 선택권을 제공한다. app과 BrowserWindow 모듈에서 발생하는 이벤트를 구독함으로써 기본적인 윈도우 관례를 구현할 수 있다.
Node의 [process.platform
][node-platform] 변수를 사용하면 특정 플랫폼에서만 코드를 실행할 수 있다. Electron이 실행 가능한 플랫폼은 win32
(Windows), linux
(Linux), darwin
(macOS) 세 가지뿐이다.
모든 윈도우가 닫히면 앱 종료하기 (Windows & Linux)
Windows와 Linux에서는 일반적으로 모든 윈도우를 닫으면 앱이 완전히 종료된다. 이 패턴을 Electron 앱에 구현하려면, app 모듈의 [window-all-closed
][window-all-closed] 이벤트를 감지하고, 사용자가 macOS가 아닌 경우 [app.quit()
][app-quit]를 호출해 앱을 종료한다.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
macOS 앱은 일반적으로 윈도우가 열려 있지 않아도 계속 실행된다. 윈도우가 없는 상태에서 앱을 활성화하면 새로운 윈도우를 열어야 한다.
이 기능을 구현하려면 앱 모듈의 [activate
][activate] 이벤트를 감지하고, 열려 있는 BrowserWindow가 없을 때 기존의 createWindow()
메서드를 호출한다.
윈도우는 ready
이벤트 전에 생성할 수 없으므로, 앱이 초기화된 후에만 activate
이벤트를 감지해야 한다. 이를 위해 기존의 whenReady()
콜백 내부에서만 activate 이벤트를 감지한다.
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
최종 시작 코드
- main.js
- index.html
const { app, BrowserWindow } = require('electron/main')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>Hello from Electron renderer!</title>
</head>
<body>
<h1>Hello from Electron renderer!</h1>
<p>👋</p>
<p id="info"></p>
</body>
<script src="./renderer.js"></script>
</html>
선택 사항: VS Code에서 디버깅하기
애플리케이션을 VS Code로 디버깅하려면, VS Code를 메인 프로세스와 렌더러 프로세스에 모두 연결해야 한다. 다음은 실행할 수 있는 샘플 설정이다. 프로젝트 내에 .vscode
폴더를 만들고 launch.json
설정 파일을 생성한다:
{
"version": "0.2.0",
"compounds": [
{
"name": "Main + renderer",
"configurations": ["Main", "Renderer"],
"stopAll": true
}
],
"configurations": [
{
"name": "Renderer",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}"
},
{
"name": "Main",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"args": [".", "--remote-debugging-port=9222"],
"outputCapture": "std",
"console": "integratedTerminal"
}
]
}
"Run and Debug"를 선택하면 "Main + renderer" 옵션이 나타난다. 이 옵션을 통해 메인 프로세스와 렌더러 프로세스 모두에 브레이크포인트를 설정하고 변수를 검사할 수 있다.
launch.json
파일에서는 세 가지 설정을 만들었다:
Main
은 메인 프로세스를 시작하고, 원격 디버깅을 위해 9222 포트를 노출한다 (--remote-debugging-port=9222
). 이 포트는Renderer
디버거를 연결하는 데 사용한다. 메인 프로세스는 Node.js 프로세스이므로 타입을node
로 설정한다.Renderer
는 렌더러 프로세스를 디버깅한다. 메인 프로세스가 렌더러 프로세스를 생성하기 때문에, 새로운 프로세스를 만드는 대신 기존 프로세스에 "연결"한다 ("request": "attach"
). 렌더러 프로세스는 웹 프로세스이므로 디버거로chrome
을 사용한다.Main + renderer
는 앞의 두 설정을 동시에 실행하는 [복합 작업][compound task]이다.
Renderer
에서 프로세스에 연결할 때, 디버거가 연결되기 전에 코드의 첫 몇 줄이 실행될 수 있다. 이 문제를 해결하려면 페이지를 새로고침하거나 개발 모드에서 코드 실행 전에 타임아웃을 설정할 수 있다.
디버깅에 대해 더 깊이 알고 싶다면 다음 가이드를 참고한다:
- [애플리케이션 디버깅][Application Debugging]
- [DevTools 확장][devtools extension]
요약
Electron 애플리케이션은 npm 패키지를 통해 설정한다. Electron 실행 파일은 프로젝트의 devDependencies
에 설치해야 하며, package.json 파일의 스크립트를 통해 개발 모드로 실행할 수 있다.
실행 파일은 package.json의 main
속성에 지정된 JavaScript 진입점을 실행한다. 이 파일은 Electron의 메인 프로세스를 제어하며, Node.js 인스턴스를 실행하고 애플리케이션의 라이프사이클, 네이티브 인터페이스 표시, 권한이 필요한 작업 수행, 그리고 렌더러 프로세스 관리를 담당한다.
렌더러 프로세스는 그래픽 콘텐츠를 표시하는 역할을 한다. 웹 주소나 로컬 HTML 파일을 지정해 렌더러에 웹 페이지를 로드할 수 있다. 렌더러는 일반 웹 페이지와 매우 유사하게 동작하며 동일한 웹 API에 접근할 수 있다.
다음 섹션에서는 렌더러 프로세스에 권한이 필요한 API를 추가하는 방법과 프로세스 간 통신을 구현하는 방법을 배울 것이다.