돌아온 앱 만들기 시리즈

 

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


링크

① 데스크탑 앱 만들기 #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기능을 구현해보도록 하겠습니다!

 

그럼 다음 시리즈에서!

 


너무 깔끔해.. 젠장, 기분 좋아.
너무 편리해서 놀랍도다!

 

아리땁군요

 


 이미지를 여러 가지로 만지고 놀던 중에 이미지 편집 라이브러리가 필요하게 되었습니다. 직접 만들어 볼 생각은  하지도 않았습니다. 그리고 찾은  게  CropperJS인데요. 너무 간편하고 편리해서 사용법을 공유하고 싶습니다.

 

 문서보고 적용하는 것이 어렵지는 않지만 한 가지 실수를 하는 바람에 2시간 정도를 꼬박 검색하는데 날린 거 같아요. 여러분은 실수하지 마시라고 팁도 넣어드립니다.

 

 적용하는데 있어서 헷갈리지 않도록 기본 코드만 넣어보았어요. 적용이 완료되면 아래처럼 보입니다.

 

저도 아리따운 분을 모셔왔어요!

 바로 코드로 들어갑니다!

 

test.vue

<template>
<div>
  <div class="w-full p-4 text-center bg-green-200 rounded-t-md">
    <div class="flex justify-between pb-2">
        <h3 class="text-lg font-medium text-gray-900">
            CropperJS!
        </h3>
        <!-- "X" image : heroicons -->
        <svg class="h-4 w-4 text-gray-700 cursor-pointer sm:mr-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
        </svg>
    </div>
    <div class="flex">
        <div class="w-3/4 mr-4">
            <img ref="image" :src="src" class="hidden" alt="image-edit">
        </div>
        <div class="w-1/4">
            <div class="pb-2">Preview</div>
            <div class="flex items-center justify-center h-32 bg-yellow-100">
                <div class="preview overflow-hidden w-full h-32 text-center bg-gray-200"></div>
            </div>
        </div>
    </div>
  </div>
  <div class="bg-teal-100 p-4 text-right rounded-b-md">
    <button type="button" class="w-32 p-2 m-2 border rounded-md shadow-sm bg-red-600">
      Save
    </button>
    <button type="button" class="w-32 p-2 m-2 border rounded-md shadow-sm bg-white">
      Cancel
    </button>
  </div>
</div>
</template>

 먼저 HTML 구성에서 15번째 줄에 img 태그가 있는데 hidden(display:none;)으로 처리해서 보이지 않게 해 주세요. 그리고 20번째 줄에 있는 preview 클래스가 중요합니다. 바로 그 곳에 CropperJS가 미리보기를 넣어줘요. 나머지는 입맛에 맞게 바꾸셔도 됩니다.

<script>
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'

export default {
  name: 'Home',
  mounted() {
    this.image = this.$refs.image
    this.cropper = new Cropper(this.image, {
        preview: '.preview',
    })
  },
  computed: {
  },
  data: ()=>({
    src: '/images/girl.jpg',
    image: {},
    cropper: {}
  }),
  methods: {
  }
}
</script>

 제가 2시간동안 헤맨이유가 여기에 있습니다. 3번째 줄에 보면 css를 꼭 import 해주세요. 가이드 문서에서도 저렇게 하라고 알려주는데 그걸 놓치고 아까운 시간을 날려버렸습니다. 만약 추가를 안 해주면 편집 컨테이너가 뜨는 게 아니라 이미지만 떠버려서 편집이 불가능해집니다.

 

 그럼 CropperJS에게 이미지의 위치를 Vue의 경우 refs속성(javascript의 경우 getElementById도 가능)을 이용해서 위치를 알려줍니다. 그리고 생성된 인스턴스를 data cropper에 저장하구요. 그리고 option으로 preview의 위치를 preview class 선택자를 이용하여 알려주면 됩니다.

<style scoped>
img {
    display: block;
    max-width: 100%; /* This rule is very important, please do not ignore this! */
}
</style>

 이제 마지막입니다. 위에 script에서 css를 import해주고 나서도 한 가지 해줘야 하는데 css를 추가해줘야 합니다. 이걸 추가 안 하게 되면 컨테이너에 이미지가 자기 사이즈대로 보여서 작게 보이든, 크게 보이든 이쁘게 보이지 않아요.

 

 자, 이제 끝났습니다. 나머지 여러가지 기능들을 추가할 수 있는데 위에 만들어 놓은 cropper 인스턴스를 이용하면 쉽게 메소드를 만들 수 있습니다. 예를 들면 이런 식입니다.

this.cropper.reset() // 원본으로 돌아가기
this.cropper.zoom(0.2) // 확대
this.cropper.zoom(-0.2) // 축소

 

 참 쉽죠? 기능들이 다양하고 많으니 라이브러리에 대한 설명은 아래 링크를 만들어 놓을게요!


라이브러리 링크!

fengyuanchen.github.io/cropperjs/

 

Cropper.js

 

fengyuanchen.github.io


성공하면 쉬워 보이는구나

 

이미지 업로드 마지막 포스팅!

여러개의 이미지를 한번에 등록해보자


① 이미지 업로드하기 #1 - VueJS + NodeJS

② 이미지 업로드하기 #2 - Amazon S3 setting + upload

③ 이미지 업로드하기 #3 - Drag and Drop

④ 이미지 업로드하기 #4 - Multi image management

 

 

 

이미지 다중 업로드 & 삭제

 이번에는 이전 코드를 조금 수정해서 이미지를 여러 개를 한 번에 업로드가 가능하도록 만들고, 등록된 이미지를 삭제하는 것까지 해보겠습니다.

 

 미리 말씀드리지만 이미지 삭제의 경우 FRONT END에서 이미지의 URL만 삭제되게 했습니다. Amazon S3에는 이미지가 저장되어 있는 상태니까 S3에서 삭제 규칙을 생성해주시거나 직접 삭제해 주어야 합니다.

 

자 바로 FRONT END로 코드로 가보겠습니다.

 

▶ FRONTEND

src/views/upload.vue

<template>
<div class="container mx-auto text-sm">
  <div class="grid grid-cols-4 gap-2">
    <div v-for="num in 4" :key="num" class="flex justify-center text-center">
      <div class="w-32 h-32 mb-4 border-2 border-dotted border-blue-500">
        <div v-if="images[num]" class="w-full h-full flex items-center justify-center"
             @mouseenter="showImageMenu(num, true)"
             @mouseleave="showImageMenu(num, false)">
          <img :src="images[num]" :alt="`image${num}`">
          <div v-if="show[num]" class="absolute">
            <div class="h-8 w-8 flex justify-center items-center rounded-full bg-blue-500"
                 @click="removeImage(num)">
              <svg class="w-6 h-6 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
              </svg>
            </div>
          </div>
        </div>
        <div v-else class="w-full h-full flex items-center justify-center cursor-pointer hover:bg-pink-100"
             @click="clickInputTag(num)"
             @drop.prevent="dropInputTag(num, $event)"
             @dragover.prevent>
          <input :ref="`image${num}`"
                 type="file" name="image" accept="image/*" multiple="multiple"
                 class="hidden"
                 @change="uploadImage(num)">
          <svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
          </svg>
        </div>
        <div>
          Image {{ num }}
        </div>
      </div>
    </div>
  </div>
</div>
</template>
<script>
import axios from 'axios'

export default {
  data: ()=>({
    images: [],
    show: []
  }),
  methods: {
    removeImage: function(num) {
      this.images.splice(num, 1, null)
    },
    showImageMenu: function(num, bool) {
      this.$set(this.show, num, bool)
    },
    uploadImage: function(num, file) {
      let images = file || Array.from(this.$refs[`image${num}`][0].files, v => v)
      
      for (let i = 0; i < images.length; i++) {
        let form = new FormData()
        let image = images[i]
        
        form.append('image', image)
		
        if (num+i < 5) {
          axios.post('/upload', form, {
              header: { 'Content-Type': 'multipart/form-data' }
          }).then( ({data}) => {
              this.$set(this.images, num+i, data)
          })
          .catch( err => console.log(err))
        }
      }
    },
    dropInputTag: function(num, event) {
      let file = Array.from(event.dataTransfer.files, v => v)
      this.uploadImage(num, file)
    },
    clickInputTag: function(num) {
      this.$refs[`image${num}`][0].click()
    }
  }
}
</script>

 

1. INPUT TAG를 4개로 변경

 먼저 HTML 코드에서 v-for를 이용해서 4개의 INPUT 태그를 만들어줍니다. 그리고 ref에도 num을 붙여서 중복되지 않도록 체크해줍니다.

 

2. clickInputTag 메소드 변경(INPUT 태그 클릭했을 때)

 INPUT 태그를 클릭했을 때 어떤 태그를 클릭했는지 알 수 있도록 num인자를 추가해줍니다(다른 메소드도 동일해요). 아, 그리고 ref의 숫자가 늘어나면서 this.$refs['images${num}']의 값이 배열이니까 [0]을 붙여주어야 제대로 작동합니다.

 

3. dropInputTag 메소드 변경(INPUT 태그에 이미지를 drop 했을 때)

 이미지를 drop했을 경우 uploadImage 메소드의 인자로 들어가는 file 변수의 경우, 1개일 때 Array.from 끝에 [0]을 붙일 때와 다르게 이것만 지워주면 배열의 형태로 전달이 됩니다. 심플하죠?

 

4. uploadImage 메소드 변경(반복문을 통해 여러 번 동작하도록 함)

 이제 여기에서 Array.from 안에 files뒤에 [0]을 지워주면 images 변수에 배열의 형태로 저장이 됩니다. 그럼 for문을 이용해서 반복적으로 이미지를 S3에 저장하고 URL을 받아오면 됩니다.

 이때 저희는 이미지를 업로드할 수 있는 INPUT 태그를 4개를 만들었고, num의 인자가 1부터 4까지 반복되도록 만들었으니, 이미지를 여러 개 업로드하더라도 5가 초과되는 이미지의 경우 upload 되지 않도록 if문을 작성해줍니다.

 

5. 이미지 업로드 성공 이후에 마우스 오버 시 삭제 가능하도록 변경

 @mouseenter, @mouseleave 속성을 이용해서 showImageMenu 메소드가 실행이 되도록 만들어서 svg가 마우스 오버가 되었을 때 보이도록 만들어줍니다. 그리고 이 svg를 클릭했을 때 removeImage 메소드가 실행되도록해서 이미지의 url을 삭제해줍니다.

 이때 중요한 것은 splice의 세 번째 인자를 빈 문자열 값('')을 설정해서 뒤에 있는 이미지가 앞으로 당겨지지 않도록 해야 완벽해지죠.


 드디어 이미지 업로드 시리즈를 끝냈습니다. 뿌듯하군요.

 

혹시 보면서 불편한 부분, 이해가 안 되는 곳 등 의견이 있으면 언제든 알려주세요! 최대한 수정하고 반영하도록 하겠습니다. 그러면서 저도 배울 수 있거든요!

 

그럼 다음에 다른 주제로 뵙겠습니다. 안녕히!


▶ BACKEND 코드는 이미지 업로드하기 #1 같습니다! 


 


배우는 즐거움을 함께 느꼈으면 좋겠어요!

 

모두가 필수 코스로 해본다는 크롤링, 제가 해보겠습니다.

 


링크

① 웹 크롤링 해보기 #1 - NodeJS, cheerio (feat. VueJS)

웹 크롤링 해보기 #2 - NodeJS, puppeteer

 

 오늘은 웹 크롤링(스크래핑)에 대한 내용입니다.

 

 저는 뭔가를 빠르게 구현해보고 싶을 때 PHP framework인 라라벨을 주로 사용하는데 웹 크롤링을 하기 위해 빠르게 비동기적으로 처리가 가능한 nodeJS를 선택했습니다. PHP는 언어의 특성상 크롤링을 하는데 적합하지 않다고 판단했습니다. 하나의 명령을 처리하고 그 명령을 완료하기 전에 다음으로 넘어가지 않아 많은 명령을 동시에 처리하지 못하는 한계를 가지고 있기 때문입니다. 물론, 그것을 보완하는 방법들도 있기는 하지만 그냥 nodeJS로 구현하면 쉬워져요.. 심지어 빠릅니다.

 

슈로록

 

 쿠팡에서 아이템의 수량을 주기적으로 파악해야 할 일이 생겨서 웹사이트를 들락날락하던 중에 클릭이 귀찮아져서 그냥 하나 만들어 봤습니다.

 

 자세히 보면 1번 행부터 순차적으로 진행되는 것이아니라 순서 없이 요청이 완료되는 순서대로 값이 보입니다. 이것이 nodeJS의 힘. 그럼 바로 하나씩 진행해 보겠습니다.

 

1. vue cli를 이용하여 프로젝트 만들기

 여기서 두가지 방법으로 나누어집니다.

 

 첫 번째는, 폴더를 두 개 만들어서(frontend, backend) frontend폴더에 vue-cli를 이용해서 프로젝트를 만들고, backend 폴더에 nodeJS, express 프로젝트를 만들어서 api 통신하게 만드는 방법!

 두 번째는 vue-cli로 프로젝트를 만들어서 express plugin을 이용하는 방법!

 

 첫 번째 방법은 다른 분들도 포스팅을 많이 하기도 했고, plugin을 사용해보고 싶어서 두 번째 방법으로 해보았습니다(vue-cli 버전이 3.x.x에서 작동합니다. vue --version으로 확인하세요).

 

vue create project-name

 먼저 폴더로 가서 프로젝트를 만들어 줍니다.

 

2. express plugin install

cd project-name
vue add express

 다음은 프로젝트 폴더로 들어가서 express를 add 해주면 끝. 간단하지요?

 

 package.json

"scripts": {
	...
	"express:watch": "vue-cli-service express:watch",
    ...
},

 개발을 하면서 코드 변경이 있으면 자동으로 서버를 재시작해주는 명령어입니다. 이거 없으면 기분이 안 좋아요. 꼭 하세요.

 

3. frontend, backend 실행

 

 

 폴더 구조는 다음과 같아지는데 src가 frontend, srv가 backend 폴더라고 보면 쉽습니다.

 

 그럼 실행!

// frontend start - http://localhost:8080
npm run serve


// backend start - http://localhost:3000
npm run express:watch

 이렇게 하면 시작할 수 있는데 저 같은 경우에는 port가 frontend는 8080, backend는 3000입니다. 개발환경에 따라 port는 달라질 수 있으니 확인하세요!

 

4. 라이브러리 설치

 이제 웹 크롤링에 사용할 cheerio 라이브러리를 설치!

npm install cheerio

 

Q. cheerio 말고 다른 라이브러리는 없나요?

A. 기본적으로 웹을 크롤링할 때는 목적이 있습니다. 저처럼 로그인이 필요 없고, 그냥 웹페이지의 내용만 가져올 때는 cheerio가 적합하죠. 가볍고 빠릅니다.

 하지만 웹에 들어가서 자동으로 로그인을 한다던가, 클릭해서 팝업창을 열어서 그 내용을 가져온다던가, 드래그 앤 드롭을 한다던가 하는 "javascript의 일"을 해줄 순 없습니다. 그래서 이런 "javascript의 일"을 하기 위해서 selenium, phantomJS, nightmareJS 등 다른 라이브러리를 이용하기도 하니까 목적이 다르다면 위 라이브러리를 검색해보세요.

 

5. 코드

 srv/index.js

import express from 'express'
import cors from 'cors'
import { checkProducts } from "./apis/checkProducts"

export default (app, http) => {

  app.use(express.json())
  app.use(cors({
    origin: 'http://localhost:8080',
    credentials: true,
  }))

  app.post('/check', (req, res) => {
    let id = req.body.id
    let link = req.body.link

    checkProducts(id, link).then( product => {
      res.json(product)
    }).catch( err => console.log(err) )
  })
  
}

 cors는 frontend에서 api 호출할 때 오류가 나서 추가했어요. 오류가 안 난다면 그냥 진행하셔도 무방합니다.

 

srv/apis/checkProducts.js

import axios from 'axios'
import cheerio from 'cheerio'

function checkProducts(id, url) {

    return axios.get( url ).then( ({data}) => {
        let $ = cheerio.load(data)
        let $prodName = $("div.prod-buy-header h2").text()
        let $salePrice = $("div.prod-sale-price span.total-price").text().replace(/[^0-9]/g,'')
        let $outOfStock = $('div.oos-label').text().trim()
        

        return {
            id: id,
            title: $prodName,
            salePrice: $salePrice,
            outOfStock: $outOfStock
        }
    })
    
}

export { checkProducts }

 코드는 모두 srv/index.js에 몰아넣어도 되는데, 한 파일에 코드량이 많아지면 읽기가 불편하더라구요, 그래서 그냥 파일을 나눈 것뿐입니다. 

 

 진행은 간단합니다.

1. "http://localhost:3000/check"라는 URL로 POST방식을 사용해서 id와 link 파라미터를 전달한다.

2. 전달된 link(url)에 있는 내용을 읽어와서 cheerio를 이용해 원하는 내용만 읽어낸다.

3. 읽어낸 내용을 return 한다(여기서는 frontend로 데이터를 보내준다).

 

cheerio 라이브러리는 jQuery를 써본 분들은 DOM에 접근하는 방식이 같아서 무난하게 사용할 수 있습니다. 그래도 처음 접하는 분들을 위해 설명을 붙여보자면

 

 제가 목이 말라서 방금 쿠팡에서 "물"을 검색해서 상품을 하나 골랐습니다. 이 상품의 가격에 접근하는 방법은 크롬 개발자 도구를 이용하면 더 쉽습니다. Elements 탭 가장 하단을 보시면 자기가 클릭한 내용이 어떤 트리로 구성되어있는지 쉽게 보여줍니다.

 

srv/apis/checkProducts.js

let $salePrice = $("div.prod-sale-price span.total-price").text().replace(/[^0-9]/g,'')

 이 부분만 떼어서 보겠습니다. 이 물의 가격에 접근할 때 span.total-price를 통해 접근 가능한데, 혹시 다른 곳에서 같은 태그와 클래스(span태그와 total-price클래스)를 사용할지도 모르니 더 정확하게 상위 태그와 클래스(div.prod-sale-price)를 추가했습니다.

 

 선택된 내용에서 텍스트만 추출(text())해서 따옴표("), 쉼표(,) 같은 녀석들은 정규표현식(replace(/[^0-9]/g,''))을 이용해서 걸러내 줍니다(정규표현식은 구글에서 검색하면 고수들이 알려주니 여기서는 외우거나 이해할 필요는 없습니다. 저도 몰라요).

 

 자 이렇게 구현은 끝났습니다. 정말이지 nodeJS는 정말 강력한 것 같습니다. 제가 들이는 업무의 시간이 1/10로 줄어들었거든요. 앞으로 확인해야 할 상품이 늘어날 걸 예상한다면 시간을 더 아끼는 셈이 되겠네요.

 

 여러분은 크롤링(스크래핑)을 이용해서 해결할 일이 있으신가요?

 

링크

① 웹 크롤링 해보기 #1 - NodeJS, cheerio (feat. VueJS)

 웹 크롤링 해보기 #2 - NodeJS, puppeteer


역시 코딩은 즐겁고, 결과물은 저를 행복하게 하는군요.
데스크탑 앱 만들기 마지막 포스팅


링크

① 데스크탑 앱 만들기 #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

 

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

 

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

 

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


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

 

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