메뉴와 트레이 — 네이티브 UX 구현하기
"메뉴와 트레이는 앱이 '데스크톱 앱답다'고 느끼게 하는 요소" — 웹 앱과 데스크톱 앱의 차이를 만드는 핵심 UI입니다.
애플리케이션 메뉴
기본 메뉴 구성
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 핸들러를 작성할 필요가 없습니다.
컨텍스트 메뉴 (우클릭 메뉴)
// 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),
});
});
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
showContextMenu: (params) =>
ipcRenderer.send('context-menu:show', params),
});
// renderer.js — 우클릭 이벤트에서 호출
document.addEventListener('contextmenu', (e) => {
e.preventDefault();
window.electronAPI.showContextMenu({
hasSelection: window.getSelection().toString().length > 0,
});
});
시스템 트레이
트레이 아이콘은 작업표시줄(Windows) 또는 메뉴바(macOS)에 앱 아이콘을 표시합니다.
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();
}
});
});
창 닫기 시 트레이로 최소화
// 창을 닫아도 트레이에 남아있게 하기
mainWindow.on('close', (event) => {
if (!app.isQuitting) {
event.preventDefault();
mainWindow.hide(); // 숨기기만 하고 프로세스는 유지
}
});
동적 메뉴 업데이트
// 메뉴 아이템을 동적으로 활성화/비활성화
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를 알아봅시다.
댓글 로딩 중...