이번 글은 사실 이전에 작성되었던 글을 다시 더 깔끔하게 바꿔서 작성한 글입니다. 그럼에도 불구하고 아직 여러가지 문제점이 많이 남아있을 수 있습니다. 그래서 오늘은 React.JS와 Electron.JS를 어떻게 해야 같이 잘 사용할 수 있는지 설명합니다.


계획하기

기본적인 애플리케이션 구조는 React.JS 앱이 가져갑니다. 빠르고 쉬운 빌드를 위해서 Create-React-App을 사용할 예정입니다. 본문에서는 제가 개인적으로 사용하는 Yarn 패키지 매니저가 기본적으로 사용될 예정입니다.

  1. Create-React-App으로 새 React.JS 앱 만들기
  2. Electron.JS 설치하기
  3. Electron-Builder 설정하기

Electron-Builder에는 기본적으로 CRA 빌드 프리셋이 있으니 그에 맞춰서 몇 가지 프로젝트 설정을 변경하는 것까지가 이번 글에서 다룰 내용입니다.

Create React App!

JavaScript 패키지 네이밍 컨벤션에 맞게 새로운 프로젝트 폴더 혹은 Git 저장소를 생성해주세요. Create-React-App을 사용하여 프로젝트를 시작할 것이기 때문에 빈 폴더가 필요합니다.

이제 Create-React-App을 실행하여 새로운 React.JS 프로젝트를 만들어줍니다. 위와 같이 경로란에 cmd를 적어주면 빠르게 명령 프롬프트를 열 수 있습니다. 그리고 yarn create react-app . 명령을 사용하여 현재 디렉터리에 새 프로젝트를 만들어줍니다.

yarn create react-app .

개발을 위한 패키지 추가하기

이제 몇 가지를 여기에 더해주어야 합니다. 그 몇 가지는...

  • cross-env: 실행하는 OS에 상관없이 환경 변수 지정을 가능하게 해줍니다.
  • electron-is-dev: 현재 Electron.JS 앱이 개발 환경에서 실행하는지 알아봅니다.
  • concurrently: 동시에 여러 프로세스를 실행하게 해줍니다.
  • wait-on: 특정 시그널을 기다리게 만들어줍니다.
  • electron: 여러분이 그토록 원하던 Electron.JS입니다.

코드에서 쓰일 electron-is-dev를 제외한 모든 패키지는 Dev dependency로 설치되게 하여 깔끔한 구성을 유지하세요. Dev dependency는 말 그대로 개발에만 쓰이는 패키지입니다. Electron.JS의 경우 바이너리 다운로드를 위해 설치 속도가 조금 더 걸릴 수 있으니 중간에 멈췄다고 생각하여 처음부터 다시시작하는 경우는 없도록 하세요. 정말 심심하다면 작업 관리자를 열고 네트워크 다운로드 상태를 보아서 작동하는 것을 확인할 수도 있습니다.

yarn add -D cross-env concurrently wait-on electron

그리고 electron-is-dev 패키지를 설치해줍니다.

yarn add electron-is-dev

+ LICENSE 넣어주기

이제 여기에 라이선스 파일을 만들어줄 예정입니다. 아래 패키지를 사용하면 손쉽게 새로운 저장소에도 LICENSE 파일을 만들 수 있습니다. 이 패키지는 현재 프로젝트와 직접적인 연관이 없고 내부에서 쓰일 일도 없으니 전역으로 설치합니다.

  • license
yarn global add license

LICENSE 만들기

MIT 라이선스를 기준으로 하겠습니다. 아래 명령어를 사용해주시면 됩니다. 하나 주의할 점이 있다면 뒤 인자에는 대문자가 들어가야 하더라고요. 인자가 없으면 선택하는 CLI를 보실 수 있습니다.

license MIT

프로젝트 설정하기

Electron.JS와 React.JS를 동시에 개발을 하게 되면 React.JS 코드를 Electron 앱에서 열게 됩니다. 이 때 개발 서버를 열고 Electron 앱은 개발 서버의 주소인 localhosot:3000을 로드하게 하는 것이 목표입니다. 여기에서 저는 프론트엔드를 interface라고 부를 예정입니다.

이제 에디터를 열고 package.json 파일을 열어주세요.

Webpack 개발 서버'만' 실행하게 만들기

저희가 개발을 할 때는 Electron 앱을 지켜보게 될테니 먼저 브라우저없이 Webpack 개발 서버를 시작할 수 있도록 cross-env 패키지를 사용하여 BROWSER 환경 변수를 none으로 설정합니다.

"scripts": {
	...
    "start-interface": "cross-env BROWSER=none npm run start"
}

Electron 앱이 시작되도록 하기

이제 Electron 앱만 시작될 수 있도록 간단한 스크립트를 하나 더 추가해줍니다. 아래 명령은 Electron 패키지를 위에서 이미 dev dependency로 설치했으니 정상적으로 동작해야 합니다.

"scripts": {
	...
    "start-electron": "electron ."
}

Electron 앱이 Webpack 개발 서버 다음에 시작되도록 하기

개발 서버가 시작되지 않은 상태로 Electron 앱이 시작되어 localhost:3000 페이지를 로드하게 되면 에러 페이지가 기본적으로 표시됩니다. Webpack 개발 서버를 시작하고 다시 페이지를 새로고쳐주면 당연히 작동하겠지만 이러한 일은 매우 거슬릴게 뻔하므로 미연에 방지해야 합니다. 아래와 같이 wait-on 패키지를 활용할 수 있습니다.

"scripts": {
	...
    "start-electron-after-interface": "wait-on http://localhost:3000 && npm run start-electron"
}

Webpack 개발 서버와 Electron 앱을 동시에 시작하기

마지막으로 이제 이 2개를 concurrently 패키지를 사용하여 동시에 시작되게 만들면 됩니다. 저는 여기에서 최대한 자연스럽고 겹치지 않는 키워드라고 생각하는 dev로 스크립트를 만들었습니다. concurrently 패키지는 npm:<scriptName> 키워드를 사용하여 즉시 이 패키지의 다른 스크립트를 실행할 수 있도록 만들어졌습니다. 그렇기 때문에 npm:start-interfacenpm run start-interface와 같은 의미를 가집니다. 앞의 -n 플래그와 같은 경우는 name이라는 것을 자연스럽게 눈치채실 수 있으실 겁니다.

"scripts": {
	...
    "dev": "concurrently -n interface,app 'npm:start-interface' 'npm:start-electron-after-interface'"
}

Electron 앱 엔트리 포인트 설정하기

React.JS의 경우에는 src 디렉터리로 정해져 있지만 Electron의 경우에는 package.jsonmain 값으로 엔트리 포인트를 결정하게 됩니다. 여기에서 2개의 파일을 만들어줍니다.

  • $project_root/public/electron.js
  • $project_root/app/index.js

여기에서 2개의 파일을 생성한 이유는 하나입니다. Electron-Builder의 Create React App 프리셋에서 엔트리 포인트를 public/electron.js로 강제하고 있기 때문입니다. 이는 위와 같이 단순히 require 문으로 해결해줄 수 있습니다.  Node.JS에서는 기본적으로 index를 키워드로, jsnode 확장자 또한 기본값으로 삼고 있기 때문에 ../app 이후에 더 이상 써줄 필요가 없습니다.

이제 app/index.js 파일에 기본적인 앱 실행을 위한 내용을 붙여넣어주시면 됩니다.

const {
  app,
  BrowserWindow
} = require('electron')
const path = require('path')
const isDev = require('electron-is-dev')

let mainWindow

const createWindow = () => {
  mainWindow = new BrowserWindow({
    width: 350,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  })
  mainWindow.loadURL(isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`)

  if (isDev) {
    // Open the DevTools.
    // BrowserWindow.addDevToolsExtension('<location to your react chrome extension>')
    // mainWindow.webContents.openDevTools()
  }
  mainWindow.on('closed', () => {
    mainWindow = null
  })
}

app.on('ready', createWindow)
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})
app.on('activate', () => {
  if (mainWindow === null) {
    createWindow()
  }
})

코드를 보면 24번 째 줄에서 Webpack 개발 서버를 로드할 지 혹은 빌드된 Static한 React 앱을 로드할 지 결정하는 코드를 볼 수 있습니다. 이제 마지막으로 package.json에 엔트리 포인트를 서술해주시면 되겠습니다.

"main": "app"

그리고 yarn dev 명령어로 실행해보면 아래와 같은 결과물을 볼 수 있습니다.

빌드하기

이제 Electron-Builder를 사용할 시간입니다. 설치부터 해야 겠죠? 아래 명령을 사용하여 Electron-Builder를 Dev dependency에 설치해주세요.

yarn add -D electron-builder

그 다음 package.json 파일을 열고 scripts 섹션에 라인을 하나 더 추가해줄겁니다. 뒤의 --dir 플래그는 선택적이니 붙여두시면 어느 폴더에 빌드를 할 지 선택할 수 있게 됩니다. 그리고 postinstall 스크립트는 여러분이 SQLite와 같은 패키지를 사용한다면 빌드가 확실히 되었는지 한 번 더 보장해주는 역할을 하게 됩니다.

"scripts": {
	...
	"dist": "npm run build && electron-builder --dir",
    "postinstall": "electron-builder install-app-deps"
}

Electron-Builder는 package.jsonbuild 섹션에 있는 설정을 가져와서 빌드하게 됩니다. 여기에 저희가 앱의 백엔드(Electron)를 구성할 때 사용한 app 폴더도 빌드할 때 포함시키기 위해서 몇 가지를 더 추가해야 해야 합니다. 저는 기본적으로 아래와 같이 구성하였습니다. build.files 배열에 사용될 에셋을 포함시켜주세요. app 디렉터리는 백엔드, build 디렉터리는 Create-React-App에서 빌드된 프론트엔드 결과물이 될 예정입니다.

"build": {
  "appId": "io.seia.<appName>",
  "copyright": "Copyright 2020 Seia-Soto. All rights reserved.",
  "npmRebuild": true,
  "files": [
    "app/**/*",
    "build/**/*",
    "node_modules/**/*"
  ]
}

React.JS 앱에서 로컬 에셋 불러오게 만들기

Create-React-App으로 만들어진 앱은 기본적으로 JavaScript와 CSS 같은 에셋을 로드할 때 package.jsonhomepage 값 혹은 정적으로 특정 경로를 참조하게 됩니다. 실제 앱이 실행될 때는 내부에 포함되어 있는 파일이 되므로 이것을 ./로 설정하여 로컬에서 정상적으로 불러오도록 만들어주어야 합니다. package.json 파일을 열고 한 가지 항목을 추가해주세요.

"homepage": "./"

그렇지 않으면 어떻게 되냐고요? 저도 알고 싶지 않았어요. 더욱 정확한 글을 위해 이전의 시행착오도 새로 쓸 때 따라했습니다. 네...

빌드된 파일들 gitignore로 제외시키기

마지막 단계입니다. 이제 빌드된 파일들은 저장소에서 제외시켜주는 작업을 할 겁니다. 총 2가지를 제외하시면 됩니다. 아래 2개 값은 모두 기본값들입니다. 여기에서 저희는 이 값들을 변경하지 않았습니다.

  • React.JS 빌드: build 폴더
  • Electron.JS 빌드: dist 폴더

.gitignore 파일을 열고 몇 줄 더 추가해줍니다.

# output

build
dist

쨘! 이제 성공적으로 빌드가 완료되는 것을 볼 수 있습니다. 그리고 실행도 잘 되고 있습니다.

이번 글은 여기까지입니다. 추가로 Electron 앱 개발을 어떻게 해야 하는지까지는 다루지 않고 있습니다. 인간적으로 너무 길지 않나요? 아무튼 이것도 충분히 할 게 많으니... 수고하셨습니다.