Skip to main content

첫 번째 앱 만들기

튜토리얼 따라하기

이것은 Electron 튜토리얼의 2편입니다.

  1. [필수 조건][prerequisites]
  2. [첫 번째 앱 만들기][building your first app]
  3. [프리로드 스크립트 사용하기][preload]
  4. [기능 추가하기][features]
  5. [애플리케이션 패키징][packaging]
  6. [배포 및 업데이트][updates]

학습 목표

이번 튜토리얼에서는 여러분이 Electron 프로젝트를 설정하고, 기본적인 시작 애플리케이션을 작성하는 방법을 배운다. 이 섹션을 마치면, 터미널에서 개발 모드로 동작하는 Electron 앱을 실행할 수 있게 된다.

프로젝트 설정하기

WSL 사용 자제

윈도우 환경에서 이 튜토리얼을 진행할 때는 [Windows Subsystem for Linux][wsl] (WSL)을 사용하지 않는 것이 좋다. WSL을 사용하면 애플리케이션 실행 시 문제가 발생할 수 있다.

npm 프로젝트 초기화

Electron 앱은 npm을 사용해 스캐폴딩하며, package.json 파일이 진입점 역할을 한다. 먼저 폴더를 생성하고 그 안에서 npm init 명령어로 npm 패키지를 초기화한다.

mkdir my-electron-app && cd my-electron-app
npm init

이 명령어는 package.json의 몇 가지 필드를 설정하도록 요청한다. 이 튜토리얼을 위해 몇 가지 규칙을 따라야 한다:

  • **진입점(entry point)**은 main.js로 설정한다 (이 파일은 곧 생성할 것이다).
  • 작성자(author), 라이선스(license), **설명(description)**은 어떤 값이든 상관없지만, 나중에 [패키징][packaging]할 때 필요하다.

그런 다음, Electron을 앱의 devDependencies에 설치한다. devDependencies는 개발 전용 외부 패키지 의존성 목록으로, 프로덕션 환경에서는 필요하지 않다.

왜 Electron을 devDependency로 설치할까?

프로덕션 코드에서 Electron API를 실행하는데도 이렇게 하는 것이 직관적이지 않게 보일 수 있다. 하지만 패키징된 앱은 Electron 바이너리와 함께 번들로 제공되므로, 프로덕션 의존성으로 지정할 필요가 없다.

npm install electron --save-dev

패키지를 초기화하고 Electron을 설치한 후 package.json 파일은 다음과 같이 보일 것이다. 또한 이제 Electron 실행 파일이 포함된 node_modules 폴더와, 설치할 정확한 의존성 버전을 지정하는 package-lock.json 파일이 생성되었을 것이다.

package.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 설치 단계

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 파일을 생성하고 다음 한 줄의 코드를 추가한다:

main.js
console.log('Hello from Electron 👋')

Electron의 메인 프로세스는 Node.js 런타임이므로, electron 명령어를 사용해 임의의 Node.js 코드를 실행할 수 있다([REPL][]로도 사용 가능). 이 스크립트를 실행하려면 package.json의 [scripts][package-scripts] 필드에 start 명령어로 electron .을 추가한다. 이 명령어는 Electron 실행 파일에게 현재 디렉토리에서 메인 스크립트를 찾아 개발 모드로 실행하라고 지시한다.

package.json
{
"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 run start

터미널에 Hello from Electron 👋이 출력될 것이다. 축하한다! 첫 번째 Electron 코드를 성공적으로 실행했다. 다음으로, HTML을 사용해 사용자 인터페이스를 만들고 이를 네이티브 윈도우에 로드하는 방법을 배울 것이다.

브라우저 윈도우에 웹 페이지 로드하기

Electron에서 각 윈도우는 로컬 HTML 파일이나 원격 웹 주소에서 로드된 웹 페이지를 표시한다. 이 예제에서는 로컬 파일을 로드한다. 프로젝트의 루트 폴더에 index.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 파일의 내용을 다음 코드로 대체한다. 각 하이라이트된 블록을 하나씩 설명할 것이다.

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()
})

모듈 임포트

main.js (Line 1)
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 문서를 참고한다.

Electron에서의 ES 모듈

ECMAScript 모듈 (즉, import를 사용해 모듈을 로드하는 방식)은 Electron 28부터 지원된다. Electron에서 ES 모듈의 상태와 이를 애플리케이션에서 사용하는 방법에 대한 자세한 내용은 ESM 가이드에서 확인할 수 있다.

재사용 가능한 윈도우 생성 함수 작성하기

createWindow() 함수는 웹 페이지를 새로운 BrowserWindow 인스턴스에 로드한다:

main.js (Lines 3-10)
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600
})

win.loadFile('index.html')
}

앱 준비 시점에 함수 호출하기

main.js (Lines 12-14)
app.whenReady().then(() => {
createWindow()
})

Electron의 핵심 모듈 대부분은 Node.js의 비동기 이벤트 기반 아키텍처를 따르는 [이벤트 에미터][event emitters]다. app 모듈도 이러한 에미터 중 하나이다.

Electron에서 BrowserWindowapp 모듈의 [ready][app-ready] 이벤트가 발생한 후에만 생성할 수 있다. 이 이벤트를 기다리기 위해 [app.whenReady()][app-when-ready] API를 사용하고, Promise가 이행되면 createWindow()를 호출한다.

info

일반적으로 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()
})
})

최종 시작 코드

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()
}
})

선택 사항: VS Code에서 디버깅하기

애플리케이션을 VS Code로 디버깅하려면, VS Code를 메인 프로세스와 렌더러 프로세스에 모두 연결해야 한다. 다음은 실행할 수 있는 샘플 설정이다. 프로젝트 내에 .vscode 폴더를 만들고 launch.json 설정 파일을 생성한다:

.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]이다.
caution

Renderer에서 프로세스에 연결할 때, 디버거가 연결되기 전에 코드의 첫 몇 줄이 실행될 수 있다. 이 문제를 해결하려면 페이지를 새로고침하거나 개발 모드에서 코드 실행 전에 타임아웃을 설정할 수 있다.

추가 자료

디버깅에 대해 더 깊이 알고 싶다면 다음 가이드를 참고한다:

  • [애플리케이션 디버깅][Application Debugging]
  • [DevTools 확장][devtools extension]

요약

Electron 애플리케이션은 npm 패키지를 통해 설정한다. Electron 실행 파일은 프로젝트의 devDependencies에 설치해야 하며, package.json 파일의 스크립트를 통해 개발 모드로 실행할 수 있다.

실행 파일은 package.json의 main 속성에 지정된 JavaScript 진입점을 실행한다. 이 파일은 Electron의 메인 프로세스를 제어하며, Node.js 인스턴스를 실행하고 애플리케이션의 라이프사이클, 네이티브 인터페이스 표시, 권한이 필요한 작업 수행, 그리고 렌더러 프로세스 관리를 담당한다.

렌더러 프로세스는 그래픽 콘텐츠를 표시하는 역할을 한다. 웹 주소나 로컬 HTML 파일을 지정해 렌더러에 웹 페이지를 로드할 수 있다. 렌더러는 일반 웹 페이지와 매우 유사하게 동작하며 동일한 웹 API에 접근할 수 있다.

다음 섹션에서는 렌더러 프로세스에 권한이 필요한 API를 추가하는 방법과 프로세스 간 통신을 구현하는 방법을 배울 것이다.