"Vue + Electron도 생산성 높은 조합" — Vue의 간결한 문법과 Electron의 네이티브 접근이 만나면 빠르게 데스크톱 앱을 만들 수 있습니다.


프로젝트 설정

electron-vite + Vue 3

BASH
npm create @quick-start/electron my-vue-app -- --template vue-ts
cd my-vue-app
npm install
npm run dev

프로젝트 구조

PLAINTEXT
my-vue-app/
├── src/
│   ├── main/
│   │   └── index.ts         # 메인 프로세스
│   ├── preload/
│   │   └── index.ts         # 프리로드 스크립트
│   └── renderer/
│       ├── src/
│       │   ├── App.vue
│       │   ├── main.ts
│       │   └── components/
│       └── index.html
├── electron.vite.config.ts
└── package.json

Composable로 Electron API 래핑

TYPESCRIPT
// renderer/src/composables/useElectron.ts
import { ref, onMounted, onUnmounted } from 'vue';

// 파일 다이얼로그 Composable
export function useFileDialog() {
  const openFile = async () => {
    return await window.electronAPI.openFile();
  };

  const saveFile = async (content: string) => {
    return await window.electronAPI.saveFile(content);
  };

  return { openFile, saveFile };
}

// 앱 버전 Composable
export function useAppVersion() {
  const version = ref('');

  onMounted(async () => {
    version.value = await window.electronAPI.getVersion();
  });

  return { version };
}

// 메인 프로세스 이벤트 수신 Composable
export function useMainEvent(eventName: string, handler: (...args: any[]) => void) {
  let cleanup: (() => void) | null = null;

  onMounted(() => {
    cleanup = window.electronAPI.on(eventName, handler);
  });

  onUnmounted(() => {
    cleanup?.();
  });
}

Vue 컴포넌트에서 사용

VUE
<!-- renderer/src/components/Editor.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { useFileDialog, useMainEvent } from '../composables/useElectron';

const content = ref('');
const filePath = ref<string | null>(null);
const { openFile, saveFile } = useFileDialog();

const handleOpen = async () => {
  const path = await openFile();
  if (path) {
    filePath.value = path;
  }
};

const handleSave = async () => {
  if (content.value) {
    await saveFile(content.value);
  }
};

// 메뉴에서 저장 단축키를 누르면 호출
useMainEvent('menu:save', () => handleSave());
</script>

<template>
  <div class="editor">
    <div class="toolbar">
      <button @click="handleOpen">열기</button>
      <button @click="handleSave">저장</button>
      <span v-if="filePath">{{ filePath }}</span>
    </div>
    <textarea v-model="content" />
  </div>
</template>

Pinia 상태 관리와 Electron

TYPESCRIPT
// renderer/src/stores/app.ts
import { defineStore } from 'pinia';

export const useAppStore = defineStore('app', {
  state: () => ({
    theme: 'light' as 'light' | 'dark',
    recentFiles: [] as string[],
  }),

  actions: {
    async loadSettings() {
      const settings = await window.electronAPI.getSettings();
      this.theme = settings.theme;
      this.recentFiles = settings.recentFiles;
    },

    async setTheme(theme: 'light' | 'dark') {
      this.theme = theme;
      await window.electronAPI.setSetting('theme', theme);
    },

    addRecentFile(path: string) {
      this.recentFiles = [
        path,
        ...this.recentFiles.filter(f => f !== path),
      ].slice(0, 10);
      window.electronAPI.setSetting('recentFiles', this.recentFiles);
    },
  },
});

Vue Router 설정

TYPESCRIPT
// renderer/src/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router';

// Electron에서는 Hash 모드 사용
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    { path: '/', component: () => import('../views/Home.vue') },
    { path: '/settings', component: () => import('../views/Settings.vue') },
    { path: '/editor', component: () => import('../views/Editor.vue') },
  ],
});

export default router;

면접 포인트 정리

  • Vue 3 Composition API의 Composable로 Electron API를 래핑하면 재사용성이 높아짐
  • onUnmounted에서 이벤트 리스너를 반드시 정리
  • Vue Router는 createWebHashHistory() 사용 (Electron은 file:// 프로토콜)
  • Pinia 스토어에서 Electron 설정을 동기화하는 패턴이 실용적

Vue 통합을 다뤘으면, 다음은 Svelte + Electron 조합을 알아봅시다.

댓글 로딩 중...