팀 내 업무 자동화를 위해 React 기반의 프론트엔드와 FastAPI 기반의 백엔드로 프로그램을 개발했습니다.
개발 환경에서는 npm start로 React 개발 서버를 실행하고, FastAPI 서버를 로컬에서 띄우는 방식으로 사용했지만,
문제는 당장 서버를 할당받아 웹 서비스 형태로 배포할 수 없는 상황이었습니다.
사내 인프라 정책과 승인 절차 때문에 클라우드나 온프레미스 서버에 올리는 것은 단기간에 불가능했기에
사용자 입장에서는 웹 서비스 접속 대신 설치 후 바로 실행할 수 있는 데스크톱 프로그램이 필요했습니다.
즉, React 앱을 exe 파일 형태로 배포해야 했습니다.
1. Node.js 기반 앱을 exe로 만드는 방법은 어떤게 있는지?
React로 만든 웹 애플리케이션을 설치형 프로그램(exe) 형태로 배포하기 위해, 먼저 Node.js 기반 앱을 어떻게 exe로 패키징하는지부터 알아보았습니다.
대표적으로는 두 가지 방법이 있는데,
1) pkg 방식
pkg는 Node.js 프로젝트를 단일 실행 파일(.exe)로 패키징해주는 도구입니다.
쉽게 말해, Node.js 런타임과 애플리케이션 코드를 하나의 바이너리로 묶어주는 방식입니다.
- 장점
- 결과물이 가볍고 단일 파일로 배포 가능.
- CLI 도구나 서버 사이드 유틸리티 제작에 적합.
- 단점
- React 같은 UI가 있는 앱은 직접 실행할 수 없음.
- 브라우저를 강제로 띄워서 UI를 보여주는 식으로 우회해야 해서 UX가 떨어짐.
2) Electron 방식
Electron은 Chromium(브라우저 엔진)과 Node.js 런타임을 함께 패키징하는 프레임워크입니다.
즉, React/Vue 같은 웹 앱을 그대로 데스크톱 앱처럼 실행할 수 있게 해줍니다.
- 장점
- React UI를 그대로 실행 가능
- 윈도우/맥/리눅스 등 크로스 플랫폼 지원
- 로컬 파일 시스템, 알림, IPC 통신 등 데스크톱 전용 API 제공
- 단점
- Chromium까지 포함하기 때문에 exe 용량이 커짐 (보통 50~100MB 이상)
정리하자면 pkg는 터미널 기반 유틸리티에는 적합하지만, 데스크톱 앱에는 한계가 있고, Electron은 UI/UX 중심의 데스크톱 앱에 적합합니다.
저희가 만든 프로그램은 단순한 CLI 유틸리티가 아니라, React 기반 UI를 그대로 제공해야 했습니다.
사용자가 버튼을 클릭하면 FastAPI API가 호출되고, 그 결과가 화면에 바로 표시되어야 합니다.
따라서 실행 파일 용량이 다소 커지더라도 사용자 경험을 위해 Electron 기반 exe 제작을 선택했습니다.
3. 환경 설정 및 제작 과정
1) 폴더 구조
프로젝트명/
frontend/
package.json # CRA + Electron 의존성 및 빌드 설정
electron/
main.js # Electron 엔트리 파일 (윈도우 생성, dev/prod 분기)
preload.js # 보안 브릿지 (렌더러 <-> 메인 안전한 통신)
build/ # CRA 빌드 산출물
release/ # electron-builder 산출물
2) Electron 환경 세팅
npm i -D electron electron-builder concurrently cross-env wait-on
- electron: 데스크톱 앱 실행 환경
- electron-builder: exe 패키징 도구
- concurrently: CRA dev 서버 + Electron 동시 실행
- wait-on: CRA 서버 준비될 때까지 대기
- cross-env: 환경변수 주입
3) main.js 작성
const { app, BrowserWindow, shell } = require('electron');
const path = require('path');
// 개발 모드 여부
const isDev = !app.isPackaged;
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // 보안 브릿지
},
});
if (isDev) {
win.loadURL(process.env.ELECTRON_START_URL || 'http://localhost:3000'); // 개발 모드
win.webContents.openDevTools();
} else {
win.loadFile(path.join(__dirname, '..', 'build', 'index.html')); // 배포 모드
}
// 외부 링크는 기본 브라우저에서 열기
win.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
}
// 앱 시작 시 창 생성
app.whenReady().then(createWindow);
// macOS 제외, 모든 창 닫히면 종료
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
- Electron 앱의 진입점으로, 창을 생성하고 개발/배포 모드에 따라 React 앱을 불러오는 역할을 하는 파일
- 외부 링크를 OS 브라우저로만 열어 보안을 지키고, 실행 시 필요한 설정(예: 백엔드 주소)을 주입해주는 역할
4) preload.js 작성
const { contextBridge } = require('electron');
// 렌더러(React)에서 접근 가능한 API 노출
contextBridge.exposeInMainWorld('api', {
// main.js에서 주입한 백엔드 주소 반환
getBackendUrl: () => globalThis.__BACKEND_URL__,
});
- 렌더러(React)와 메인 프로세스(Electron) 사이의 보안 브릿지 역할을 하는 파일
- Node API를 직접 노출하지 않고, 백엔드 주소 등 필요한 기능만 제한적으로 제공해 안전하게 통신할 수 있도록 해줌
5) 빌드 스크립트 작성
"scripts": {
"dev": "concurrently \\\\"react-scripts start\\\\" \\\\"wait-on <http://localhost:3000> && cross-env ELECTRON_START_URL=http://localhost:3000 electron .\\\\"",
"build:react": "react-scripts build",
"dist": "npm run build:react && electron-builder"
}
- npm run dev → 개발 환경에서 React + Electron 동시에 실행
- npm run dist → React 빌드 후 exe 패키징
6) 빌드 산출물
- release/프로젝트명 Setup.exe : 설치형 인스톨러
- release/win-unpacked/프로젝트명.exe : 설치 없이 실행 가능한 포터블 버전
3. 결과
이 과정을 거쳐 최종적으로 React 기반 앱을 exe 형태로 배포할 수 있었습니다.
사용자는 더 이상 개발 환경을 직접 세팅할 필요가 없고, 단순히 설치형 인스톨러(Setup.exe)나 포터블 실행 파일(프로젝트명.exe)을 실행하면 바로 UI와 백엔드가 연동된 프로그램을 쓸 수 있습니다.
설치형/포터블 두 가지를 제공하면서, 사용자 환경에 따라 유연하게 선택할 수 있게 된 것도 장점이었습니다.
4. 주요 트러블 슈팅
빌드 후 실행 시 ERR_FILE_NOT_FOUND 오류가 발생했습니다.
원인은 CRA(Create React App) 기본 설정에서 정적 리소스 경로를 절대경로(/static/...)로 지정하기 때문이었는데요,
브라우저 환경에서는 문제가 없지만, Electron exe에서는 리소스를 file://… 경로로 불러오기 때문에 절대경로를 찾지 못했던 것이었습니다.
이를 해결하기 위해 package.json에 "homepage": "./"를 설정했습니다.
이렇게 하면 CRA 빌드 결과가 상대경로(./static/...)로 생성되어 exe 내부에서도 정상적으로 리소스를 로드할 수 있었습니다.
5. 회고
처음 exe 빌드를 시도할 때는 막막함과 두려움이 컸지만, 막상 해보니 새로운 걸 만든다는 설렘과 재미도 있었습니다.
Electron 환경 구성, 보안 브릿지(preload.js), 사내 네트워크 정책, 코드 서명 문제 등 예상치 못한 이슈들이 이어졌지만, 하나씩 원인을 찾고 해결하면서 역시 개발은 이런 재미로 한다는 생각이 들었습니다!
이번 과정을 통해
- 웹 앱을 데스크톱 앱으로 패키징하는 전체 흐름 (React → Electron → exe)
- 실무 환경에서 흔히 맞닥뜨리는 네트워크·보안·빌드 문제 해결 경험
을 얻게 되었습니다.
추후엔 자동 업데이트나 용량 최적화에도 도전해 보고 싶습니다.
'개발 > 프론트엔드' 카테고리의 다른 글
| 30개의 상태가 달린 하나의 컴포넌트를 마주친 적 있으신가요? (2) | 2025.05.14 |
|---|---|
| 리코일을 도입해보십다 (2) | 2025.03.26 |
| CRA 지고 Vite가 왔다...!! (0) | 2025.03.26 |
| Redux Toolkit을 도입하면서 (1) | 2024.12.23 |
| 왜 Redux 대신 Redux Toolkit을 사용하는가? (4) | 2024.12.23 |