돌아온 앱 만들기 시리즈, 그 두 번째!

Hot reload!


링크

① 데스크탑 앱 만들기 #1 - Vite2 + Vue3 + Electron

② 데스크탑 앱 만들기 #2 - Hot reload

③ 데스크탑 앱 만들기 #3 - ipcMain, ipcRenderer(예정)

 

 자, 데크스탑 앱 만들기 두 번째 Hot reload 기능 구현하기입니다. Electron도 Web처럼 프론트와 백엔드로 나누어져 있습니다. 백엔드는 main.js(또는 background.js)에 해당하고, 프론트는 src안에 있는 파일들입니다.

 

 백엔드 코드가 바뀔 때는 앱 자체가 꺼졌다가 다시 실행되고, 프론트 코드가 바뀔때는 바로바로 적용될 수 있게 만들어볼게요.

 


 먼저 백엔드입니다.

 

1. 백엔드 리로드

 백엔드 리로드 기능을 구현하기 위해 라이브러리가 여러 가지 있지만 저는 2021년 11월 11일 현재 2-알파 버전인 electron-reload를 쓰도록 하겠습니다.

npm install --save-dev electron-reload
//or
yarn add -D electron-reload

 설치 이후 백엔드에 해당하는 main.js파일을 엽니다

 

/main.js

const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')

// 이부분을 넣어주세요!!
require('electron-reload')(__dirname, {
  electron: path.join(__dirname, 'node_modules', '.bin', 'electron'),
})
// 여기까지!!

function createWindow() {
  // ... 코드생략

 이제 electron-reload 라이브러리가 electron이 자리 잡고 있는 저 경로를 바라보고 변경이 일어날 때마다 앱을 껐다가 다시 켜줍니다.

 

 백엔드는 끝났습니다. 간단하죠?

 


2. 프론트 핫 리로드

 백엔드는 껐다 켜주는 수준이면 프론트는 확실히 핫하게 리로드해줍니다. vite를 쓰는 이유가 바로 이거지 않겠습니까? 그럼 설정 들어갑니다.

 

 먼저 기본적인 로직은 이렇습니다. 현재는 npm run start(또는 yarn start)를 하면 프론트 코드들이 build 되어서 /dist폴더에 묶어져서 들어갑니다. 그래서 /src에 있는 파일을 수정을 해도 변경이 되었는지 Electron이 알아챌 방법이 없죠.

/main.js

function createWindow() {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 1000,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },
  });

  // and load the index.html of the app.
  // mainWindow.loadFile("./dist/index.html");
  // 여기부터
  if (process.env.NODE_ENV === 'production') {
    mainWindow.loadFile('./dist/index.html')
  } else {
    mainWindow.loadURL('http://localhost:3000')
  }
  // 여기까지 코드 삽입!

  // Open the DevTools.
  // mainWindow.webContents.openDevTools()
}

 여기서 NODE_ENV 환경변수를 이용합니다. 개발환경에서는 Electron이 build되어 있는 파일을 바라보는 게 아니라 vite가 실행되고 있는 URL을 바라보게 해서 수정될 때 적용되게 만드는 겁니다.

 

/vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
const path = require("path");

// https://vitejs.dev/config/
export default defineConfig({
  base: process.env.NODE_ENV === "production" ? path.resolve(__dirname, "dist") : "", // <-- 얘 수정해주세요.
  plugins: [vue()],
});

 여기서는 development 환경에서는 아이들이 asset들이 build 된 파일을 바라보지 않아야하니까!

 

 자, 이제 스크립트를 바꿉니다.

/package.json

// ...생략

"scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview",
    "start": "vite && electron ." // <-- 이부분 vite && 추가
},
  
// ... 생략

 

 그리고 실행해보시면.

vite v2.6.7 dev server running at:

> Local: http://localhost:3000/
> Network: use `--host` to expose

ready in 467ms.

 이렇게 뜨고 일렉트론은 실행조차 되지 않습니다. 그렇다고 start 스크립트에서 "vite"를 빼면 아래 에러가 뿜뿜하죠.

 

스크립트 에러

 에러의 이유는 일렉트론이 실행되면서 localhost:3000를 읽어오지 못하기 때문입니다. localhost:3000이 만들어지기 전에 electron이 먼저 실행되면서 읽어올 수 없다고 찡찡대는 거죠.

 

자, 그럼 방법이 보입니다.

1) vite를 실행시킨다.(개발환경에서만 쓸 거니까 NODE_ENV도 development로 설정해준다.)

2) localhost:3000을 기다린 이후에 일렉트론을 실행한다.

 

 이 두 개의 명령이 따로 실행되면 안 되니 같이 실행될 수 있게 해주는 뭔가가 또 필요하겠죠.

 

 그럼 이제 세 개의 라이브러리가 필요해집니다

cross-env: 윈도우, 맥, 리눅스 환경에 구애받지 않고 환경변수를 설정할 수 있게 해주는 애

wait-on: port가 열릴 때까지 기다릴 수 있게 만들어주는 애

concurrently: 1)과 2)가 동시에 실행될 수 있게 만들어주는 애

 

그럼 설치!

npm install --save-dev concurrently wait-on cross-env
//or
yarn add -D concurrently wait-on cross-env

 설치가 완료되면 스크립트 파일을 아래처럼 만들어주세요./package.json

// ... 위에 생략

"scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview",
    "start": "concurrently -k \"cross-env NODE_ENV=development && vite\" \"wait-on tcp:3000 && electron .\"" // <-- 여기요 여기
},

// ... 아래 생략

 

그리고 npm run start(또는 yarn start)를 실행해봅니다.

제발 실행되었기를 빕니다.

 

 잘 실행이 되었다면 소스코드를 변경해봅니다.

/src/App.vue

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld msg="Hello Vue 3 + Vite!!" /> // <-- 이부분에 느낌표 두개 추가해볼게요.
</template>

편안


 마음이 편안해지는군요. 이제 백엔드와 프론트 둘 다 소스코드가 변경되면 알아서 앱이 껐다 켜지거나, 화면이 리로드 됩니다. 한번 체험해보세요 속도를. 감동입니다.

 

 이제 여러분도 그 감동과 함께 하시기를! 그리고 다음 포스팅은 2021 버전 데스크탑 앱 만들기 마지막입니다. preload.js를 활용해서 ipc 통신하는 방법을 올려보겠습니다. 굿바이!

 


2021.11.12 오류 발생!

NODE_ENV=development 환경에서 localhost:3000이 실행된 이후에 electron이 안뜬다면 start 스크립에서 wait-on tcp:3000을 빼보세요! 그러니 되더군요!

 

 

한 글자 바꿀 때마다 새로고침 하고 싶어.. 너무 좋아..
돌아온 앱 만들기 시리즈

 

바이트가 아니라 빗! 이라고 강조하더군요


링크

① 데스크탑 앱 만들기 #1 - Vite2 + Vue3 + Electron

② 데스크탑 앱 만들기 #2 - Hot reload

③ 데스크탑 앱 만들기 #3 - ipcMain, ipcRenderer(예정)

 

 데스크탑 앱 만들기 시리즈를 작년 9월에 시작하고 벌써 1년이 넘게 지났네요. 그동안 많은 기술이 나왔고 일렉트론도 엄청난 버전업을 했습니다. 2021년 10월 13일 오전 1시 30분 현재 15.1.2 버전이군요. 작년 이맘때가 10.1.0 버전이었는데.(얘네는 왜 이렇게 빨리 버전을 올리는지 모르겠습니다)

 

 그리고 바로 몇 시간 전 11시에 Nuxt3가 또 발표됐습니다. 두근대는 마음으로 만지작 거리다가 오랜만에 블로그를 업데이트해야겠다는 생각이 들어서 바로 시작합니다! 조만간  Nuxt에 대한 내용도 포스팅하고 싶군요 두근두근.

 

 아무튼 그 동안 Vue3도 나오고 Vite도 2 버전으로 업그레이드되었습니다. Vite를 처음 접했을 때의 그 놀라움을 아직도 잊지 못하고 있습니다. 항상 VueCLI로만 작업을 하던 저에게 새로고침 속도의 미학을 알려주었죠. (감동)

에반유. 당신은 정말..

그래서 여러분께도 그 감동을 전해드리고 싶었어요. 자 그럼 각설하고 바로 앱 만들기 시리즈 첫번째. 환경설정에 들어가 보도록 하겠습니다.


1. Vite 설치

npm init vite@latest
// or
yarn create vite

 뒤에 프로젝트 이름을 넣을 수 있는 데 그냥 바로 실행하면 아래 처럼 이름과 옵션을 고를 수 있어요.

~$ yarn create vite

yarn create v1.22.15
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Installed "create-vite@2.6.6" with binaries:
      - create-vite
      - cva
✔ Project name: … vite-electron-test
? Select a framework: › - Use arrow-keys. Return to submit.
    vanilla
❯   vue
    react
    preact
    lit
    svelte

타입스크립트는 vue-ts를 골라주면 되는데 일단 저는 vue 선택해서 Javascript로 진행합니다.

✔ Project name: … vite-electron-test
✔ Select a framework: › vue
? Select a variant: › - Use arrow-keys. Return to submit.
❯   vue
    vue-ts

그러면 완료. 들어가서 종속성을 설치하고 실행합니다.

Done. Now run:

  cd vite-electron-test
  yarn
  yarn dev

✨  Done in 262.96s.

그리고 yarn dev 또는 npm run dev 하면

짜장

라라벨 Watch나 VueCLI serve를 쓰다 보니 속도 하나에도 감동을 느낍니다. 물론 쟤네가 느린 건 아니지만..

localhost:3000만큼 사랑해

완성된 첫페이지입니다.

 

그리고 폴더구조는 아래와 같아요.

아무것도 없지요

아무것도 없어요. 이제부터 하나하나 설치해봅니다.

 

2. Electron 설치

 바로  Dev 디펜던시에 추가해줍니다.

npm install --save-dev electron
//or
yarn add -D electron

설치한 결과  package.json

{
  "name": "vite-electron-test",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview"
  },
  "dependencies": {
    "vue": "^3.2.16"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^1.9.3",
    "electron": "^15.1.2",
    "vite": "^2.6.4"
  }
}

자 이제 설치했으니 다음 단계로!

 

3. Vite 빌드

 Vite를 빌드하면 dist 폴더에 빌드 파일이 생성됩니다. 그걸 Electron이 읽을 수 있도록 만들어볼 거예요.

npm run build
//or
yarn build

빠른빌드

빌드를 하면 dist폴더에 static파일들이 생성된 것을 확인할 수 있습니다. 이제 Electron이 얘를 읽을 수 있게 경로 설정할 거예요.

 

다음은 Electron 설정을 시작해보죠!

 

4. Electron 파일 설정

Electron 빠른 시작 페이지 맨 밑에 보면 코드가 싹 다 나와있습니다.

링크: https://www.electronjs.org/docs/latest/tutorial/quick-start 

 

네가지 파일 정도를 신경써줘야합니다. 실제로는 3개파일이예요.

- 1. main.js

- 2. preload.js

- 3. index.html(있는거 그대로)

- 4. package.json

 

저 중에 2.preload.js는 세 번째 시리즈(③ 데스크탑 앱 만들기 #3 - ipcMain, ipcRenderer)에서 통신을 위해 필요하니까 파일만 만들면 두시면 됩니다. 그리고 index.html은 그냥 가만히 냅두면 되고 package.json파일은 만들 필요 없으니 main.js파일 경로랑 "start"스크립트만 추가하면 됩니다.

 

package.json

{
  "name": "vite-electron-test",
  "version": "0.0.0",
  "main": "main.js", // <-- 요기 추가
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview",
    "start": "electron ." // <-- 요기 추가
  },
  "dependencies": {
    "vue": "^3.2.16"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^1.9.3",
    "electron": "^15.1.2",
    "vite": "^2.6.4"
  }
}

main.js 파일명이나 script에 "start" 명령은 좋아하는 걸로 바꿔주세요. 편하신 걸로!

 

main.js 생성

const { app, BrowserWindow } = require('electron')
const path = require('path')

function createWindow () {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  // and load the index.html of the app.
  mainWindow.loadFile('./dist/index.html') // <-- 요기에 './dist/'를 추가!

  // Open the DevTools.
  // mainWindow.webContents.openDevTools()
}

app.whenReady().then(() => {
  createWindow()

  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

 

preload.js 파일 생성

// 냉무

 

자 끝났습니다. 이제 시작해볼까요?

 

5. 실행 그리고 오류 고치기

 설정은 대략 끝났으니 실행해봅니다.

npm run start
//or 
yarn start

짜잔! 빈 화면이 보입니다. 그리고 콘솔 창에 이런 오류가 있을 거예요.

뭐라는 거야 대체.

dist안에 있는 index.html 파일을 열어봅니다.

 

파일들의 경로가

/assets/index.860ec699.js

이런 식으로 지정되어 있을 거예요. Electron은 root폴더에서 실행이 되는데 asset폴더는 root폴더에 있는 게 아니란 말이죠. 그래서 저 경로들을 모조리 dist폴더를 향하게 만들 겁니다.

 

vite.config.js로 가볼게요.

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
const path = require("path"); // <-- 얘 추가하고

// https://vitejs.dev/config/
export default defineConfig({
  base: path.resolve(__dirname, "dist"), // <-- 얘 추가하고
  plugins: [vue()],
});

저 두 줄을 추가하고 다시 빌드합니다.

 

그리고 다시 dist폴더 안에 index.html 파일에 소스 경로를 확인해보면

/Users/my-name/electron/vite-electron-test/dist/assets/index.860ec699.js

 

요로케 뭔가 길어진 것을 확인할 수 있습니다. 그러니 이제 root에 있는 main.js가 읽어 들일 수 있게 변했군요.

 

그럼 다시 "start"해봅시다.

깔끔!

 

6. 마무리

 끝났습니다. 이제 Vite랑 Vue3랑 Electron을 실행할 준비가 됐어요. 근데 개발을 하려면 소스코드가 변경되었을 때 알아서 바뀌게 하고 싶잖아요? 매번 코드 바뀔 때마다 빌드하고 "start"해주는 건 너무 번거롭잖아요? 그래서 다음 포스팅은  Hot reload기능을 구현해보도록 하겠습니다!

 

그럼 다음 시리즈에서!

 


너무 깔끔해.. 젠장, 기분 좋아.
데스크탑 앱 만들기 마지막 포스팅


링크

① 데스크탑 앱 만들기 #1 - Vue CLI + Electron

② 데스크탑 앱 만들기 #2 - ExcelJS, SheetJS, js-xlsx, js-xlsx-style

③ 데스크탑 앱 만들기 #3 - ipcMain, ipcRenderer

④ 데스크탑 앱 만들기 #4 - ExcelJS, ipcRenderer, ipcMain, removeListener

 

 그럼 이번 포스팅에서는 ipc 통신을 이용해서 frontend(vue)와 backend(electron) 사이에서 데이터를 교환해보겠습니다. 그리고 그 데이터는 시스템에 저장되어있는 엑셀 파일을 ExcelJS로 변환해서 가공해볼게요.(코드에 문제가 있거나 더 좋은 방법 아시는 분은 친절하게 댓글 남겨주세요!)

 

 순서는 다음과 같습니다.

1. frontend에서 엑셀 파일 로드를 요청한다.

2. backend에서 엑셀 파일을 로드한다. ExcelJS 라이브러리를 이용해서 데이터를 가공한다. 가공한 데이터를 frontend로 보낸다.

3. frontend에서 데이터를 갖고 논다.

4. backend로 갖고 놀았던 데이터를 보내 저장할 것을 요청한다.

5. 받은 데이터를 엑셀에 덮어 씌운다.

6. 끝.

 

 하나씩 시작해보겠습니다.

1. frontend에서 엑셀파일 로드를 요청한다.

App.vue

let filePath = '/Users/username/excel/test.xlsx'

ipcRenderer.send('load-excel-files', filePath)

 이 코드는 mounted 또는 created에 넣어서 frontend page가 로드되면 바로 실행되게 했습니다. 그리고 filePath는 엑셀 파일이 있는 위치를 적어줍니다. 저는 맥을 사용하고 있으니 /Users가 시작이고, 윈도우 유저라면 C:\로 시작하겠지요. 그리고 send 메소드를 통해 backend로 파일 경로만 패스해줍니다.

 

2. backend에서 엑셀 파일을 로드한다. ExcelJS 라이브러리를 이용해서 데이터를 가공한다. 가공한 데이터를 frontend로 보낸다.

background.js

ipcMain.on('load-excel-files', async (event, filePath) => {
  const sheetData = []
  const workbook = new ExcelJS.Workbook()
  await workbook.xlsx.readFile(filePath)
  const worksheet = workbook.worksheets[0] // 첫번째 sheet 선택

  const option = { includeEmpty: true }

  await worksheet.eachRow(option, (row, rowNum) => {
    sheetData[rowNum] = []
    row.eachCell(option, (cell, cellNum) => {
      sheetData[rowNum][cellNum] = { value:cell.value, style:cell.style }
    })
  })

  event.reply('reply-excel-files', sheetData)
})

 backend에서 ipcMain.on 메소를 통해 데이터를 받습니다. 그리고 ExcelJS 라이브러리를 이용해서 각 셀을 순회합니다.

 

 eachRow는 각 행을 순회하는 메소드입니다. 첫 번째 줄부터 한 칸씩 아래 방향으로 순회합니다.

 eachCell은 오른쪽 방향으로 각 cell을 순회합니다.

 이제 값을 얻어옵니다. 저 같은 경우에는 cell값과 style만 필요하니까 두 가지만 받아왔어요.

 

 값을 얻어왔으면 다시 frontend로 넘겨줍니다. event 인자에 reply 메소드를 통하면 바로 보내는 것이 가능하다고 합니다.

 

App.vue

ipcRenderer.on('reply-excel-files', (event, sheetData) => {
	console.log(sheetData)
})

 위 코드는 created에 넣어주셔야 위 event.reply 메소드에 반응해서 frontend console창에 sheetData를 출력해줍니다.

 

 

3. frontend에서 데이터를 갖고 논다.

App.vue

function deepClone(obj) {
    if(obj === null || typeof obj !== 'object') {
        return obj;
    }
    const result = Array.isArray(obj) ? [] : {};
    for(let key of Object.keys(obj)) {
        result[key] = deepClone(obj[key])
    }
    return result;
}

myCell.style = await deepClone(mySheetData[7][2].style)
myCell.style.fill = {
	fgColor: {argb: "09B050"},
	pattern: "solid",
	type: "pattern"
}
myCell.value = "myCellValue"

  이제 데이터를 마음껏 가지고 놀면 됩니다.

 

 가지고 놀 때 실제로 해보면서 알게 된 점은 cell에 있는 value는 값이 잘 적용이 되는데 style은 잘 적용이 안되더라구요. 그래서 ExcelJS를 통해 데이터를 읽을 때 가지고 왔던 임의의 원본 cell을 deepClone 해서 style을 바꾸니 잘 되었습니다. 코드에 deepClone 코드도 추가해 놓았으니 참고하세요.(나중에 기회가 생기면 clone에 대한 내용도 쓰고 싶군요!)

 

 fgColor는 color HEX 값인데 앞에 "#"을 지워서 입력하면 잘 작동합니다.

 

4. backend로 갖고 놀았던 데이터를 보내 저장할 것을 요청한다.

App.vue

ipcRenderer.send('write-excel-files', filePath, sheetData)

 

 잘 가지고 놀았던 데이터 뭉치를 ipcRenderer.send 메소드를 통해 전달합니다. 어디에 저장할지도 전달해 주어야 하니 1번에서 사용되었던 filePath('/Users/username/excel/test.xlsx')를 인자로 전달하고, 데이터(sheetData)도 인자로 전달합니다.

 

5. 받은 데이터를 엑셀에 덮어 씌운다.

background.js

ipcMain.on('write-excel-files', async (event, filePath, sheetData) => {
  const workbook = new ExcelJS.Workbook()
  await workbook.xlsx.readFile(filePath)
  const worksheet = workbook.worksheets[0]

  for (let rowNum=1; rowNum<sheetData.length; rowNum++) {
    let row = worksheet.getRow(rowNum)
    for (let cellNum=1; cellNum<sheetData[rowNum].length; cellNum++) {
      let cell = row.getCell(cellNum)
      if ( cell ) {
        cell.value = sheetData[rowNum][cellNum].value
        cell.style = sheetData[rowNum][cellNum].style
      }
    }
  }

  workbook.xlsx.writeFile(filePath).then( ()=>{
    event.reply('reply-excel-files', sheetData)
  }).catch( (error) => {
    event.reply('reply-excel-files', error)
  })
})

 제가 선택한 방법은 기존의 엑셀 파일을 불러와서 row(행)와 cell을 순회하면서 저희가 만지작 거렸던 데이터 뭉치에 있는 value와 style값을 집어넣는 것이었습니다.

 

※ 주의 : 제가 쓰는 방법을 쓰게 되면 문제가 생길 수 있습니다. 예를 들어, frontend에서 데이터를 만지작 거릴 때 프로그래밍 오류로 인해 값이 전부 사라지게 되고, 확인 하지 않고 기존 파일에 덮어 씌우면 기존 데이터들이 다 날아가 버리는 신비를 확인할 수 있습니다.

두 가지를 기억해 주세요.

1. 엑셀 파일을 작업하기 전에 다른 곳에 파일을 복사해 놓는다. 또는 자동으로 복사해 놓는 코딩을 한다.

2. 만지작 거린 데이터는 데이터가 잘 살아있는지 확인하는 코드를 집어넣는다.

 

 잘 입력이 되면 다시 frontend로 reply 합니다. 1번에서 미리 만들어 놓은 코드가 있어서 또 작성할 필요는 없습니다. 1석 2조.

 

6. 끝.

 자 이렇게 되면 과정이 끝나게 됩니다. 그런데 frontend page가 로드될 때 한 번만 불러와져야 할 엑셀 파일이 page가 로드될 때마다 중첩이 되더라구요. 예를 들면 한번 열면 1, 두 번째 열면 2. 이런 식으로 로드되는 횟수만큼 통신이 이루어져서 고치는 방법을 찾게 되었습니다.

 

 위에 보면 2, 3번씩 엑셀 파일 로딩 확인이 되는 것을 볼 수 있습니다.(실제로 로딩을 3번 하는 건 아니고 'reply-excel-files'의 중첩) 프로그램을 완전히 다시 로드(Ctrl + R 또는 ⌘ + R)하면 다시 초기화되는데 라우팅을 설정해서 입장 퇴장을 반복하면 점점 더 쌓이게 됩니다. 이유는 페이지가 로드될 때마다 저희가 1번에서 설정해 놓은 ipcRenderer.on 메소드로 만들어놓은 listener가 중첩되는 현상 같아서 방법을 좀 찾아본 결과 수정하는 데 성공했습니다.

 

App.vue

const listener = ipcRenderer.rawListeners('reply-excel-files')
if  ( listener.length > 1)
	ipcRenderer.removeListener('reply-excel-files', listener[1])

 

 저 같은 경우에는 for문을 이용해서 엑셀 파일을 여러 개 호출하기 때문에 ipcRenderer.on 메소드를 이용하고 Listener가 늘어나면 삭제하는 방법을 사용했지만, 엑셀 파일을 한 개만 호출할 경우 ipcRenderer.on이 아닌 ipcRenderer.once 메소드를 이용하면 Listener가 중첩되지 않는 것을 확인했습니다.

 

 이렇게 프로그램 만들어 보았습니다. 엑셀 파일은 업무에서 많이 사용하고 여러분들도 반복적으로 하는 일이 있을 것이라고 생각합니다. 제 경험이 누군가에게 도움이 되길 바래요. 한번 프로그램을 만들어 놓으면 누구보다 빠르게 업무를 처리할 수 있으니(빠르게 퇴근한다고는 안 했습니다.) 아직 업무 중인 척 연기하시면서 여유로운 회사생활되시기를 바랄게요.

 


프로그램 만드는데 시간은 들어가지만 재밌군요.

 

생소해서 어려운. 하지만 의외로 쉬운.

 

너 거기있고, 나 여기있고.

 

 


링크

데스크탑 앱 만들기 #1 - Vue CLI + Electron

데스크탑 앱 만들기 #2 - ExcelJS, SheetJS, js-xlsx, js-xlsx-style

데스크탑 앱 만들기 #3 - ipcMain, ipcRenderer

④ 데스크탑 앱 만들기 #4 - ExcelJS, ipcRenderer, ipcMain, removeListener

 

 위 포스팅에서 Vue CLI를 이용해서 frontend(renderer)를 만들고, electron-builder를 이용해서 backend(Main)를 만들었습니다. 그리고 excel 파일을 읽는 라이브러리를 ExcelJS를 이용해서 로딩에 성공하였으니 이제 이 객체를 frontend로 보내줘야 데이터를 가지고 이것저것 만지작 거릴 수 있겠죠? 그 이후에 만지작 거린 객체를 다시 백엔드로 보내서 엑셀 파일에 저장해주면 제 프로젝트가 성공하게 됩니다.

 

 아, 그 전에 애초에 frontend에서 파일을 읽고 바로 수정하면 되지 않냐 생각도 했었는데 시스템 안에 있는 파일을 직접 읽고 쓰기 위해서는 일렉트론 같은 시스템 프로그램이 필요하더라구요. 시스템 프로그램이 없으면 프로젝트 로컬 폴더 안에 있는 내용만 접근 가능하고, 폴더 밖에 있는 파일들은 접근이 안되니 업로드하는 방식으로 밖에 없는 것 같았습니다. 애초에 기획이 자동화니까 일렉트론을 쓰기로 결정했었죠.


 그럼 이제 본격적으로 fontend와 backend의 데이터 교환 방법을 알아보겠습니다.

 

 일렉트론 공식 문서에 뭐라고 설명되어있는지 부터 확인하고 갈게요!

 

★ ipcRenderer - renderer 프로세스에서 main 프로세스로 비동기적인 통신을 합니다.

★ ipcMain - main 프로세스에서 renderer 프로세스들로 비동기적인 통신을 합니다.

 

 내용만 봐서는 뭔지 정확히 감이 오질 않습니다. 혹시 Javascript 좀 해보신 분들은 좀 익숙하실지도 모르겠습니다. 웹페이지에서 event emit, listen과 많이 닮아 있다고 생각이 들었거든요.

 

 그럼 공식 문서에 나와있는 테스트 코드를 확인해봅니다.

 

 다음 코드는 frontend에 있는 아무 파일에 넣으면 되는데 저는 App.vue 파일에 넣어서 테스트를 해보겠습니다.

// renderer 프로세스(웹 페이지)안에서
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // "pong"이 출력됩니다.

ipcRenderer.on('asynchronous-reply', (event, arg) => {
  console.log(arg) // "pong"이 출력됩니다.
})
ipcRenderer.send('asynchronous-message', 'ping')

 

 ipcRenderer 테스트 코드에서 사용된 메소드는 on, send, sendSync로 3가지입니다.

- on: backend에서 frontend로 통신을 시도하면 받아 주는 메소드

- send: frontend에서 backend로 통신을 보내는 메소드

- sendSync: send메소드는 비동기적으로 보내지고, 이 메소드는 동기적으로 작동한다(동기, 비동기는 여기서 설명하기엔 내용이 복잡해지므로 패스!).

 

 아래 코드가 backend 파일인 background.js에 들어갑니다.

// main 프로세스안에서
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
  console.log(arg) // "ping" 출력
  event.reply('asynchronous-reply', 'pong')
})

ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg) // "ping" 출력
  event.returnValue = 'pong'
})

 ipcMain 테스트 코드에서 사용된 메소드는 on, reply로 2가지입니다.

- on: frontend에서 background.js로 통신을 요청하면 받는 주는 메소드

- reply: on이 성공하면 frontend로 다시 통신을 넣어주는 메소드

 

 위의 테스트 코드를 실행했을 때의 결과는 다음과 같습니다.

 

▶︎ frontend(renderer) side

▶︎ backend(main) side

 

 frontend에서 pong 두 번, backend에서 ping 두번이 로그에 출력이 되었네요. 보내고 받고를 두번 했다는 뜻인데 코드를 한번 살펴볼게요.

 빨간색은 비동기(asychronous) 통신을 나타내고, 파란색은 동기(synchronous) 통신을 나타냅니다. 두 가지의 통신을 테스트하는 코드였네요. 그래서 frontend와 backend 콘솔에 pong이 2개, ping이 2개가 출력된 것입니다.

 

 자세히 좀 볼까요?

 

▶︎ 비동기 통신(분홍색, asychronous)

① frontend에서 ipcRenderer.send 메소드를 'asynchronous-message' 채널을 통해 'ping'이라는 데이터를 backend로 보냅니다.

② backend에서 ipcMain.on 메소드에서 정의한 'asynchronous-message' 채널을 통해서 인자를 받고 콘솔에 출력하네요.

그리고 바로 ipcMain.reply 메소드를 'asychronous-reply' 채널을 통해 'pong'이라는 데이터를 frontend로 보냅니다.

④ 마지막으로 frontend에서 ipcRenderer.on 메소드를 통해 정의된 'asychronous-reply' 채널을 통해 받아진 'pong'이라는 데이터는 콘솔에 출력되는 것으로 끝이 납니다.

 

▶︎ 동기 통신(파란색, sychronous)

① frontend에서 ipcRenderer.sendSync 메소드를 'synchronous-message' 채널을 통해 'ping'이라는 데이터를 backend로 보냅니다.

② backend에서 ipcMain.on 메소드에서 정의한 'synchronous-message' 채널을 통해서 인자를 받고 콘솔에 출력하네요.

③ 그리고 event의 returnValue를 'pong'으로 입력하면서 자동적으로 frontend로 데이터가 리턴됩니다.

④ 마지막으로 frontend로 ipcRenderer.sendSync의 리턴 값이 'pong'으로 전달되었고 바로 콘솔에 출력됩니다.

 

 

 어떤가요? 일렉트론의 frontend와 backend이 통신을 어떻게 하는지 같이 살펴보았는데 이해가 되셨으면 좋겠네요. 저는 테스트 코드를 해보면서 생각보다 쉽다는 생각을 했습니다. ipc라는 용어 자체가 헷갈릴 뿐이죠.

 

 그럼 다음 포스팅에서는 제 프로젝트에서 ExcelJS와 ipcMain, ipcRenderer를 이용해서 어떻게 구현하였는지, 그리고 Listner가 중첩돼서 중복적으로 통신 이벤트가 발생하는 현상을 수정하는 방법들을 적어보도록 하겠습니다.

 

 

※ 동기와 비동기의 차이점은 생활코딩님께서 영상으로 너무 잘 설명해주셔서 링크를 남깁니다. 영상은 개념 설명이고 아래 더보기에 강좌 링크는 남겨놓았으니 동기, 비동기가 궁금하신 분은 참고해주세요!

 

생활코딩 - 동기와 비동기의 개념

 


다음 : 데스크탑 앱 만들기 #4 - ExcelJS, ipcRenderer, ipcMain, removeListener

더보기 : 생활코딩 - nodejs에서의 동기, 비동기, 콜백

프로젝트가 끝나갈 무렵, 코딩은 참 즐겁다는 생각이 들었다.

 

어떤 라이브러리를 쓰는 것이 좋을까?

쉬운길로 가려다가, 결국 다 써보게 되었다..

 


링크

① 데스크탑 앱 만들기 #1 - Vue CLI + Electron

② 데스크탑 앱 만들기 #2 - ExcelJS, SheetJS, js-xlsx, js-xlsx-style

③ 데스크탑 앱 만들기 #3 - ipcMain, ipcRenderer

④ 데스크탑 앱 만들기 #4 - ExcelJS, ipcRenderer, ipcMain, removeListener

 

 

 6개의 엑셀 파일에 데이터를 매일 집어넣는 게 귀찮아서 시작한 데스크톱 앱 만들기 2탄입니다. 이번 글에서는 엑셀 파일을 일렉트론 background.js(웹앱으로 치면 backend 또는 server)로 불러오는 작업을 실행해볼 겁니다. 방대한 JAVASCRIPT 생태계에 분명 이와 관련된 라이브러리가 있을 거라고 생각했습니다. 그리고 구글링을 시작했죠. 그리고 처음 발견한 라이브러리가 SheetJS입니다.

 

 결과적으로 저는  ExcelJS 라이브러리를 사용하고 있습니다. 이유는 스타일 오류 때문입니다.

 

 SheetJS는 엑셀을 JSON 형식으로 뽑아서 읽고 쓸 수 있다는 점은 정말 편리합니다. 보기에 익숙하니 그 장점이 컸었죠. 엑셀 파일을 읽어서 JSON 형식으로 읽어오는 데는 대부분 문제가 없었습니다. 문제는 쓰기에서 발생했죠.

 

The black cell in excel

  위처럼 데이터가 들어간 부분의 대부분이 검은색이 되어버렸습니다. 고칠 방법을 찾아 이리저리 헤맸더랬죠. SheetJS에서 문제가 발생했으니 혹시 다른 라이브러리를 적용하면 어떨까 해서 js-xlsx도 써보았습니다. 하지만 결과는 같았죠. 더 찾아보니 xlsx-style 라이브러리도 있다길래 희망을 가지고 추가로 사용해보았습니다. 역시 실패.

 

 그리고 혹시나 SheetJS의 유료버전은 어떤가 싶어서 가격 정책을 알기 위해 구글링 해보았으나 찾는데 실패. 그래서 직접 이메일을 보내보았습니다. 답변은 

가격은 "누가"(회사 내부, 다른 회사의 계약 작업, 고객 대면, 공개), "무엇"(아래에 설명 된 기능 세트) 및 "어디"(서버 측 또는 브라우저 또는 인앱)에 따라 다릅니다.

라고 왔습니다.

 

 혼자 쓸꺼니까 얼마냐고 물어보려다가 왠지 비쌀 거 같기도 하고 굳이 돈을 내면서까지 만들어야 하나 생각도 들고 귀찮기도 해서 참았습니다. 그리고 첨부된 PDF 파일에 프로 버전에 대한 설명이 있었는데요.

 

 네, 돈주면 cell backgrounds와 borders를 customize 할 수 있다고 합니다. 회사에서 프로젝트를 받아서 만든다면 SheetJS 프로 버전도 써보고 싶다는 생각이 들었습니다. JSON은 편리하거든요.

 

 어쨋든 실패하고 다시 찾은 라이브러리가 ExcelJS입니다. 깃헙에 들어가서 쭉 읽어보니 코딩 초보인 저에게는 SheetJS보다 확실히 어려운 코드들이었습니다. 그래도 어떻게 하겠나요. 이 또한 넘어야 할 산인 것을.

 

 먼저 설치!

npm install exceljs
// or
yarn add exceljs

 

 background.js에 임포트 해줍니다. 파일을 로드할 거니까 경로 관리를 쉽게 하기 위해 path도 임포트 합니다.

import path from 'path'
import ExcelJS from 'exceljs'

 

 그럼 하단에 async function을 하나 만들어줍니다. 파일을 읽는 동안 기다려주지 않으면 오류를 내뿜거든요.

const filePath = '/Users/username/filepath/filename.xlsx'

async loadExcelFile(filePath) {
	const workbook = new ExcelJS.Workbook()
	await workbook.xlsx.readFile(filePath)
	const worksheet = workbook.worksheets[0]

	console.log(worksheet)
}

loadExcelFile(filePath)

 

 아래처럼 Worksheet 객체가 소환되면 성공!

 이제 객체에서 행(row)과 셀(cell)을 순회하면서 값을 받아오겠습니다.

const filePath = '/Users/username/filepath/filename.xlsx'

async loadExcelFile(filePath) {
	const sheetData = [] 

	const workbook = new ExcelJS.Workbook()
	await workbook.xlsx.readFile(filePath)
	const worksheet = workbook.worksheets[0] // 첫 번째 sheet 선택
    
	const options = { includeEmpty: true }

	await worksheet.eachRow(options, (row, rowNum) => {
		sheetData[rowNum] = []
		row.eachCell(options, (cell, cellNum) => {
			sheetData[rowNum][cellNum] = { value:cell.value, style:cell.style }
		})
	})

	console.log(sheetData)
}

loadExcelFile(filePath)

 

 ExcelJS 라이브러리에서는 eachRow를 이용해서 각 행을 순회하면서 값을 가져오고, 각 행을 돌 때 다시 row객체에서 eachCell을 이용해서 각 cell의 내용을 가져옵니다. 행 또는 셀의 내용을 가져올 때 빈 셀의 값과 스타일도 가져오고 싶다면 options에 includeEmpty를 true로 설정하면 됩니다.

 

 이제 콘솔 창을 보면 잘 다듬어진 결과값이 보입니다. 깔끔하지요?

 

 다음 포스팅에서는 이 결과값을 가지고 FrontEnd와 BackEnd 사이에서 통신하는 방법인 ipcMain, ipcRenderer에 관해 다뤄보겠습니다. 그럼 안녕히!

 


다음 : 데스크탑 앱 만들기 #3 - ipcMain, ipcRenderer

오늘도 하나 해냈다..
시작은 업무 중에 매일 작성하는 엑셀 파일이 많아서 그 시간을 줄여보고자 함이었다.


링크

① 데스크탑 앱 만들기 #1 - Vue CLI + Electron

② 데스크탑 앱 만들기 #2 - ExcelJS, SheetJS, js-xlsx, js-xlsx-style

③ 데스크탑 앱 만들기 #3 - ipcMain, ipcRenderer

④ 데스크탑 앱 만들기 #4 - ExcelJS, ipcRenderer, ipcMain, removeListener

 

 저는 직장인입니다. 직장에서 근무를 하면 매일 작성해야 하는 문서들이 있죠. 매일 작성하다 보니 그런 생각이 들었습니다.

 

반복 작업하는 거 귀찮다.. 

 

 그래서 만들었습니다. 적당한 값을 입력받으면 나머지 파일에 그 정보들을 뿌려주는 방식이었습니다. 그런 것이 가능할 것이라고 생각할 수 있었던 계기는 웹사이트를 만들면서 다져진 것이랄까. 그 정도는 가능하지 않을까 하는 생각에 시작을 해보았습니다.


대략적인 프로그램의 내용은 다음과 같았습니다.

 

1. 프로그램을 실행한다.

2. 자동으로 엑셀 파일(6개)을 불러온다.

3. 그날 업무내용을 양식에 입력한다.

4. 입력한 내용을 적절히 엑셀 파일에 뿌린다.

5. 끝.

 

데스크톱 프로그램을 만들기 위해 electron이 필요하고, VueJS에 익숙하니 누군가 두 가지를 섞어 놓은 것이 있지 않을까 싶어서 검색!

 

electron-vue

 그리고 찾아냈습니다. 문서도 깔끔하게 되어있고 GitHub Star도 12,000개가 넘어 바로 설치를 해보았고 내용을 좀 살펴보다가 결론을 냈습니다.

 

탈락!

 

 이유는 electron이 2.0.4 version으로 설치가 되어서입니다. 최근 버전이 9.0.1인데 잘 모르지만 뭔가 문제가 생길 것 같은 느낌이 들어서 무서웠어요(문서를 작성하고 있는 2020.9.1 현재 10.1.0 버전까지 나왔습니다).

 

 그럼 이제 electron 최신 버전을 유지하면서 VueJS를 사용하는 방법이 무엇인가를 검색하다가 Vue CLI에 electron builder를 덮어 씌우는 방식으로 만들 수 있는 방법을 찾아냈습니다.

 

Vue CLI Plugin Electron Builder

 Vue CLI는 VueJS를 표준화된 방법으로 시작할 수 있게 도와주는 데(여기서 CLI는 Command Line Interface를 말합니다) 맥의 경우 터미널에서, 윈도는 command창을 이용해서 쉽게 프로젝트를 시작할 수 있게 만들어줍니다.

 

 먼저, Vue CLI를 설치하고,

// 이미 설치되어 있으면 패스!
npm install -g @vue/cli
// OR
yarn global add @vue/cli

 

 그리고 잘 설치되어 있는지 확인!

vue --version

 

 저는 4.1.1 버전이지만 2020년 9월 1일 현재 4.5.4 버전이 최신입니다. 그럼 이제 버전 확인이 잘 되었으면 테스트 프로젝트를 하나 만들어볼게요. vue-electron-test 프로젝트를 만들어보겠습니다.

vue create vue-electron-test

 

 

 이것저것 설치하라고 나오는데 저는 코딩 습관을 누가 잡아줘야 하니까 Linter도 깔고, Router와 Vuex도 쓸 거 같으니까 같이 설치를 합니다. 그 이후에도 뭐 이것저것 물어보는데 ESLinter standard만 고르고 나머지는 계속 엔터 쳐서 넘겼습니다.

 

 여기에 electron builder를 입혀봅시다.

// 프로젝트 폴더로 이동
cd vue-electron-test
// electron builder 설치
vue add electron-builder

 

 설치를 시작하면 electron version을 선택하라고 나옵니다. 7, 8, 9 버전이 있으니 혹시 electron 이전 버전에 익숙한 분은 선택하시면 될듯합니다. 하지만 저는 잘 모르니까 최신의 안정적일 것 같은 9 버전을 선택합니다.

 

 

electron builder 설치 전(좌)과 후(우)

 electron builder를 설치하게 되면 노란색으로 보이는 파일 내용이 변하고 초록색 파일은 background.js파일이 생깁니다. 바로 이 파일이 데스크톱 앱에서 우리의 컴퓨터를 컨트롤해줄 영역이죠. 웹 앱으로 치면 backend 정도가 되겠습니다.

 

그리고 실행!

npm run electron:serve
// OR
yarn electron:serve

 

 짜잔!

성공!


다음 : 데스크탑 앱 만들기 #2 - ExcelJS, SheetJS, js-xlsx, js-xlsx-style

 

만드는 시간이 길어질수록 그냥 엑셀 파일에 데이터를 직접 넣을걸 하는 후회가 밀려왔다.

+ Recent posts