데스크탑 앱 만들기 마지막 포스팅


링크

① 데스크탑 앱 만들기 #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에서의 동기, 비동기, 콜백

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

 

+ Recent posts