본문 바로가기

React

React 앱에서 Electron 연동하여 데스크톱 애플리케이션 개발하기

React로 만든 웹 앱을 Electron과 결합하면 Windows, macOS, Linux용 데스크톱 애플리케이션을 하나의 코드베이스로 배포할 수 있습니다. 이 글은 실무에서 바로 쓰는 최소 설정으로 개발, IPC 연동, 보안, 패키징까지 빠르게 구축하는 방법을 안내합니다.

1. 목표와 개요

목표는 다음과 같습니다. React는 UI를 담당하고, Electron 메인 프로세스는 윈도우 관리와 OS API 접근을 담당합니다. 보안을 위해 nodeIntegration은 끄고 preload에서 필요한 API만 노출합니다. 개발 중에는 Vite 개발 서버를 열고, 배포 시 정적 파일을 로드합니다.

2. 프로젝트 초기화와 디렉터리 구조

Vite + React 기준 예시입니다. CRA도 동일한 방식으로 적용 가능합니다.

// 1) 프로젝트 생성
// npm create vite@latest my-app -- --template react
// cd my-app
// npm i
// 2) 필수 패키지 설치
// npm i -D electron electron-builder concurrently wait-on cross-env esbuild
// 3) 디렉터리 구조(예시)
// my-app/
//   electron/
//     main.js
//     preload.js
//   src/            // React 소스
//   dist/           // Vite 빌드 산출물
//   package.json

3. Electron 메인 프로세스 코드

개발 시에는 Vite 서버를 로드하고, 배포 시 빌드된 index.html을 로드합니다. 보안을 위해 contextIsolation을 켜고 nodeIntegration을 끕니다.

// electron/main.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
const fs = require('fs/promises');

const isDev = process.env.NODE_ENV === 'development' || !!process.env.ELECTRON_START_URL;

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
      sandbox: true
    }
  });

  // 외부 네비게이션 차단 및 팝업 차단
  win.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
  win.webContents.on('will-navigate', (e, url) => {
    if (url !== win.webContents.getURL()) e.preventDefault();
  });

  if (isDev) {
    const devUrl = process.env.ELECTRON_START_URL || 'http://localhost:5173';
    win.loadURL(devUrl);
    win.webContents.openDevTools({ mode: 'detach' });
  } else {
    // 배포 시: dist/index.html 로드
    win.loadFile(path.join(__dirname, '../dist/index.html'));
  }
}

// IPC 핸들러: 다이얼로그, 파일 읽기, 핑 테스트
ipcMain.handle('dialog:openFile', async () => {
  const { canceled, filePaths } = await dialog.showOpenDialog({
    properties: ['openFile']
  });
  if (canceled) return null;
  return filePaths[0];
});

ipcMain.handle('file:read', async (_e, filePath) => {
  if (!filePath) return null;
  // 실무에서는 허용 목록이나 디렉터리 범위를 검증하세요.
  const data = await fs.readFile(filePath, 'utf-8');
  return data;
});

ipcMain.handle('app:ping', async (_e, msg) => {
  return 'pong: ' + String(msg);
});

app.whenReady().then(() => {
  createWindow();
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

4. Preload에서 안전한 브리지 노출

렌더러는 window.api를 통해서만 OS 기능에 접근합니다. 직접 Node API를 노출하지 않습니다.

// electron/preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('api', {
  openFile: () => ipcRenderer.invoke('dialog:openFile'),
  readFile: (filePath) => ipcRenderer.invoke('file:read', filePath),
  ping: (msg) => ipcRenderer.invoke('app:ping', msg)
});

5. React에서 IPC 사용 예시

간단한 파일 열기 및 읽기 예제입니다. TypeScript를 쓴다면 window.api 타입 선언을 추가하세요.

// src/App.jsx
import { useEffect, useState } from 'react';

function App() {
  const [filePath, setFilePath] = useState('');
  const [content, setContent] = useState('');
  const [pong, setPong] = useState('');

  useEffect(() => {
    window.api.ping('hello').then(setPong);
  }, []);

  const handleOpen = async () => {
    const fp = await window.api.openFile();
    if (fp) {
      setFilePath(fp);
      const text = await window.api.readFile(fp);
      setContent(text || '');
    }
  };

  return (
    <div style={{ padding: 16 }}>
      <h1>React + Electron Demo</h1>
      <p>IPC Test: {pong}</p>
      <button onClick={handleOpen}>파일 열기</button>
      <p>선택된 파일: {filePath}</p>
      <pre style={{ whiteSpace: 'pre-wrap', background: '#f5f5f5', padding: 12 }}>{content}</pre>
    </div>
  );
}

export default App;

6. 개발 서버와 동시 실행 스크립트

Vite와 Electron을 동시에 띄워 빠르게 개발합니다. wait-on으로 개발 서버가 뜬 뒤 Electron을 실행합니다.

// package.json (일부)
{
  "main": "build/main.js",
  "scripts": {
    "dev:react": "vite",
    "dev:electron": "cross-env NODE_ENV=development ELECTRON_START_URL=http://localhost:5173 electron .",
    "dev": "concurrently -k \"npm:dev:react\" \"wait-on http://localhost:5173 && npm:dev:electron\"",
    "build:react": "vite build",
    "build:electron": "esbuild electron/main.js --bundle --platform=node --outfile=build/main.js && esbuild electron/preload.js --bundle --platform=node --outfile=build/preload.js",
    "build": "npm run build:react && npm run build:electron",
    "dist": "npm run build && electron-builder"
  },
  "devDependencies": {
    // ...vite, esbuild, etc.
  },
  "dependencies": {
    // ...react, react-dom, electron, etc.
  }
}

7. 빌드와 패키징 설정 electron-builder

React 빌드 산출물(dist)과 번들된 Electron 코드(build)를 포함해 패키징합니다.

// package.json 의 build 설정
{
  "name": "my-app",
  "version": "1.0.0",
  "main": "build/main.js",
  "build": {
    "appId": "com.example.reactelectron",
    "files": [
      "build/**",
      "dist/**",
      "package.json"
    ],
    "mac": {
      "category": "public.app-category.developer-tools",
      "target": ["dmg", "zip"]
    },
    "win": {
      "target": ["nsis", "zip"]
    },
    "linux": {
      "target": ["AppImage", "deb"]
    }
  }
}

8. 보안 체크리스트 핵심

중요 원칙은 최소 권한과 입력 검증입니다. nodeIntegration을 끄고 contextIsolation과 sandbox를 켭니다. preload에서는 ipcRenderer를 통해 제한된 함수만 노출합니다. IPC 파라미터를 검증하고 허용된 범위의 파일만 접근합니다. 외부 네비게이션, 새 창을 차단하고 필요 시 CSP를 설정합니다. 자동 업데이트나 로깅 시에도 민감정보를 남기지 않도록 주의합니다.

9. 배포와 업데이트 개요

배포는 electron-builder의 dist 스크립트로 OS별 설치 파일을 생성합니다. macOS는 코드서명과 공증, Windows는 코드서명이 필요할 수 있습니다. 자동 업데이트는 Github Releases나 사설 업데이트 서버를 사용하며 electron-updater 연동을 검토합니다.

10. 디버깅과 트러블슈팅 팁

Renderer는 브라우저 DevTools, Main은 콘솔 로그와 app.whenReady 이후 로직을 점검합니다. 경로 이슈는 __dirname 기준으로 dist와 build 포함 여부를 확인합니다. 네이티브 모듈 의존성이 있다면 rebuild를 적용하고, 안티바이러스 예외가 필요한지 모니터링합니다.

11. 마무리

위 구성은 실무에서 검증된 React + Electron 최소 템플릿입니다. 개발 중 빠른 HMR, 안전한 IPC, 간단한 패키징을 동시에 만족합니다. 이 베이스에 트레이, 메뉴, 바로가기, 자동 실행, 업데이트 등을 단계적으로 추가하여 프로덕션 수준 데스크톱 앱을 완성하시기 바랍니다.