Ver3.0 ⚡ Electron으로 데스크톱 앱 만들기 완벽 가이드

⚡ Electron으로 데스크톱 앱 만들기 완벽 가이드
웹 기술로 네이티브 앱처럼! 성능 최적화까지 한 번에 마스터하기 🚀
💡 이런 고민 해본 적 있지?
"웹 개발은 할 줄 아는데, 데스크톱 앱도 만들 수 있을까?"
"Windows, Mac, Linux 각각 따로 개발해야 하나?"
"Electron으로 만들면 느리다던데 진짜야?"
오늘은 이 모든 궁금증을 시원하게 해결해줄게! Electron을 활용한 데스크톱 앱 개발부터 성능 최적화까지, 실전에서 바로 써먹을 수 있는 노하우를 친구처럼 편하게 알려줄게. 😊
🎯 Electron이 뭐길래?
Electron은 웹 기술(HTML, CSS, JavaScript)로 데스크톱 애플리케이션을 만들 수 있게 해주는 프레임워크야. GitHub에서 개발했고, 지금은 OpenJS Foundation에서 관리하고 있어.
가장 큰 매력은 뭐냐고? 바로 크로스 플랫폼이라는 거지! 코드 한 번 작성하면 Windows, macOS, Linux에서 모두 동작하는 앱을 만들 수 있어. 🎉
실제로 우리가 매일 쓰는 앱들 중에도 Electron으로 만들어진 게 엄청 많아:
🌟 Electron으로 만들어진 유명 앱들
• Visual Studio Code - 개발자들의 필수 에디터
• Slack - 협업 메신저의 대명사
• Discord - 게이머들의 소통 공간
• Notion - 올인원 워크스페이스
• Figma Desktop - 디자이너들의 협업 도구
• Spotify Desktop - 음악 스트리밍 서비스
• Twitch Desktop - 스트리밍 플랫폼
이 정도면 Electron의 파워를 실감할 수 있지? 💪
🏗️ Electron의 핵심 구조 이해하기
Electron을 제대로 활용하려면 먼저 구조를 이해해야 해. 크게 두 가지 프로세스로 나뉘어져 있어:
1. Main Process (메인 프로세스)
애플리케이션의 진입점이자 관리자 역할을 해. Node.js 환경에서 실행되기 때문에 파일 시스템 접근, 네이티브 API 호출, 창(Window) 생성 및 관리 같은 시스템 레벨 작업을 담당해.
2. Renderer Process (렌더러 프로세스)
실제 UI를 그리는 프로세스야. Chromium 브라우저 환경에서 실행되고, HTML/CSS/JavaScript로 화면을 구성해. 각 창(BrowserWindow)마다 독립적인 렌더러 프로세스가 생성돼.
이 두 프로세스는 IPC(Inter-Process Communication)를 통해 서로 통신해. 마치 프론트엔드와 백엔드가 API로 통신하는 것처럼 말이야! 🔄
💡 Pro Tip: Main Process는 단 하나만 존재하지만, Renderer Process는 여러 개가 동시에 실행될 수 있어. 각 창이나 웹뷰마다 별도의 렌더러 프로세스가 생성되는 거지. 이런 구조 덕분에 한 창에서 문제가 생겨도 다른 창에는 영향을 주지 않아!
🚀 첫 Electron 앱 만들기
이론은 이 정도면 충분해! 이제 직접 만들어보자. 생각보다 훨씬 간단해. 😎
Step 1: 프로젝트 초기화
mkdir my-electron-app
cd my-electron-app
npm init -y
npm install electron --save-dev
Step 2: package.json 수정
package.json 파일을 열어서 다음 내용을 추가해줘:
{
"name": "my-electron-app",
"version": "1.0.0",
"description": "나의 첫 Electron 앱",
"main": "main.js",
"scripts": {
"start": "electron .",
"dev": "electron . --inspect=5858"
},
"keywords": ["electron", "desktop"],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"electron": "^28.0.0"
}
}
Step 3: Main Process 파일 생성 (main.js)
const { app, BrowserWindow } = require('electron');
const path = require('path');
// 창 생성 함수
function createWindow() {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false, // 보안을 위해 false
contextIsolation: true // 보안을 위해 true
}
});
// HTML 파일 로드
mainWindow.loadFile('index.html');
// 개발자 도구 열기 (개발 중에만)
// mainWindow.webContents.openDevTools();
}
// 앱이 준비되면 창 생성
app.whenReady().then(() => {
createWindow();
// macOS에서 독에서 클릭했을 때 창 재생성
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
// 모든 창이 닫히면 앱 종료 (macOS 제외)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
Step 4: Preload Script 생성 (preload.js)
보안을 위해 렌더러 프로세스와 메인 프로세스 사이에 안전한 다리를 만들어줘:
const { contextBridge, ipcRenderer } = require('electron');
// 렌더러 프로세스에서 사용할 API 노출
contextBridge.exposeInMainWorld('electronAPI', {
// 메인 프로세스로 메시지 보내기
sendMessage: (channel, data) => {
const validChannels = ['toMain'];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
// 메인 프로세스에서 메시지 받기
onMessage: (channel, callback) => {
const validChannels = ['fromMain'];
if (validChannels.includes(channel)) {
ipcRenderer.on(channel, (event, ...args) => callback(...args));
}
}
});
Step 5: HTML 파일 생성 (index.html)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
<title>나의 첫 Electron 앱</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
padding: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-align: center;
}
h1 { font-size: 3em; margin-bottom: 20px; }
button {
padding: 15px 30px;
font-size: 1.2em;
background: white;
color: #667eea;
border: none;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s;
}
button:hover { transform: scale(1.05); }
</style>
</head>
<body>
<h1>🎉 Hello Electron!</h1>
<p>웹 기술로 만든 데스크톱 앱입니다!</p>
<button id="myBtn">클릭해보세요!</button>
<p id="result"></p>
<script>
document.getElementById('myBtn').addEventListener('click', () => {
document.getElementById('result').textContent =
'버튼이 클릭되었습니다! 🚀';
});
</script>
</body>
</html>
Step 6: 실행하기
npm start
짜잔! 🎊 이제 네 첫 Electron 앱이 실행될 거야. 창이 뜨면서 예쁜 그라디언트 배경에 버튼이 보일 거야. 클릭해보면 메시지가 나타나지?
이게 끝이야! 정말 간단하지? 이제 이 기본 구조를 바탕으로 더 복잡한 기능들을 추가해나가면 돼.
⚠️ 주의사항: nodeIntegration: false와 contextIsolation: true 설정은 보안을 위해 매우 중요해! 이 설정들이 없으면 렌더러 프로세스에서 Node.js의 모든 기능에 접근할 수 있어서 XSS 공격에 취약해질 수 있어. 항상 preload 스크립트를 통해 필요한 API만 선택적으로 노출하는 게 안전해! 🔒
🔄 IPC 통신 마스터하기
Electron 앱에서 가장 중요한 개념 중 하나가 바로 IPC(Inter-Process Communication)야. 메인 프로세스와 렌더러 프로세스가 서로 대화하는 방법이지.
IPC 통신에는 크게 세 가지 패턴이 있어:
📡 IPC 통신 패턴
1. 단방향 통신 (Renderer → Main)
렌더러에서 메인으로 메시지만 보내고 응답은 받지 않아.
2. 양방향 통신 (Request-Response)
렌더러에서 요청을 보내고 메인에서 응답을 받아.
3. 메인에서 렌더러로 푸시
메인 프로세스에서 렌더러로 일방적으로 메시지를 보내.
실제 코드로 각 패턴을 살펴볼까?
패턴 1: 단방향 통신
// main.js
const { ipcMain } = require('electron');
ipcMain.on('save-file', (event, data) => {
console.log('파일 저장 요청:', data);
// 파일 저장 로직 실행
fs.writeFileSync('data.json', JSON.stringify(data));
});
// renderer.js (preload를 통해 노출된 API 사용)
window.electronAPI.sendMessage('save-file', {
name: '문서.txt',
content: '내용'
});
패턴 2: 양방향 통신 (가장 많이 사용!)
// main.js
const { ipcMain } = require('electron');
ipcMain.handle('load-file', async (event, filename) => {
try {
const data = fs.readFileSync(filename, 'utf8');
return { success: true, data: data };
} catch (error) {
return { success: false, error: error.message };
}
});
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
loadFile: (filename) => ipcRenderer.invoke('load-file', filename)
});
// renderer.js
async function loadMyFile() {
const result = await window.electronAPI.loadFile('data.json');
if (result.success) {
console.log('파일 내용:', result.data);
} else {
console.error('에러:', result.error);
}
}
패턴 3: 메인에서 렌더러로 푸시
// main.js
function sendNotification(message) {
const allWindows = BrowserWindow.getAllWindows();
allWindows.forEach(window => {
window.webContents.send('notification', message);
});
}
// 5초마다 알림 보내기
setInterval(() => {
sendNotification('새로운 메시지가 도착했습니다!');
}, 5000);
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
onNotification: (callback) => {
ipcRenderer.on('notification', (event, message) => {
callback(message);
});
}
});
// renderer.js
window.electronAPI.onNotification((message) => {
alert(message);
});
💡 IPC 베스트 프랙티스:
1. 채널 이름 관리: 채널 이름을 상수로 관리해서 오타를 방지해
2. 에러 핸들링: 항상 try-catch로 에러를 잡고 적절히 처리해
3. 타입 체크: 받은 데이터의 타입을 검증해서 예상치 못한 에러를 방지해
4. 보안: preload 스크립트에서 화이트리스트 방식으로 채널을 제한해
5. 성능: 큰 데이터는 직렬화 비용이 크니까 필요한 데이터만 주고받아
⚡ 성능 최적화 전략 - 실전편
자, 이제 가장 중요한 부분이야! Electron 앱이 "무겁다", "느리다"는 말을 많이 들어봤을 거야. 하지만 제대로 최적화하면 네이티브 앱 못지않은 성능을 낼 수 있어! 💪
1. 메모리 관리 최적화
Electron 앱의 가장 큰 문제는 메모리 사용량이야. Chromium과 Node.js를 함께 실행하니까 기본적으로 메모리를 많이 먹거든. 하지만 걱정 마! 해결 방법이 있어.
🎯 메모리 최적화 체크리스트
① 불필요한 창 닫기
사용하지 않는 BrowserWindow는 즉시 닫아줘. 각 창마다 별도의 렌더러 프로세스가 생성되니까 메모리를 많이 차지해.
// 창 닫을 때 메모리 해제
let myWindow = null;
function closeWindow() {
if (myWindow) {
myWindow.close();
myWindow = null; // 참조 제거로 가비지 컬렉션 유도
}
}
② 이미지 최적화
큰 이미지는 앱을 느리게 만드는 주범이야. 적절한 크기로 리사이징하고 WebP 같은 효율적인 포맷을 사용해.
// Sharp 라이브러리로 이미지 최적화
const sharp = require('sharp');
async function optimizeImage(inputPath, outputPath) {
await sharp(inputPath)
.resize(800, 600, { fit: 'inside' })
.webp({ quality: 80 })
.toFile(outputPath);
}
③ 메모리 누수 방지
이벤트 리스너를 제거하지 않으면 메모리 누수가 발생해. 항상 cleanup을 잊지 마!
// 나쁜 예 ❌
window.addEventListener('resize', handleResize);
// 좋은 예 ✅
const handleResize = () => { /* ... */ };
window.addEventListener('resize', handleResize);
// 컴포넌트 언마운트 시
window.removeEventListener('resize', handleResize);
2. 렌더링 성능 최적화
UI가 버벅거리면 사용자 경험이 최악이 되지. 부드러운 60fps를 유지하는 게 목표야!
🚀 렌더링 최적화 기법
① Virtual Scrolling 구현
긴 리스트를 렌더링할 때는 보이는 부분만 렌더링해. React-Window나 React-Virtualized 같은 라이브러리를 활용하면 좋아.
// React-Window 예시
import { FixedSizeList } from 'react-window';
const MyList = ({ items }) => (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
{items[index]}
</div>
)}
</FixedSizeList>
);
② CSS 애니메이션 최적화
transform과 opacity만 사용해서 애니메이션을 만들면 GPU 가속을 받을 수 있어.
/* 나쁜 예 ❌ - Reflow 발생 */
.box {
transition: width 0.3s, height 0.3s;
}
/* 좋은 예 ✅ - GPU 가속 */
.box {
transition: transform 0.3s, opacity 0.3s;
will-change: transform;
}
③ 디바운싱과 쓰로틀링
빈번한 이벤트는 성능을 크게 저하시켜. 적절히 제어해줘야 해.
// 디바운스 - 마지막 호출만 실행
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
// 쓰로틀 - 일정 시간마다 한 번만 실행
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 사용 예시
const handleSearch = debounce((query) => {
// 검색 로직
}, 300);
const handleScroll = throttle(() => {
// 스크롤 로직
}, 100);
3. 앱 시작 속도 최적화
첫인상이 중요하잖아? 앱이 빠르게 시작되면 사용자 만족도가 크게 올라가!
⚡ 시작 속도 개선 방법
① 지연 로딩(Lazy Loading)
필요한 모듈만 먼저 로드하고, 나머지는 나중에 로드해.
// 나쁜 예 ❌ - 모든 모듈을 처음에 로드
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { dialog } = require('electron');
// 좋은 예 ✅ - 필요할 때만 로드
function saveFile() {
const fs = require('fs'); // 여기서 로드
const { dialog } = require('electron');
// 파일 저장 로직
}
② V8 스냅샷 활용
자주 사용하는 코드를 미리 컴파일해서 스냅샷으로 만들면 시작 속도가 빨라져.
// electron-builder 설정
{
"build": {
"electronCompile": true,
"asar": true,
"asarUnpack": ["**/*.node"]
}
}
③ Splash Screen 활용
앱이 로딩되는 동안 예쁜 스플래시 화면을 보여주면 체감 속도가 빨라져.
let splashWindow;
let mainWindow;
function createSplashWindow() {
splashWindow = new BrowserWindow({
width: 400,
height: 300,
transparent: true,
frame: false,
alwaysOnTop: true
});
splashWindow.loadFile('splash.html');
}
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
show: false // 준비될 때까지 숨김
});
mainWindow.loadFile('index.html');
// 준비되면 스플래시 닫고 메인 윈도우 표시
mainWindow.once('ready-to-show', () => {
setTimeout(() => {
splashWindow.close();
mainWindow.show();
}, 500);
});
}
app.whenReady().then(() => {
createSplashWindow();
setTimeout(createMainWindow, 1000);
});
4. 번들 크기 최적화
앱 크기가 작을수록 다운로드도 빠르고 설치도 빠르지!
📦 번들 크기 줄이기
① Tree Shaking
사용하지 않는 코드는 번들에서 제거해.
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
minimize: true
}
};
② 외부 의존성 최소화
무거운 라이브러리는 가벼운 대안으로 교체해.
// 나쁜 예 ❌ - moment.js (무거움)
const moment = require('moment');
// 좋은 예 ✅ - date-fns (가벼움)
const { format } = require('date-fns');
// 더 좋은 예 ✅✅ - 네이티브 API 사용
const date = new Date().toLocaleDateString();
③ ASAR 아카이브 사용
파일들을 하나의 아카이브로 묶으면 I/O 성능이 향상돼.
// package.json
{
"build": {
"asar": true,
"asarUnpack": [
"**/node_modules/sharp/**/*",
"**/node_modules/sqlite3/**/*"
]
}
}
🔧 고급 최적화 테크닉
기본적인 최적화는 다 했어? 그럼 이제 진짜 고수들이 쓰는 고급 테크닉을 알려줄게! 🎓
1. 멀티 프로세스 활용
무거운 작업은 별도의 프로세스로 분리해서 메인 UI가 멈추지 않게 해야 해.
🔄 Worker Threads 활용
// worker.js
const { parentPort } = require('worker_threads');
parentPort.on('message', (data) => {
// 무거운 계산 작업
const result = heavyComputation(data);
parentPort.postMessage(result);
});
function heavyComputation(data) {
// 복잡한 데이터 처리
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += Math.sqrt(i);
}
return result;
}
// main.js
const { Worker } = require('worker_threads');
function runHeavyTask(data) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js');
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
worker.postMessage(data);
});
}
// 사용
ipcMain.handle('heavy-task', async (event, data) => {
const result = await runHeavyTask(data);
return result;
});
2. 네이티브 모듈 활용
정말 성능이 중요한 부분은 C++로 작성한 네이티브 모듈을 사용할 수 있어. Node.js의 N-API를 활용하면 돼.
⚙️ 네이티브 모듈 예시
// binding.gyp
{
"targets": [
{
"target_name": "native_module",
"sources": [ "native_module.cc" ],
"include_dirs": [
"<!(node -e \"require('nan')\")"
]
}
]
}
// native_module.cc
#include <nan.h>
void FastCalculation(const Nan::FunctionCallbackInfo<v8::Value>& info) {
if (info.Length() < 1) {
Nan::ThrowTypeError("Wrong number of arguments");
return;
}
double value = info[0]->NumberValue();
double result = 0;
// 초고속 계산
for (int i = 0; i < 1000000; i++) {
result += sqrt(value * i);
}
info.GetReturnValue().Set(Nan::New(result));
}
void Init(v8::Local<v8::Object> exports) {
exports->Set(Nan::New("fastCalculation").ToLocalChecked(),
Nan::New<v8::FunctionTemplate>(FastCalculation)
->GetFunction());
}
NODE_MODULE(native_module, Init)
// JavaScript에서 사용
const nativeModule = require('./build/Release/native_module');
const result = nativeModule.fastCalculation(42);
3. 데이터베이스 최적화
로컬 데이터를 다룰 때는 적절한 데이터베이스를 선택하는 게 중요해.
💾 SQLite 최적화 팁
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('myapp.db');
// 1. WAL 모드 활성화 (Write-Ahead Logging)
db.run('PRAGMA journal_mode = WAL');
// 2. 동기화 모드 조정 (성능 vs 안정성)
db.run('PRAGMA synchronous = NORMAL');
// 3. 캐시 크기 증가
db.run('PRAGMA cache_size = -64000'); // 64MB
// 4. 트랜잭션 사용으로 대량 삽입 최적화
db.serialize(() => {
db.run('BEGIN TRANSACTION');
const stmt = db.prepare('INSERT INTO users VALUES (?, ?, ?)');
for (let i = 0; i < 10000; i++) {
stmt.run(i, `user${i}`, `email${i}@test.com`);
}
stmt.finalize();
db.run('COMMIT');
});
// 5. 인덱스 생성
db.run('CREATE INDEX IF NOT EXISTS idx_user_email ON users(email)');
// 6. VACUUM으로 데이터베이스 최적화 (주기적으로)
db.run('VACUUM');
IndexedDB 활용 (렌더러 프로세스)
// 대용량 데이터는 IndexedDB 사용
class DataStore {
constructor() {
this.dbName = 'MyAppDB';
this.version = 1;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 객체 저장소 생성
if (!db.objectStoreNames.contains('documents')) {
const store = db.createObjectStore('documents',
{ keyPath: 'id', autoIncrement: true });
store.createIndex('title', 'title', { unique: false });
store.createIndex('date', 'date', { unique: false });
}
};
});
}
async saveDocument(doc) {
const tx = this.db.transaction(['documents'], 'readwrite');
const store = tx.objectStore('documents');
return new Promise((resolve, reject) => {
const request = store.add(doc);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getDocuments() {
const tx = this.db.transaction(['documents'], 'readonly');
const store = tx.objectStore('documents');
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
// 사용
const dataStore = new DataStore();
await dataStore.init();
await dataStore.saveDocument({
title: '문서1',
content: '내용...',
date: new Date()
});
🎨 UI/UX 최적화
성능도 중요하지만 사용자 경험도 똑같이 중요해! 빠르면서도 예쁜 앱을 만들어보자. ✨
1. 프레임워크 선택
Electron과 함께 사용할 수 있는 프레임워크가 많아. 각각 장단점이 있으니 프로젝트에 맞게 선택해!
| 프레임워크 | 장점 | 단점 | 추천 용도 |
|---|---|---|---|
| React | 생태계 방대, 컴포넌트 재사용성 | 번들 크기 큼 | 복잡한 대시보드, SaaS 앱 |
| Vue | 학습 곡선 낮음, 가벼움 | 생태계 React보다 작음 | 중소규모 앱, 빠른 개발 |
| Svelte | 매우 가벼움, 빠른 성능 | 생태계 작음, 인력 구하기 어려움 | 성능이 중요한 앱 |
| Vanilla JS | 가장 가벼움, 완전한 제어 | 개발 속도 느림 | 간단한 유틸리티 앱 |
2. 반응형 디자인
데스크톱 앱이라고 해서 창 크기가 고정된 건 아니야. 사용자가 창 크기를 조절할 수 있으니 반응형으로 만들어야 해!
📱 반응형 레이아웃 예시
// CSS Grid로 반응형 레이아웃
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
padding: 20px;
}
// 미디어 쿼리로 세밀한 조정
@media (max-width: 768px) {
.sidebar {
display: none;
}
.main-content {
grid-column: 1 / -1;
}
}
@media (min-width: 1200px) {
.container {
grid-template-columns: 250px 1fr 300px;
}
}
// JavaScript로 창 크기 감지
window.addEventListener('resize', debounce(() => {
const width = window.innerWidth;
if (width < 768) {
// 모바일 레이아웃
document.body.classList.add('mobile');
} else {
document.body.classList.remove('mobile');
}
}, 250));
3. 다크 모드 지원
요즘 다크 모드는 필수야! 시스템 테마를 자동으로 감지해서 적용하는 방법을 알려줄게.
🌙 다크 모드 구현
// main.js - 시스템 테마 감지
const { nativeTheme } = require('electron');
function setupTheme() {
// 시스템 테마 변경 감지
nativeTheme.on('updated', () => {
const isDark = nativeTheme.shouldUseDarkColors;
// 모든 창에 테마 변경 알림
BrowserWindow.getAllWindows().forEach(window => {
window.webContents.send('theme-changed', isDark);
});
});
}
// preload.js
contextBridge.exposeInMainWorld('themeAPI', {
isDarkMode: () => {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
},
onThemeChange: (callback) => {
ipcRenderer.on('theme-changed', (event, isDark) => {
callback(isDark);
});
}
});
// renderer.js
// CSS 변수로 테마 관리
const root = document.documentElement;
function applyTheme(isDark) {
if (isDark) {
root.style.setProperty('--bg-color', '#1e1e1e');
root.style.setProperty('--text-color', '#ffffff');
root.style.setProperty('--border-color', '#404040');
} else {
root.style.setProperty('--bg-color', '#ffffff');
root.style.setProperty('--text-color', '#000000');
root.style.setProperty('--border-color', '#e0e0e0');
}
}
// 초기 테마 적용
applyTheme(window.themeAPI.isDarkMode());
// 테마 변경 감지
window.themeAPI.onThemeChange((isDark) => {
applyTheme(isDark);
});
// CSS
:root {
--bg-color: #ffffff;
--text-color: #000000;
--border-color: #e0e0e0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
}
.card {
border: 1px solid var(--border-color);
background-color: var(--bg-color);
}
🔐 보안 강화하기
Electron 앱은 웹 기술을 사용하기 때문에 보안에 특히 신경 써야 해. 해커들이 노리는 취약점들을 막아보자! 🛡️
1. Context Isolation 활성화
이건 정말 중요해! 렌더러 프로세스와 preload 스크립트를 격리시켜서 보안을 강화하는 거야.
⚠️ 보안 체크리스트
✅ 반드시 해야 할 것들:
1. nodeIntegration: false - Node.js 통합 비활성화
2. contextIsolation: true - 컨텍스트 격리 활성화
3. sandbox: true - 샌드박스 모드 활성화
4. webSecurity: true - 웹 보안 활성화 (기본값)
5. allowRunningInsecureContent: false - 안전하지 않은 콘텐츠 차단
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false, // ✅
contextIsolation: true, // ✅
sandbox: true, // ✅
webSecurity: true, // ✅
allowRunningInsecureContent: false, // ✅
enableRemoteModule: false, // ✅
webviewTag: false // ✅ (필요한 경우만 true)
}
});
2. Content Security Policy (CSP)
XSS 공격을 막기 위한 필수 설정이야!
🔒 CSP 설정
// index.html
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;">
// 또는 main.js에서 설정
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'; script-src 'self'"
]
}
});
});
3. 안전한 외부 링크 처리
사용자가 외부 링크를 클릭했을 때 앱 내부가 아닌 기본 브라우저에서 열리게 해야 해.
🔗 외부 링크 안전하게 처리하기
// main.js
const { shell } = require('electron');
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
// 외부 URL은 기본 브라우저에서 열기
if (url.startsWith('http://') || url.startsWith('https://')) {
shell.openExternal(url);
return { action: 'deny' };
}
// 내부 URL은 허용
return { action: 'allow' };
});
// 네비게이션 제한
mainWindow.webContents.on('will-navigate', (event, url) => {
const allowedDomains = ['myapp.com', 'api.myapp.com'];
const urlObj = new URL(url);
if (!allowedDomains.includes(urlObj.hostname)) {
event.preventDefault();
shell.openExternal(url);
}
});
📦 빌드와 배포
앱을 다 만들었으면 이제 배포할 차례야! Electron Builder를 사용하면 쉽게 설치 파일을 만들 수 있어. 🚀
1. Electron Builder 설정
⚙️ electron-builder 설치 및 설정
npm install electron-builder --save-dev
// package.json
{
"name": "my-awesome-app",
"version": "1.0.0",
"description": "멋진 Electron 앱",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder",
"build:win": "electron-builder --win",
"build:mac": "electron-builder --mac",
"build:linux": "electron-builder --linux"
},
"build": {
"appId": "com.mycompany.myapp",
"productName": "My Awesome App",
"copyright": "Copyright © 2024 My Company",
// 파일 포함/제외
"files": [
"main.js",
"preload.js",
"renderer/**/*",
"assets/**/*",
"node_modules/**/*"
],
"extraResources": [
{
"from": "resources",
"to": "resources",
"filter": ["**/*"]
}
],
// Windows 설정
"win": {
"target": ["nsis", "portable"],
"icon": "assets/icon.ico",
"publisherName": "My Company"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "My Awesome App"
},
// macOS 설정
"mac": {
"target": ["dmg", "zip"],
"icon": "assets/icon.icns",
"category": "public.app-category.productivity",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist"
},
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
]
},
// Linux 설정
"linux": {
"target": ["AppImage", "deb", "rpm"],
"icon": "assets/icon.png",
"category": "Utility"
}
}
}
2. 자동 업데이트 구현
사용자가 항상 최신 버전을 사용할 수 있도록 자동 업데이트 기능을 추가해보자!
🔄 Auto Updater 설정
npm install electron-updater --save
// main.js
const { autoUpdater } = require('electron-updater');
const log = require('electron-log');
// 로그 설정
autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = 'info';
// 업데이트 체크
function checkForUpdates() {
autoUpdater.checkForUpdatesAndNotify();
}
// 앱 준비 후 업데이트 체크
app.whenReady().then(() => {
createWindow();
// 5초 후 업데이트 체크 (앱이 완전히 로드된 후)
setTimeout(checkForUpdates, 5000);
// 1시간마다 업데이트 체크
setInterval(checkForUpdates, 60 * 60 * 1000);
});
// 업데이트 이벤트 처리
autoUpdater.on('checking-for-update', () => {
log.info('업데이트 확인 중...');
});
autoUpdater.on('update-available', (info) => {
log.info('업데이트 가능:', info);
// 사용자에게 알림
dialog.showMessageBox({
type: 'info',
title: '업데이트 가능',
message: '새로운 버전이 있습니다. 다운로드하시겠습니까?',
buttons: ['다운로드', '나중에']
}).then(result => {
if (result.response === 0) {
autoUpdater.downloadUpdate();
}
});
});
autoUpdater.on('update-not-available', (info) => {
log.info('최신 버전 사용 중:', info);
});
autoUpdater.on('error', (err) => {
log.error('업데이트 에러:', err);
});
autoUpdater.on('download-progress', (progressObj) => {
let message = `다운로드 속도: ${progressObj.bytesPerSecond}`;
message += ` - ${progressObj.percent}% 완료`;
message += ` (${progressObj.transferred}/${progressObj.total})`;
log.info(message);
// 진행 상황을 렌더러에 전송
mainWindow.webContents.send('download-progress', progressObj);
});
autoUpdater.on('update-downloaded', (info) => {
log.info('업데이트 다운로드 완료:', info);
dialog.showMessageBox({
type: 'info',
title: '업데이트 준비 완료',
message: '업데이트가 다운로드되었습니다. 지금 재시작하시겠습니까?',
buttons: ['재시작', '나중에']
}).then(result => {
if (result.response === 0) {
autoUpdater.quitAndInstall();
}
});
});
// package.json에 publish 설정 추가
{
"build": {
"publish": [
{
"provider": "github",
"owner": "your-username",
"repo": "your-repo"
}
]
}
}
🧪 테스팅과 디버깅
버그 없는 앱을 만들려면 철저한 테스트가 필수야! Electron 앱을 테스트하는 방법을 알아보자. 🔍
1. 단위 테스트
🧪 Jest로 단위 테스트 작성
npm install --save-dev jest @types/jest
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "coverage",
"collectCoverageFrom": [
"src/**/*.js",
"!src/**/*.test.js"
]
}
}
// utils.test.js
const { formatDate, calculateTotal } = require('./utils');
describe('Utils 함수 테스트', () => {
test('날짜 포맷팅', () => {
const date = new Date('2024-01-15');
expect(formatDate(date)).toBe('2024-01-15');
});
test('합계 계산', () => {
const items = [
{ price: 1000, quantity: 2 },
{ price: 2000, quantity: 1 }
];
expect(calculateTotal(items)).toBe(4000);
});
test('빈 배열 처리', () => {
expect(calculateTotal([])).toBe(0);
});
});
2. E2E 테스트
실제 사용자처럼 앱을 테스트해보자! Spectron을 사용하면 돼.
🎭 Spectron으로 E2E 테스트
npm install --save-dev spectron
// e2e.test.js
const Application = require('spectron').Application;
const path = require('path');
describe('Electron 앱 E2E 테스트', () => {
let app;
beforeEach(async () => {
app = new Application({
path: path.join(__dirname, '../node_modules/.bin/electron'),
args: [path.join(__dirname, '../main.js')],
env: { NODE_ENV: 'test' }
});
await app.start();
});
afterEach(async () => {
if (app && app.isRunning()) {
await app.stop();
}
});
test('앱이 정상적으로 시작됨', async () => {
const isVisible = await app.browserWindow.isVisible();
expect(isVisible).toBe(true);
});
test('창 제목이 올바름', async () => {
const title = await app.client.getTitle();
expect(title).toBe('My Awesome App');
});
test('버튼 클릭 동작', async () => {
await app.client.click('#myButton');
const text = await app.client.getText('#result');
expect(text).toBe('버튼이 클릭되었습니다!');
});
test('메뉴 동작', async () => {
await app.client.keys(['Control', 'n']);
const windowCount = await app.client.getWindowCount();
expect(windowCount).toBe(2);
});
});
3. 디버깅 팁
🐛 효과적인 디버깅 방법
1. DevTools 활용
// 개발 중에는 자동으로 DevTools 열기
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools();
}
// 특정 키 조합으로 DevTools 토글
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.control && input.shift && input.key.toLowerCase() === 'i') {
mainWindow.webContents.toggleDevTools();
}
});
2. 로깅 시스템
const log = require('electron-log');
// 로그 레벨 설정
log.transports.file.level = 'info';
log.transports.console.level = 'debug';
// 로그 파일 위치
console.log(log.transports.file.getFile().path);
// 사용
log.info('앱 시작됨');
log.warn('경고 메시지');
log.error('에러 발생:', error);
// 커스텀 로그 포맷
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}] [{level}] {text}';
3. 원격 디버깅
// main.js에서 디버그 포트 열기
app.commandLine.appendSwitch('remote-debugging-port', '9222');
// Chrome에서 chrome://inspect 접속하여 디버깅
🌟 실전 프로젝트 예제
이론만 배우면 재미없지! 실제로 만들어볼 수 있는 프로젝트 아이디어를 몇 가지 소개할게. 💡
프로젝트 1: 마크다운 에디터
간단하면서도 실용적인 마크다운 에디터를 만들어보자!
📝 마크다운 에디터 핵심 기능
• 실시간 미리보기
• 파일 저장/불러오기
• 다크 모드 지원
• 단축키 지원
• PDF 내보내기
// renderer.js
const marked = require('marked');
const hljs = require('highlight.js');
// 마크다운 설정
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
}
return hljs.highlightAuto(code).value;
},
breaks: true,
gfm: true
});
const editor = document.getElementById('editor');
const preview = document.getElementById('preview');
// 실시간 미리보기
editor.addEventListener('input', debounce(() => {
const markdown = editor.value;
const html = marked.parse(markdown);
preview.innerHTML = html;
}, 300));
// 파일 저장
document.getElementById('saveBtn').addEventListener('click', async () => {
const content = editor.value;
const result = await window.electronAPI.saveFile(content);
if (result.success) {
showNotification('파일이 저장되었습니다!');
}
});
// 파일 열기
document.getElementById('openBtn').addEventListener('click', async () => {
const result = await window.electronAPI.openFile();
if (result.success) {
editor.value = result.content;
editor.dispatchEvent(new Event('input'));
}
});
// PDF 내보내기
document.getElementById('exportBtn').addEventListener('click', async () => {
const html = preview.innerHTML;
await window.electronAPI.exportPDF(html);
});
프로젝트 2: 시스템 모니터링 대시보드
CPU, 메모리, 디스크 사용량을 실시간으로 모니터링하는 앱!
📊 시스템 모니터 핵심 기능
// main.js
const si = require('systeminformation');
// 시스템 정보 수집
async function getSystemInfo() {
const [cpu, mem, disk, network] = await Promise.all([
si.currentLoad(),
si.mem(),
si.fsSize(),
si.networkStats()
]);
return {
cpu: {
usage: cpu.currentLoad.toFixed(2),
cores: cpu.cpus.length
},
memory: {
total: (mem.total / 1024 / 1024 / 1024).toFixed(2),
used: (mem.used / 1024 / 1024 / 1024).toFixed(2),
percentage: ((mem.used / mem.total) * 100).toFixed(2)
},
disk: disk.map(d => ({
mount: d.mount,
size: (d.size / 1024 / 1024 / 1024).toFixed(2),
used: (d.used / 1024 / 1024 / 1024).toFixed(2),
percentage: d.use.toFixed(2)
})),
network: network.map(n => ({
interface: n.iface,
rx: (n.rx_sec / 1024 / 1024).toFixed(2),
tx: (n.tx_sec / 1024 / 1024).toFixed(2)
}))
};
}
// 1초마다 정보 업데이트
setInterval(async () => {
const info = await getSystemInfo();
mainWindow.webContents.send('system-info', info);
}, 1000);
// renderer.js - Chart.js로 시각화
const ctx = document.getElementById('cpuChart').getContext('2d');
const cpuChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'CPU 사용률 (%)',
data: [],
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
max: 100
}
},
animation: {
duration: 0
}
}
});
window.electronAPI.onSystemInfo((info) => {
// CPU 차트 업데이트
const now = new Date().toLocaleTimeString();
cpuChart.data.labels.push(now);
cpuChart.data.datasets[0].data.push(info.cpu.usage);
// 최근 60개 데이터만 유지
if (cpuChart.data.labels.length > 60) {
cpuChart.data.labels.shift();
cpuChart.data.datasets[0].data.shift();
}
cpuChart.update();
// 메모리 정보 업데이트
document.getElementById('memoryUsage').textContent =
`${info.memory.used} GB / ${info.memory.total} GB (${info.memory.percentage}%)`;
});
프로젝트 3: 스크린샷 & 화면 녹화 도구
화면을 캡처하고 녹화하는 유틸리티 앱!
📸 스크린샷 도구 핵심 기능
// main.js
const { desktopCapturer, screen } = require('electron');
// 화면 캡처
ipcMain.handle('capture-screen', async () => {
const sources = await desktopCapturer.getSources({
types: ['screen'],
thumbnailSize: screen.getPrimaryDisplay().workAreaSize
});
return sources[0].thumbnail.toDataURL();
});
// 특정 영역 캡처
ipcMain.handle('capture-area', async (event, bounds) => {
const sources = await desktopCapturer.getSources({
types: ['screen'],
thumbnailSize: {
width: bounds.width,
height: bounds.height
}
});
return sources[0].thumbnail.toDataURL();
});
// renderer.js
// 전체 화면 캡처
document.getElementById('captureBtn').addEventListener('click', async () => {
const dataUrl = await window.electronAPI.captureScreen();
// 이미지 표시
const img = document.createElement('img');
img.src = dataUrl;
document.getElementById('preview').appendChild(img);
// 저장 옵션 제공
showSaveDialog(dataUrl);
});
// 영역 선택 캡처
document.getElementById('areaBtn').addEventListener('click', () => {
// 반투명 오버레이 표시
const overlay = document.createElement('div');
overlay.className = 'selection-overlay';
document.body.appendChild(overlay);
let startX, startY;
overlay.addEventListener('mousedown', (e) => {
startX = e.clientX;
startY = e.clientY;
});
overlay.addEventListener('mouseup', async (e) => {
const bounds = {
x: Math.min(startX, e.clientX),
y: Math.min(startY, e.clientY),
width: Math.abs(e.clientX - startX),
height: Math.abs(e.clientY - startY)
};
const dataUrl = await window.electronAPI.captureArea(bounds);
overlay.remove();
showPreview(dataUrl);
});
});
💼 실무에서 자주 마주치는 문제와 해결법
실제로 Electron 앱을 개발하다 보면 여러 가지 문제에 부딪히게 돼. 내가 겪었던 문제들과 해결 방법을 공유할게! 🔧
문제 1: 메모리 누수
⚠️ 증상:
앱을 오래 사용하면 메모리 사용량이 계속 증가해서 결국 느려지거나 크래시가 발생해.
✅ 해결 방법:
// 1. 이벤트 리스너 정리
class MyComponent {
constructor() {
this.handleResize = this.handleResize.bind(this);
window.addEventListener('resize', this.handleResize);
}
destroy() {
// 반드시 리스너 제거!
window.removeEventListener('resize', this.handleResize);
}
handleResize() {
// 리사이즈 처리
}
}
// 2. 타이머 정리
class DataFetcher {
start() {
this.intervalId = setInterval(() => {
this.fetchData();
}, 5000);
}
stop() {
// 타이머 정리
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
}
// 3. DOM 참조 해제
let cachedElements = new Map();
function cleanup() {
cachedElements.clear();
cachedElements = null;
}
// 4. 큰 객체 null 처리
let bigData = loadBigData();
// 사용 후
bigData = null;
문제 2: 앱 시작이 너무 느림
⚠️ 증상:
앱을 실행하면 창이 뜨기까지 5초 이상 걸려.
✅ 해결 방법:
// 1. 지연 로딩 적용
// 나쁜 예
const heavyModule = require('./heavy-module');
const anotherHeavy = require('./another-heavy');
// 좋은 예
let heavyModule = null;
function getHeavyModule() {
if (!heavyModule) {
heavyModule = require('./heavy-module');
}
return heavyModule;
}
// 2. 병렬 로딩
async function initializeApp() {
// 순차 로딩 (느림) ❌
await loadDatabase();
await loadSettings();
await loadPlugins();
// 병렬 로딩 (빠름) ✅
await Promise.all([
loadDatabase(),
loadSettings(),
loadPlugins()
]);
}
// 3. 백그라운드 초기화
app.whenReady().then(() => {
// 즉시 창 표시
createWindow();
// 무거운 작업은 백그라운드에서
setTimeout(() => {
initializeHeavyFeatures();
}, 1000);
});
문제 3: 크로스 플랫폼 호환성
⚠️ 증상:
Windows에서는 잘 되는데 Mac에서는 안 돼. 또는 그 반대.
✅ 해결 방법:
// 1. 플랫폼별 분기 처리
const isMac = process.platform === 'darwin';
const isWindows = process.platform === 'win32';
const isLinux = process.platform === 'linux';
// 메뉴 설정
const template = [
// Mac에서만 앱 메뉴 추가
...(isMac ? [{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
}] : []),
// 파일 메뉴
{
label: 'File',
submenu: [
isMac ? { role: 'close' } : { role: 'quit' }
]
}
];
// 2. 경로 처리
const path = require('path');
// 나쁜 예 ❌
const filePath = 'C:\\Users\\data\\file.txt';
// 좋은 예 ✅
const filePath = path.join(app.getPath('userData'), 'file.txt');
// 3. 단축키 처리
const accelerator = isMac ? 'Cmd+Q' : 'Ctrl+Q';
// 4. 네이티브 모듈 빌드
// package.json
{
"scripts": {
"rebuild": "electron-rebuild -f -w native-module",
"postinstall": "electron-builder install-app-deps"
}
}
🎓 학습 리소스와 커뮤니티
Electron을 더 깊이 배우고 싶다면? 이런 리소스들을 활용해봐! 📚
공식 문서와 가이드
📖 필수 학습 자료
1. 공식 문서
• Electron 공식 문서: https://www.electronjs.org/docs
• API 레퍼런스가 정말 잘 되어 있어. 영어지만 예제 코드가 많아서 이해하기 쉬워.
2. 튜토리얼
• Electron Fiddle: 브라우저에서 바로 Electron 코드를 실험해볼 수 있어
• Awesome Electron: 유용한 라이브러리와 도구 모음
3. 유튜브 채널
• Traversy Media: 초보자 친화적인 튜토리얼
• Fireship: 빠르고 핵심만 담은 설명
4. 한국 커뮤니티
• 재능넷(https://www.jaenung.net)의 프로그래밍 카테고리에서도 다양한 개발자들과 지식을 나눌 수 있어!
• 개발자 커뮤니티에서 질문하고 답변받기
• GitHub Issues에서 실제 문제 해결 사례 찾아보기
추천 라이브러리
| 라이브러리 | 용도 | 특징 |
|---|---|---|
| electron-store | 설정 저장 | 간단한 key-value 저장소 |
| electron-log | 로깅 | 파일과 콘솔에 로그 기록 |
| electron-updater | 자동 업데이트 | GitHub 릴리스와 연동 |
| electron-builder | 빌드/패키징 | 크로스 플랫폼 빌드 지원 |
| electron-reload | 개발 도구 | 코드 변경 시 자동 새로고침 |
| electron-context-menu | UI | 컨텍스트 메뉴 쉽게 추가 |
| electron-dl | 다운로드 | 파일 다운로드 간편화 |
🚀 성능 벤치마킹
최적화를 했으면 결과를 측정해야지! 성능을 측정하는 방법을 알아보자. 📊
⏱️ 성능 측정 도구
// 1. 메모리 사용량 측정
function logMemoryUsage() {
const usage = process.memoryUsage();
console.log({
rss: `${(usage.rss / 1024 / 1024).toFixed(2)} MB`,
heapTotal: `${(usage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
heapUsed: `${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`,
external: `${(usage.external / 1024 / 1024).toFixed(2)} MB`
});
}
// 주기적으로 측정
setInterval(logMemoryUsage, 10000);
// 2. 실행 시간 측정
function measurePerformance(fn, label) {
const start = performance.now();
fn();
const end = performance.now();
console.log(`${label}: ${(end - start).toFixed(2)}ms`);
}
// 사용 예시
measurePerformance(() => {
// 무거운 작업
processLargeData();
}, '데이터 처리');
// 3. 렌더링 성능 측정
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
}
});
observer.observe({ entryTypes: ['measure', 'navigation'] });
// 4. Chrome DevTools Performance API
performance.mark('start-heavy-task');
heavyTask();
performance.mark('end-heavy-task');
performance.measure('heavy-task', 'start-heavy-task', 'end-heavy-task');
// 5. 앱 시작 시간 측정
const startTime = Date.now();
app.whenReady().then(() => {
createWindow();
mainWindow.once('ready-to-show', () => {
const loadTime = Date.now() - startTime;
console.log(`앱 시작 시간: ${loadTime}ms`);
// 분석 서비스로 전송
analytics.track('app-start-time', { duration: loadTime });
});
});
🎯 실전 팁 모음
마지막으로 실무에서 정말 유용한 팁들을 정리해줄게! 이것만 알아도 개발이 훨씬 수월해질 거야. 💎
💡 개발 생산성 향상 팁
1. Hot Reload 설정
// electron-reload 사용
require('electron-reload')(__dirname, {
electron: path.join(__dirname, 'node_modules', '.bin', 'electron'),
hardResetMethod: 'exit'
});
// 또는 nodemon 사용
// package.json
{
"scripts": {
"dev": "nodemon --exec electron . --watch main.js --watch preload.js"
}
}
2. 환경 변수 관리
// .env 파일
NODE_ENV=development
API_URL=https://api.example.com
DEBUG=true
// dotenv 사용
require('dotenv').config();
const isDev = process.env.NODE_ENV === 'development';
const apiUrl = process.env.API_URL;
// 렌더러에서 사용
contextBridge.exposeInMainWorld('config', {
apiUrl: process.env.API_URL,
isDev: process.env.NODE_ENV === 'development'
});
3. 에러 핸들링
// 전역 에러 핸들러
process.on('uncaughtException', (error) => {
log.error('Uncaught Exception:', error);
// 사용자에게 알림
dialog.showErrorBox('오류 발생',
'예상치 못한 오류가 발생했습니다. 앱을 재시작해주세요.');
// 크래시 리포트 전송
sendCrashReport(error);
// 앱 종료
app.quit();
});
process.on('unhandledRejection', (reason, promise) => {
log.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
// 렌더러 프로세스 크래시 감지
app.on('render-process-gone', (event, webContents, details) => {
log.error('Renderer process gone:', details);
if (details.reason === 'crashed') {
// 창 재생성
const window = BrowserWindow.fromWebContents(webContents);
if (window) {
window.reload();
}
}
});
4. 개발자 도구 단축키
// 개발 중 유용한 단축키 등록
if (isDev) {
globalShortcut.register('CommandOrControl+Shift+I', () => {
const window = BrowserWindow.getFocusedWindow();
if (window) {
window.webContents.toggleDevTools();
}
});
globalShortcut.register('CommandOrControl+R', () => {
const window = BrowserWindow.getFocusedWindow();
if (window) {
window.reload();
}
});
globalShortcut.register('F5', () => {
const window = BrowserWindow.getFocusedWindow();
if (window) {
window.reload();
}
});
}
5. 사용자 데이터 경로
// 플랫폼별 적절한 경로 사용
const userDataPath = app.getPath('userData');
const documentsPath = app.getPath('documents');
const tempPath = app.getPath('temp');
const desktopPath = app.getPath('desktop');
// 커스텀 경로 설정
app.setPath('userData', path.join(app.getPath('appData'), 'MyApp'));
// 파일 저장
const configPath = path.join(userDataPath, 'config.json');
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
🎬 마무리하며
와! 여기까지 읽었다면 정말 대단해! 👏
Electron은 처음에는 복잡해 보이지만, 한 번 익숙해지면 정말 강력한 도구야. 웹 개발 지식만 있으면 데스크톱 앱을 만들 수 있다는 게 얼마나 멋진 일이야!
핵심 정리:
1. 구조 이해가 기본 - Main Process와 Renderer Process의 역할을 명확히 알자
2. 보안은 필수 - nodeIntegration: false, contextIsolation: true는 꼭 설정하자
3. 성능 최적화는 선택이 아닌 필수 - 메모리 관리, 렌더링 최적화, 번들 크기 줄이기
4. 테스트와 디버깅 - 단위 테스트, E2E 테스트로 품질 보장
5. 실전 경험 - 작은 프로젝트부터 시작해서 점점 확장하기
Electron 개발이 처음에는 어렵게 느껴질 수 있어. 하지만 포기하지 말고 계속 도전해봐! 막히는 부분이 있으면 공식 문서를 찾아보고, 커뮤니티에 질문하고, 다른 사람들의 코드를 참고해봐.
재능넷(https://www.jaenung.net)에서도 다양한 개발자들과 지식을 나누고, 필요하면 전문가의 도움을 받을 수도 있어. 혼자 고민하지 말고 함께 성장하자! 🌱
자, 이제 당신 차례야! 오늘 배운 내용을 바탕으로 멋진 Electron 앱을 만들어봐. 시작이 반이라고, 일단 시작하면 생각보다 재미있을 거야! 💪
화이팅! 🚀✨
🎉 Electron 마스터의 길은 이제 시작이야! 🎉
계속 배우고, 실험하고, 만들어봐!
당신의 아이디어가 현실이 되는 순간을 기대할게! 😊
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.