개발/프론트엔드

리액트로 만든 SW 프로그램을 exe 파일로 배포하자!

paice 2025. 8. 22. 19:28

팀 내 업무 자동화를 위해 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)
  • 실무 환경에서 흔히 맞닥뜨리는 네트워크·보안·빌드 문제 해결 경험

을 얻게 되었습니다.
추후엔 자동 업데이트나 용량 최적화에도 도전해 보고 싶습니다.