2021년 11월 작성, 2026년 10월 보완. 당시에는 렌더러에서
ipcRenderer를 직접 쓰는 방식(nodeIntegration: true)이었다. 지금 Electron은contextIsolation이 기본이라, preload에서contextBridge로 필요한 함수만 노출하고ipcRenderer.invoke/ipcMain.handle로 주고받는 방식을 권장한다. 아래 예제는 이 방식으로 다시 썼다.
목차
개발 환경
Vue CLI Plugin Electron Builder로 Electron 앱을 개발했다.
문제 상황
앱의 한 화면에서 Axios로 파일을 받아 fs.writeFile() 로 저장했더니, 파일은 만들어지는데 내용이 깨져서 열리지 않았다.
원인
Electron에는 두 종류의 프로세스가 있다. 창(화면)을 그리는 렌더러 프로세스와, 앱 전체를 관리하고 Node.js 기능을 마음껏 쓸 수 있는 메인 프로세스다. 둘은 ipcRenderer 와 ipcMain 이라는 IPC(Inter-Process Communication) 모듈로 데이터를 주고받는다.
당시에는 렌더러 프로세스에서 파일을 받아 바로 저장하려고 한 것을 원인으로 판단했고, 저장을 메인 프로세스로 옮겨서 해결했다.
지금 돌아보면 데이터의 형식도 함께 봐야 한다. 렌더러의 Axios는 브라우저처럼 동작해서, responseType 을 지정하지 않으면 응답을 문자열로 받는다. 바이너리 파일을 문자열로 받으면 그 시점에 이미 내용이 깨진다. 그래서 responseType: "arraybuffer" 로 받고, 메인 프로세스에서 Buffer 로 바꿔 저장하는 것이 안전하다.
해결 방법
- 렌더러에서 «다운로드» 버튼을 누르면 Axios로 파일을
ArrayBuffer로 받는다. - 받은 데이터를 IPC로 메인 프로세스에 넘긴다.
- 메인 프로세스에서
Buffer로 바꿔fs.writeFile()로 저장한다.
preload.js
const { contextBridge, ipcRenderer } = require("electron");
// 렌더러에는 저장 함수 하나만 노출한다
contextBridge.exposeInMainWorld("fileApi", {
save: (fileName, data) => ipcRenderer.invoke("save-file", fileName, data),
});
렌더러
import axios from "axios";
async function download(url) {
// 바이너리는 반드시 arraybuffer로 받는다
const res = await axios.get(url, { responseType: "arraybuffer" });
const saved = await window.fileApi.save("report.pdf", res.data);
console.log(saved ? "저장 완료" : "저장 취소");
}
메인 프로세스
const { ipcMain, dialog } = require("electron");
const fs = require("fs/promises");
ipcMain.handle("save-file", async (event, fileName, data) => {
const { canceled, filePath } = await dialog.showSaveDialog({
defaultPath: fileName,
});
if (canceled || !filePath) return false;
await fs.writeFile(filePath, Buffer.from(data));
return true;
});
정리
- 렌더러와 메인 프로세스의 역할을 구분한다. 파일 저장처럼 Node.js 기능이 필요한 일은 메인 프로세스가 맡는다.
- 바이너리 파일은
responseType: "arraybuffer"로 받는다. - 렌더러는 preload가 노출한 함수로 데이터를 넘기고, 메인 프로세스에서
Buffer.from()으로 바꿔 저장한다.