1. Electron-Vite 프로젝트 생성

설치 참고 : https://electron-vite.org/guide/

$ npm create @quick-start/electron@latest
$ npm install
$ npm run dev
  • 생성된 폴더 내에서 npm run dev 명령어를 실행하면 실시간 미리보기가 가능하다.
  • npm run preview 명령어를 실행하면 build된 결과를 preview로 볼 수 있다.
  • npm run build:win 명령어를 실행하면 윈도우용 빌드를 진행한다.


2. Tailwind CSS 라이브러리 설치

(1) 라이브러리 설치

설치 참고 : https://tailwindcss.com/docs/guides/vite

프로젝트 폴더 안에서 아래 명령어를 실행한다.

$ npm install -D tailwindcss postcss autoprefixer
$ npx tailwindcss init -p

(2) postcss.config.js 파일을 postcss.config.cjs 으로 확장자 변경

(3) tailwind.config.cjs 파일 수정

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/renderer/index.html",
    "./src/renderer/src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

(4) src/renderer/src/assets 내의 다른 파일들 모두 삭제

(5) src/renderer/src/assets/index.css 파일 생성

body {
  margin: 0;
  display: flex;
  justify-content: center;
  min-height: 100vh;
  @apply text-[#404040];
  @apply font-['NanumSquareRound-B'];
}

/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
  -webkit-appearance: none;
}
/* Firefox */
input[type="number"] {
  -moz-appearance: textfield;
}

@tailwind base;
@tailwind components;
@tailwind utilities;

@font-face {
  font-family: "NanumSquareNeo-B";
  src: url("./fonts/NanumSquareNeo/NanumSquareNeoTTF-cBd.woff");
}

@font-face {
  font-family: "NanumSquareNeo-EB";
  src: url("./fonts/NanumSquareNeo/NanumSquareNeoTTF-dEb.woff");
}

@font-face {
  font-family: "NanumSquareNeo-HB";
  src: url("./fonts/NanumSquareNeo/NanumSquareNeoTTF-eHv.woff");
}

@font-face {
  font-family: "NanumSquareNeo-L";
  src: url("./fonts/NanumSquareNeo/NanumSquareNeoTTF-aLt.woff");
}

@font-face {
  font-family: "NanumSquareNeo-R";
  src: url("./fonts/NanumSquareNeo/NanumSquareNeoTTF-bRg.woff");
}

@font-face {
  font-family: "NanumSquareRound-B";
  src: url("./fonts/NanumSquareRound/NanumSquareRoundOTFB.otf");
}

@font-face {
  font-family: "NanumSquareRound-EB";
  src: url("./fonts/NanumSquareRound/NanumSquareRoundOTFEB.otf");
}

@font-face {
  font-family: "NanumSquareRound-L";
  src: url("./fonts/NanumSquareRound/NanumSquareRoundOTFL.otf");
}

@font-face {
  font-family: "NanumSquareRound-R";
  src: url("./fonts/NanumSquareRound/NanumSquareRoundOTFR.otf");
}

(6) src/renderer/src/assets/fonts 폴더에 폰트 파일 넣기


3. Redux toolkit 설치

(1) 라이브러리 설치

$ npm install @reduxjs/toolkit react-redux

(2) src\renderer\src\redux\index.jsx 파일 생성

import { configureStore } from "@reduxjs/toolkit";

const store = configureStore({
  reducer: {},
});

export default store;

(3) src\renderer\src\main.jsx 수정

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

import "@assets/index.css";
import { Provider } from "react-redux";
import store from "@redux";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);


4. electron-store 라이브러리 설치

(1) 라이브러리 설치

$ npm install electron-store


5. package.json 파일 수정

아래 내용을 추가한다.

{
	"type": "module",
}


6. src\renderer\src\App.jsx 파일 수정

import Test from "@components/Test";

function App() {
  return (
    <>
      <Test />
    </>
  );
}

export default App;


7. src\renderer\src\components\Test.jsx 파일 생성

function Test() {
  return (
    <>
      <h1>Test</h1>
    </>
  );
}

export default Test;


8. src\renderer\index.html 파일 수정

{프로젝트 이름} 부분은 적절히 수정한다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>{프로젝트 이름}</title>
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self' http://localhost:* ws://127.0.0.1:*; script-src 'self'; style-src 'self' 'unsafe-inline'"
    />
  </head>

  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>


9. src\main\index.js 파일 수정

import { app, shell, BrowserWindow, dialog, ipcMain } from "electron";
import { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import icon from "../../resources/icon.png?asset";

import Store from "electron-store";

function createWindow() {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 1280,
    height: 720,
    minWidth: 1280,
    minHeight: 720,
    show: false,
    autoHideMenuBar: true,
    ...(process.platform === "linux" ? { icon } : {}),
    webPreferences: {
      preload: join(__dirname, "../preload/index.js"),
      sandbox: false,
    },
  });
  mainWindow.removeMenu();

  mainWindow.on("ready-to-show", () => {
    mainWindow.show();
  });

  mainWindow.webContents.setWindowOpenHandler((details) => {
    shell.openExternal(details.url);
    return { action: "deny" };
  });

  // HMR for renderer base on electron-vite cli.
  // Load the remote URL for development or the local html file for production.
  if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
    mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
  } else {
    mainWindow.loadFile(join(__dirname, "../renderer/index.html"));
  }
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
  // Set app user model id for windows
  electronApp.setAppUserModelId("com.electron");

  // Default open or close DevTools by F12 in development
  // and ignore CommandOrControl + R in production.
  // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
  app.on("browser-window-created", (_, window) => {
    optimizer.watchWindowShortcuts(window);
  });

  ipcMain.handle("message", async (event, type, title, message) => {
    return dialog.showMessageBox(BrowserWindow.fromWebContents(event.sender), {
      message: message,
      type: type,
      title: title,
    });
  });

  ipcMain.handle("openDir", async (event, defalut_path) => {
    return dialog
      .showOpenDialog(BrowserWindow.fromWebContents(event.sender), {
        defaultPath: defalut_path,
        properties: ["openDirectory"],
      })
      .then(({ canceled, filePaths }) => {
        if (canceled) return;
        return filePaths[0];
      });
  });

  ipcMain.handle("openFile", async (event, filters) => {
    return dialog
      .showOpenDialog(BrowserWindow.fromWebContents(event.sender), {
        filters: filters,
      })
      .then(({ canceled, filePaths }) => {
        if (canceled) return;
        return filePaths[0];
      });
  });

  ipcMain.handle("saveFile", async (event, filters) => {
    return dialog
      .showSaveDialog(BrowserWindow.fromWebContents(event.sender), {
        filters: filters,
      })
      .then(({ canceled, filePath }) => {
        if (canceled) return;
        return filePath;
      });
  });

  ipcMain.handle("setStore", (_, key, value) => {
    const store = new Store();
    store.set(key, value);
    return;
  });

  ipcMain.handle("getStore", (_, key) => {
    const store = new Store();
    return store.get(key);
  });

  createWindow();

  app.on("activate", function () {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.


10. src\preload\index.js 파일 수정

import { contextBridge, ipcRenderer } from "electron";
import { electronAPI } from "@electron-toolkit/preload";

// Custom APIs for renderer
const api = {};

// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
  try {
    contextBridge.exposeInMainWorld("electron", electronAPI);
    contextBridge.exposeInMainWorld("api", api);
  } catch (error) {
    console.error(error);
  }
} else {
  window.electron = electronAPI;
  window.api = api;
}

contextBridge.exposeInMainWorld("electronAPI", {
  message: (type, title, message) =>
    ipcRenderer.invoke("message", type, title, message),
  openDir: (defalut_path) => ipcRenderer.invoke("openDir", defalut_path),
  openFile: (filters) => ipcRenderer.invoke("openFile", filters),
  saveFile: (filters) => ipcRenderer.invoke("saveFile", filters),
  setStore: (key, value) => ipcRenderer.invoke("setStore", key, value),
  getStore: (key) => ipcRenderer.invoke("getStore", key),
});


11. .eslintrc.cjs 파일 수정

module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react/jsx-runtime',
    '@electron-toolkit',
    '@electron-toolkit/eslint-config-prettier'
  ],
  rules: {
    'prettier/prettier': [
      'error',
      {
        endOfLine: 'auto'
      }
    ],
    'react/no-unknown-property': [
      'error',
      {
        ignore: [
          'intensity',
          'args',
          'position',
          'rotation',
          'attach',
          'array',
          'count',
          'itemSize',
          'object',
          'enableRotate',
          'frustumCulled',
          'transparent'
        ]
      }
    ]
  }
}


12. .gitignore 파일 수정

node_modules
dist
out
.DS_Store
*.log*

.vscode


13. electron-builder.yml 파일 수정

{프로젝트 이름} 부분은 적절히 수정한다.

appId: com.electron.app
productName: { 프로젝트 이름 }
directories:
  buildResources: build
files:
  - "!**/.vscode/*"
  - "!src/*"
  - "!electron.vite.config.{js,ts,mjs,cjs}"
  - "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}"
  - "!{.env,.env.*,.npmrc,pnpm-lock.yaml}"
asarUnpack:
  - resources/**
win:
  executableName: { 프로젝트 이름 }
nsis:
  artifactName: ${name}-${version}-setup.${ext}
  shortcutName: ${productName}
  uninstallDisplayName: ${productName}
  createDesktopShortcut: always
  oneClick: false
  allowToChangeInstallationDirectory: true
mac:
  entitlementsInherit: build/entitlements.mac.plist
  extendInfo:
    - NSCameraUsageDescription: Application requests access to the device's camera.
    - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
    - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
    - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
  notarize: false
dmg:
  artifactName: ${name}-${version}.${ext}
linux:
  target:
    - AppImage
    - snap
    - deb
  maintainer: electronjs.org
  category: Utility
appImage:
  artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
  provider: generic
  url: https://example.com/auto-updates


14. electron.vite.config.mjs 파일 수정

import { resolve } from "path";
import { defineConfig, externalizeDepsPlugin } from "electron-vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  main: {
    plugins: [externalizeDepsPlugin()],
  },
  preload: {
    plugins: [externalizeDepsPlugin()],
  },
  renderer: {
    resolve: {
      alias: {
        "@renderer": resolve("src/renderer/src"),
        "@assets": resolve("src/renderer/src/assets"),
        "@components": resolve("src/renderer/src/components"),
        "@utils": resolve("src/renderer/src/utils"),
        "@redux": resolve("src/renderer/src/redux"),
      },
    },
    plugins: [react()],
  },
});


Updated: