"메뉴와 트레이는 앱이 '데스크톱 앱답다'고 느끼게 하는 요소" — 웹 앱과 데스크톱 앱의 차이를 만드는 핵심 UI입니다.


애플리케이션 메뉴

기본 메뉴 구성

JAVASCRIPT
const { app, Menu, BrowserWindow } = require('electron');

const template = [
  {
    label: '파일',
    submenu: [
      {
        label: '새 파일',
        accelerator: 'CmdOrCtrl+N',  // 단축키
        click: (menuItem, browserWindow) => {
          browserWindow.webContents.send('menu:newFile');
        },
      },
      {
        label: '열기',
        accelerator: 'CmdOrCtrl+O',
        click: async (menuItem, browserWindow) => {
          const { dialog } = require('electron');
          const result = await dialog.showOpenDialog(browserWindow, {
            properties: ['openFile'],
          });
          if (!result.canceled) {
            browserWindow.webContents.send('menu:openFile', result.filePaths[0]);
          }
        },
      },
      { type: 'separator' },  // 구분선
      {
        label: '종료',
        accelerator: 'CmdOrCtrl+Q',
        click: () => app.quit(),
      },
    ],
  },
  {
    label: '편집',
    submenu: [
      { role: 'undo', label: '실행 취소' },
      { role: 'redo', label: '다시 실행' },
      { type: 'separator' },
      { role: 'cut', label: '잘라내기' },
      { role: 'copy', label: '복사' },
      { role: 'paste', label: '붙여넣기' },
      { role: 'selectAll', label: '모두 선택' },
    ],
  },
  {
    label: '보기',
    submenu: [
      { role: 'reload', label: '새로고침' },
      { role: 'toggleDevTools', label: '개발자 도구' },
      { type: 'separator' },
      { role: 'zoomIn', label: '확대' },
      { role: 'zoomOut', label: '축소' },
      { role: 'resetZoom', label: '원래 크기' },
      { type: 'separator' },
      { role: 'togglefullscreen', label: '전체 화면' },
    ],
  },
];

// macOS에서는 첫 번째 메뉴가 앱 이름
if (process.platform === 'darwin') {
  template.unshift({
    label: app.getName(),
    submenu: [
      { role: 'about', label: `${app.getName()} 정보` },
      { type: 'separator' },
      { role: 'hide', label: '숨기기' },
      { role: 'hideOthers', label: '기타 숨기기' },
      { role: 'unhide', label: '모두 보기' },
      { type: 'separator' },
      { role: 'quit', label: '종료' },
    ],
  });
}

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

role의 편리함

role을 사용하면 OS별 기본 동작과 단축키가 자동으로 설정됩니다. 직접 click 핸들러를 작성할 필요가 없습니다.


컨텍스트 메뉴 (우클릭 메뉴)

JAVASCRIPT
// main.js
const { ipcMain, Menu } = require('electron');

ipcMain.on('context-menu:show', (event, params) => {
  const template = [
    {
      label: '잘라내기',
      role: 'cut',
      enabled: params.hasSelection,
    },
    {
      label: '복사',
      role: 'copy',
      enabled: params.hasSelection,
    },
    {
      label: '붙여넣기',
      role: 'paste',
    },
    { type: 'separator' },
    {
      label: '새로고침',
      click: () => {
        event.sender.reload();
      },
    },
  ];

  const menu = Menu.buildFromTemplate(template);
  menu.popup({
    window: BrowserWindow.fromWebContents(event.sender),
  });
});
JAVASCRIPT
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  showContextMenu: (params) =>
    ipcRenderer.send('context-menu:show', params),
});
JAVASCRIPT
// renderer.js — 우클릭 이벤트에서 호출
document.addEventListener('contextmenu', (e) => {
  e.preventDefault();
  window.electronAPI.showContextMenu({
    hasSelection: window.getSelection().toString().length > 0,
  });
});

시스템 트레이

트레이 아이콘은 작업표시줄(Windows) 또는 메뉴바(macOS)에 앱 아이콘을 표시합니다.

JAVASCRIPT
const { app, Tray, Menu, nativeImage, BrowserWindow } = require('electron');
const path = require('path');

let tray = null;
let mainWindow = null;

app.whenReady().then(() => {
  // 트레이 아이콘 생성
  const icon = nativeImage.createFromPath(
    path.join(__dirname, 'assets', 'tray-icon.png')
  );

  // macOS에서는 16x16 또는 18x18 사이즈가 적절합니다
  tray = new Tray(icon.resize({ width: 16, height: 16 }));

  // 트레이 메뉴
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '앱 열기',
      click: () => {
        if (mainWindow) {
          mainWindow.show();
          mainWindow.focus();
        }
      },
    },
    {
      label: '상태',
      submenu: [
        {
          label: '온라인',
          type: 'radio',
          checked: true,
          click: () => setStatus('online'),
        },
        {
          label: '자리 비움',
          type: 'radio',
          click: () => setStatus('away'),
        },
        {
          label: '방해 금지',
          type: 'radio',
          click: () => setStatus('dnd'),
        },
      ],
    },
    { type: 'separator' },
    {
      label: '종료',
      click: () => {
        app.isQuitting = true;
        app.quit();
      },
    },
  ]);

  tray.setContextMenu(contextMenu);
  tray.setToolTip('내 앱 — 실행 중');

  // 트레이 아이콘 클릭 시 윈도우 토글
  tray.on('click', () => {
    if (mainWindow.isVisible()) {
      mainWindow.hide();
    } else {
      mainWindow.show();
      mainWindow.focus();
    }
  });
});

창 닫기 시 트레이로 최소화

JAVASCRIPT
// 창을 닫아도 트레이에 남아있게 하기
mainWindow.on('close', (event) => {
  if (!app.isQuitting) {
    event.preventDefault();
    mainWindow.hide();  // 숨기기만 하고 프로세스는 유지
  }
});

동적 메뉴 업데이트

JAVASCRIPT
// 메뉴 아이템을 동적으로 활성화/비활성화
function updateMenuState(hasDocument) {
  const menu = Menu.getApplicationMenu();

  // ID로 메뉴 아이템 찾기
  const saveItem = menu.getMenuItemById('save');
  if (saveItem) {
    saveItem.enabled = hasDocument;
  }
}

// 메뉴 템플릿에 id 추가
const template = [
  {
    label: '파일',
    submenu: [
      {
        id: 'save',
        label: '저장',
        accelerator: 'CmdOrCtrl+S',
        enabled: false,  // 초기에는 비활성화
        click: () => saveDocument(),
      },
    ],
  },
];

면접 포인트 정리

  • role을 사용하면 OS별 기본 동작과 단축키가 자동 설정됨
  • macOS에서 첫 번째 메뉴는 반드시 앱 이름이어야 함
  • 컨텍스트 메뉴는 Menu.popup()으로 우클릭 위치에 표시
  • 트레이 아이콘은 close 이벤트를 가로채서 백그라운드 실행 구현
  • app.isQuitting 플래그로 진짜 종료와 숨기기를 구분

메뉴와 트레이를 구현했으면, 다음은 다이얼로그 API를 알아봅시다.

댓글 로딩 중...