ABOUT ME

-

  • [ Electron + Vue ] 새 윈도우 열기
    Desktop App/Electron + Vue 2026. 1. 10. 19:46
    반응형

    메모앱을 지속적으로 업데이트를 하면서

    이미지도 복사 붙혀넣기를 해서 넣을 수 있게 했는데 

    그 과정에서 이미지를 확대해서 보거나 할 수 가 없어서 이미지를 좀 더 상세하게 볼 수 있도록 이미지를 더블클릭하면 새로운 창이 열리면서 이미지를 볼 수 있도록 처리했다.


    Electron

    우선 electron의 main process에 새로운 윈도우를 열 수 있도록 sub window를 여는 함수를 만든다.

    추후에 여러 서브 윈도우를 열 수 있도록 만들려고 하기에 새로 열리는 서브 윈도우들은 map에 담아서 관리

     

    이후 파라미터로 page는 새로 열 윈도우의 경로, resource는 새로 여는 윈도우에 전달할 데이터,

    타입은 단순히 윈도우를 열것이냐 아니면 단순히 데이터만 전달할 것인지 분기용으로 처리

     

    이후 이미 윈도우가 열려있다면 해당 윈도우에 resoruce데이터만 보내주고,

    안열려있다면 윈도우를 열고 resource를 보내준다.

    did-finish-load는 해당 윈도우가 열리고 난 후에 실행되므로 열리고 나서 데이터를 보내주도록 한다.

    let subWindows = new Map();
    function createSubWindow(page, resource, type) {
        const existingWin = subWindows.get(page)
    
        if(existingWin && !existingWin.isDestroyed()) {
            existingWin.focus()
            existingWin.webContents.send('sub-win-resource',resource)
            return;
        }
    
        // type이 state라면 윈도우를 열지 않음
        if(type && type === 'state') return
    
        const win = new BrowserWindow({
            width: 500,
            height: 400,
            icon,
            webPreferences: {
                preload: join(__dirname, 'preload.js'), // preload 위치 확인
                contextIsolation: true,
                nodeIntegration: false,
            },
            frame: false
        });
    
        subWindows.set(page,win)
    
        win.on('closed',() => {
            subWindows.delete(page)
        })
    
        if (isDev) {
            win.loadURL(`http://localhost:5000/#/${page}`);
            win.webContents.openDevTools({ mode: 'detach' }); // 새 창으로 DevTools
        } else {
            win.loadFile(path.join(__dirname, "../dist/index.html"), {
                hash: `#/${page}`,
            });
        }
    
        win.webContents.once('did-finish-load',() => {
            win.webContents.send('sub-win-resource',resource)
        })
    }

     

    이제 해당 함수를 호출할 ipc를 등록

    ipcMain.on("open-sub-window", (_e, page, resource, type) => {
        createSubWindow(page, resource, type)
    })

     

    preload.js에서 데이터를 전달하기 위한 ipcRenderer를 등록해준다.

    did-finish-load를 하면 새로운 윈도우가 열리고나서 데이터를 전달해주므로 

     

    vue파일에서 onMounted로 해서 데이터를 받을때 이미 데이터가 전달되고 나서 ipcRender가 등록될 수 있는 타이밍 이슈가 있을 수 있기 때문에 preload에서 전달해야할 데이터를 변수에 담아두어서 화면에 렌더링이 된 후에도 데이터를 받을 수 있도록 처리

    const { contextBridge, ipcRenderer } = require('electron');
    
    let lastSubResource = null
    const subResourceListeners = new Set()
    
    ipcRenderer.on('sub-win-resource', (_e, resource) => {
        lastSubResource = resource
    
        subResourceListeners.forEach(cb => {
            try {
                cb(resource)
            } catch (e) {
                console.error(e)
            }
        })
    })
    
    contextBridge.exposeInMainWorld('electronAPI', {
    	// ...
        // sub window api
        onSubResource: (cb) => {
            subResourceListeners.add(cb)
    
            if (lastSubResource !== null) {
                cb(lastSubResource)
            }
    
            return () => subResourceListeners.delete(cb)
        },
    });

    Router

    새로운 윈도우를 열기 위해서 이제 해당 url에 맞는 라이터를 설정해줘야하므로

    기존의 router설정에서 새로 열 윈도우의 router를 설정해준다.

    // src/router/index.ts
    import type {RouteRecordRaw} from "vue-router";
    import {createRouter, createWebHashHistory,} from "vue-router";
    
    const constantRoutes: RouteRecordRaw[] = [
    	// ...
        {
            path: '/preview',
            name: 'preview',
            component: () => import('@/viewsub/preview/index.vue'),
        },
    ];
    
    const router = createRouter({
        history: createWebHashHistory(),
        routes: constantRoutes
    })
    
    export default router;

    Component

    이후 이미지를 클릭하면 이미지 url을 resource데이터로

    새로 열 창은 preview창을 열도록 파라미터를 전달한다.

    function onPreview(e: any) {
      const imgSrc = e.detail.src
      window.electronAPI.openSubWindow('preview', imgSrc)
    }

     

    이제 preview의 index.vue파일에서 onMount에서 데이터를 받을 수 있도록 처리

    사용자가 앱 테마를 바꾸면 앱 테마도 바뀌도록 처리했다.

     

    electron에서 새로운 윈도우가 열리게 되면 윈도우들은 각자만의 store를 가지고 있어서 

    서로 공유할려면 서로 데이터를 주고 받아줘야한다.

     

    store관리가 간단한거라면 상관이 없지만 모든 윈도우끼리 상태가 공유가 되어야한다면

    electron store를 사용해서 main process에서 상태를 관리해줘야한다.

     

    메모앱에서는 pinia를 사용해서 서로 store가 공유되지 않아서 변경하면 해당 열려있는 윈도우에서도 변경되었다는 것을 알려줘야한다.

    onMounted(() => {
      window.electronAPI.onSubResource((payload) => {
        if(payload.theme !== undefined) {
          appStore.appTheme = payload.theme
        } else {
          addImage(payload)
        }
      })
    })

     

    전달받은 이미지는 리스트에 넣어주고 화면에 보여준다.

    이렇게 전달받은 이미지를 왼쪽에는 리스트를 보여주고 오른쪽에는 해당 이미지를 상세보기를 주었다.


    Image Wheel Event

    이미지 상세보기가 되었으니 이제 이 이미지를 확대하거나 축소 해서 자세히 볼 수 있도록 기능을 구현해야한다.

    @wheel.prevent로 기존 휠 이벤트를 막고 새로운 이벤트를 실행시킨다.

    ctrl을 같이 누르면서 휠을 조정하면 scale을 조정하면서 이미지 크기를 조절한다.

     

    이후 확대되면 이미지의 위치를 조절해야하므로 

    이미지를 누르고 드리그하면 이미지 위치를 드래그한 위치로 바꿀 수 있도록 x,y좌표를 조정

     

    마지막으로 이제 조정된 scale과 x,y좌표를 이용해서 image style을 조정한다.

    <script>
    const scale = ref(1)
    const translate = ref({ x: 0, y: 0 })
    
    const isDragging = ref(false)
    const dragStart = ref({ x: 0, y: 0 })
    
    const MIN_SCALE = 0.2
    const MAX_SCALE = 6
    const ZOOM_STEP = 0.1
    
    const onWheelZoom = (e: WheelEvent) => {
      if (!e.ctrlKey) return
    
      if (e.deltaY < 0) {
        // 확대
        scale.value = Math.min(MAX_SCALE, scale.value + ZOOM_STEP)
      } else {
        // 축소
        scale.value = Math.max(MIN_SCALE, scale.value - ZOOM_STEP)
      }
    
      if (scale.value === 1) {
        translate.value = { x: 0, y: 0 }
      }
    }
    
    function onMouseDown(e: MouseEvent) {
      if (scale.value <= 1) return
    
      isDragging.value = true
      dragStart.value = {
        x: e.clientX - translate.value.x,
        y: e.clientY - translate.value.y,
      }
    }
    
    function onMouseMove(e: MouseEvent) {
      if (!isDragging.value) return
    
      translate.value = {
        x: e.clientX - dragStart.value.x,
        y: e.clientY - dragStart.value.y,
      }
    }
    
    function onMouseUp() {
      isDragging.value = false
    }
    
    const resetView = () => {
      scale.value = 1
      translate.value.x = 0
      translate.value.y = 0
    }
    
    const imageStyle = computed(() => ({
      transform: `
        translate(-50%, -50%)
        translate(${translate.value.x}px, ${translate.value.y}px)
        scale(${scale.value})
      `,
      transition: isDragging.value ? 'none' : 'transform 0.05s ease-out',
    }))
    
    const imageBoxStyle = computed(() => ({
      cursor:
          scale.value > 1
              ? (isDragging.value ? 'grabbing' : 'grab')
              : 'default',
    }))
    
    </script>
    
    <template>
    <div class="app-preview-view-wrapper"
         @wheel.prevent="onWheelZoom"
         @mousedown="onMouseDown"
         @mousemove="onMouseMove"
         @mouseup="onMouseUp"
         @mouseleave="onMouseUp"
         :style="imageBoxStyle"
         @dblclick="resetView"
    >
      <div class="image-box" v-if="previewImage">
        <img
            :src="previewImage"
            :style="imageStyle"
        />
      </div>
    </div>
    </template>

     

    그러면 ctrl을 누르면서 휠을 조정하면 확대 축소가 되며 드래그를 통해 이미지의 위치를 바꿀 수 있다.

     

     


    아래는 해당 앱을 다운받을 수 있는 사이트입니다!

    아직 자잘한 오류나 버그가 있을 수 있지만 계속해서 지속적으로 업데이트를 진행하고 있습니다.

    현재 윈도우에서만 설치가 가능합니다. 나중에는 ios 개발자 계정 등록하고 나서는 맥도 설치할 수 있게 진행할 예정입니다!

     

    https://yume-note.netlify.app/

     

    YumeNote - Notes and Schedule Management | 노트, 일정 관리 앱

     

    yume-note.netlify.app

     

     

     

    728x90
    반응형