출시·고도화 중
Electron 안내서 · 4/6
데스크톱 앱은 설정, 최근 파일 목록, 사용자 문서처럼 컴퓨터에 남겨야 할 데이터가 많습니다. 이 장에서는 데이터를 어디에 저장할지, 파일 대화 상자와 알림 같은 네이티브 API를 어떻게 쓰는지, 그리고 렌더러와 메인 프로세스 사이에서 상태를 어떻게 나누어 관리하는지 살펴봅니다.
운영체제마다 앱 데이터를 두는 위치가 다릅니다. app.getPath()는 이 차이를 감추고 알맞은 경로를 돌려줍니다. 앱 전용 데이터는 userData에 저장하는 것이 원칙입니다.
| 이름 | 용도 |
|---|---|
userData | 앱 설정과 데이터. 기본적으로 앱 이름을 딴 폴더 |
documents | 사용자의 문서 폴더 |
downloads | 다운로드 폴더 |
temp | 임시 파일 |
logs | 앱 로그 폴더 |
const { app } = require('electron')
app.whenReady().then(() => {
console.log(app.getPath('userData'))
// Windows 예: C:\Users\<사용자>\AppData\Roaming\<앱 이름>
// macOS 예: ~/Library/Application Support/<앱 이름>
// Linux 예: ~/.config/<앱 이름>
})설치 폴더(app.getAppPath()나 실행 파일 옆)는 패키징 후 읽기 전용이거나 업데이트 때 덮어써질 수 있으므로, 사용자 데이터를 쓰는 곳으로 쓰지 않습니다.
작은 설정은 JSON 파일 하나로 충분합니다. 파일 접근은 메인 프로세스에서 하고, 렌더러에는 IPC로 읽기와 쓰기만 열어 줍니다. 쓰는 도중 앱이 꺼져도 파일이 깨지지 않도록 임시 파일에 먼저 쓰고 이름을 바꿉니다.
// main.js
const { app, ipcMain, BrowserWindow } = require('electron')
const fs = require('node:fs/promises')
const path = require('node:path')
const settingsFile = () => path.join(app.getPath('userData'), 'settings.json')
const defaults = { theme: 'system', recentFiles: [] }
async function loadSettings() {
try {
return { ...defaults, ...JSON.parse(await fs.readFile(settingsFile(), 'utf8')) }
} catch {
return { ...defaults }
}
}
async function saveSettings(settings) {
const tmp = `${settingsFile()}.tmp`
await fs.writeFile(tmp, JSON.stringify(settings, null, 2))
await fs.rename(tmp, settingsFile())
}
ipcMain.handle('settings:get', () => loadSettings())
ipcMain.handle('settings:setTheme', async (_event, theme) => {
if (!['light', 'dark', 'system'].includes(theme)) throw new Error('invalid theme')
const settings = { ...(await loadSettings()), theme }
await saveSettings(settings)
// 열려 있는 모든 창에 바뀐 설정을 알린다
for (const win of BrowserWindow.getAllWindows()) win.webContents.send('settings:changed', settings)
return settings
})더 편한 키-값 저장소가 필요하면 커뮤니티 패키지 electron-store를, 많은 데이터를 다룬다면 SQLite 같은 내장 데이터베이스를 고려합니다. better-sqlite3처럼 네이티브 모듈을 쓰면 Electron에 맞게 다시 빌드해야 하는데, Electron Forge는 이를 자동으로 처리하고, 직접 할 때는 npx @electron/rebuild를 실행합니다.
dialog 모듈은 운영체제 기본 대화 상자를 엽니다. 파일 열기(showOpenDialog), 저장(showSaveDialog), 확인 메시지(showMessageBox)가 있으며, 창을 첫 인자로 넘기면 그 창에 붙은 모달 대화 상자가 됩니다.
const { dialog } = require('electron')
ipcMain.handle('file:save', async (event, content) => {
if (typeof content !== 'string') throw new Error('invalid content')
const win = BrowserWindow.fromWebContents(event.sender)
const { canceled, filePath } = await dialog.showSaveDialog(win, {
defaultPath: path.join(app.getPath('documents'), 'note.txt'),
filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
})
if (canceled || !filePath) return false
await fs.writeFile(filePath, content, 'utf8')
return true
})메인 프로세스의 Notification은 운영체제 알림 센터에 알림을 보냅니다. 렌더러에서는 웹 표준 Notification API도 그대로 쓸 수 있습니다. Windows에서는 알림이 앱과 연결되도록 Application User Model ID가 필요하며, 설치 프로그램이 보통 이를 처리합니다. 개발 중에는 app.setAppUserModelId()로 지정할 수 있습니다.
비밀번호나 접근 토큰처럼 민감한 문자열은 safeStorage로 운영체제의 키 저장 기능을 이용해 암호화한 뒤 저장합니다.
const { Notification, safeStorage } = require('electron')
function notifyDone(fileName) {
if (Notification.isSupported()) {
new Notification({ title: '내보내기 완료', body: `${fileName} 파일을 저장했습니다.` }).show()
}
}
async function saveToken(token) {
if (!safeStorage.isEncryptionAvailable()) throw new Error('encryption unavailable')
const encrypted = safeStorage.encryptString(token) // Buffer
await fs.writeFile(path.join(app.getPath('userData'), 'token.bin'), encrypted)
}
async function loadToken() {
const encrypted = await fs.readFile(path.join(app.getPath('userData'), 'token.bin'))
return safeStorage.decryptString(encrypted)
}렌더러는 일반 웹 앱처럼 상태를 다룹니다. 화면 상태는 React, Vue, Svelte 같은 프레임워크의 상태 관리 도구로 관리하고, 작은 값은 localStorage, 큰 데이터는 IndexedDB에 둘 수 있습니다. 이 저장소들은 기본 세션에서 userData 아래에 보관되므로 앱을 다시 시작해도 남습니다.
여러 창이 같은 데이터를 보아야 한다면 메인 프로세스를 하나의 기준(source of truth) 으로 삼습니다. 렌더러는 IPC로 값을 요청하고, 바뀐 값은 메인 프로세스가 모든 창에 알립니다.
// preload.js
contextBridge.exposeInMainWorld('settings', {
get: () => ipcRenderer.invoke('settings:get'),
setTheme: (theme) => ipcRenderer.invoke('settings:setTheme', theme),
onChanged: (callback) => {
ipcRenderer.on('settings:changed', (_event, value) => callback(value))
},
})
// renderer.js
const apply = (settings) => { document.documentElement.dataset.theme = settings.theme }
window.settings.get().then(apply)
window.settings.onChanged(apply)app.getPath('userData') 아래에 저장하고, 설치 폴더에는 쓰지 않습니다.dialog, Notification, safeStorage로 대화 상자, 알림, 민감한 값 암호화를 처리합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.