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


링크

① 데스크탑 앱 만들기 #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가 중첩되지 않는 것을 확인했습니다.

 

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

 


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

 

+ Recent posts