Electron+React 프로그램에서 커스텀 메뉴바 구현

우디·2024년 3월 4일
0
post-thumbnail

안녕하세요:) 개발자 우디입니다! 아래 내용 관련하여 작업 중이신 분들께 도움이되길 바라며 글을 공유하니 참고 부탁드립니다😊
(이번에 벨로그로 이사오면서 예전 글을 옮겨적었습니다. 이 점 양해 부탁드립니다!)

작업 시점: 2021년 11월

배경

  • 기존 사용자들이 영상 분석 후 프로그램의 기능들을 제대로 사용하지 못함
  • 영상 분석 후에 활용 가능한 기능들을 쉽게 파악하고 사용할 수 있도록 메뉴바를 상단에 위치시키면 좋을 것.

구현 과정

  • 메인 프로세스에서 기존의 electron에 있던 메뉴바의 구조를 살펴보기

    • 기존 메뉴는 electron의 Menu 클래스를 활용하여 메뉴 항목들을 템플릿 형식으로 관리하면서, setApplicationMenu 메소드를 통해 메뉴로 구현되는 방식임. → 리액트로 구현되는 커스텀 메뉴바도 확장성 있게 작성되려면 이렇게 템플릿 형식으로 구현되면 좋을 것.

    • 기존 메뉴에서 관리되고 있는 항목들을 살펴보자면, 아래와 같음.

      • label
        • 메뉴에 표시되는 문구.
      • accelerator
        • 단축키 문구.
      • id
        • 메뉴들을 구분해주는 고유의 값
      • click
        • 클릭 시 실행될 함수
      • submenu
        • 하위 메뉴가 있을 경우 [{}, {}, {} …] 형태로 구현함
        • visible은 해당 메뉴 아이템을 숨기는지 여부
      • 기타
        • { type: 'separator' }
          • 메뉴 구분선을 표시함
      const { ... Menu, ... } = require('electron');
      ...
      function createWindow() {
        ...
      
        let menu = Menu.buildFromTemplate([
          {
            label: '파일',
            id: 'fileMenu',
            submenu: [
              {
                label: '프로젝트 생성',
                accelerator: 'CmdOrCtrl+Alt+N',
                id: 'newProject',
                click: e => {
                  win.webContents.send('new-project-clicked');
                },
              },
            ]
           ...
         },
        ]);
        Menu.setApplicationMenu(menu);
      	...
      }
    • 본격적으로 렌더러 프로세스에서 커스텀 메뉴바를 구현하기 위해, 앞서 살펴 본 메인 프로세스의 메뉴 코드를 지워 줌

  • 렌더러 프로세스에서의 메뉴 구현

    • 메뉴는 여러 기능들을 실행할 수 있어야 하고 실시간으로 렌더링이 필요한 경우도 있을 것이기 때문에, 최상단 컴포넌트인 App.js의 하위 컴포넌트로 배치함.
        render() {
        ...
        return (
          ...
            <MenuBar
              menu={this.state.menu}
              ...
            />
          ...
        );
      }
      }
      export default App;
    • 메뉴의 여러 항목들은 App.js의 state에서 관리함.
      • this.state.menu를 MenuBar 컴포넌트로 전달하여 메뉴바 UI 구현하는 방식임.
      • 각 항목 설명
        • label - 메뉴에 표시될 문구
        • id - 메뉴들을 구분하는 고유 id 값
        • enabled - 메뉴 아이템의 활성화 여부
        • submenu - 클릭 시 보이는 하위 메뉴
        • icon - 메뉴 텍스트 위에 보이는 이미지 아이콘
        • accelerator - 단축키 안내 문구
        • click - 메뉴 클릭 시 실행될 함수
        • 메뉴 구분선은 { id: 'separator' } 으로 구현.
         this.state = {
          ...
          menu: [
            {
              label: '파일',
              id: 'fileMenu',
              enabled: true,
              submenu: [
                {
                  label: '새 프로젝트',
                  icon: 'assets/menubarIcon/newProjectIcon.svg',
                  id: 'newProject',
                  enabled: true,
                  accelerator: 'Ctrl + Alt + N',
                  click: () => {
                    ...
                  },
                },
  • 메뉴바 관련 메소드들

    /**
     * get path object from menuList
     * @param {String} idPrefix
     * @param {Array} menuArray
     * @returns {undefined | Object}
     */
    getMenuItem = (idPrefix, menuArray = this.state.menu) => {
      let found = menuArray.find(item => item.id.startsWith(idPrefix));
      for (let i = 0; !found && i < menuArray.length; i++) {
        const { submenu } = menuArray[i];
        if (submenu) {
          found = this.getMenuItem(idPrefix, submenu);
        }
      }
      return found;
    };
    
    /**
     * set new title menu item
     * @param {Function} cb : callback after setState
     */
    setNewMenu = (cb = () => {}) => {
      const newMenu = cloneDeep(this.state.menu);
      this.setState(
        {
          menu: newMenu,
        },
        cb,
      );
    };
    
    enablePartialMenuOnProjectLoaded = () => {
      const editMenu = this.getMenuItem('editMenu');
      const fileMenu = this.getMenuItem('fileMenu');
    
      for (const mainMenu of fileMenu.submenu) {
        mainMenu.enabled = true;
      }
      editMenu.enabled = true;
    
      this.setNewMenu();
    };
    • 메뉴바를 가져오고 설정하는 getMenuItem, setNewMenu 함수와 활성화 설정을 담당하는 enablePartialMenuOnProjectLoaded 함수
  • 메뉴바 UI

              {this.props.menu.map((title, idx) => {
                return (
                  <div
                    className="titleMenuBarButton"
                    disabled={!title.enabled}
                    onClick={() => {
                      this.setSelectedTitleMenuId(title.id);
                    }}
                    style={
                      ...
                    }
                    key={`titleMenu${idx}`}
                  >
                    {title.label}
                  </div>
                );
              })}
    • props로 전달받은 menu를 map 하여 보여주는 방식
    • separator 인지, submenu가 있는지 등등 조건에 따라 다르게 보여줌

추가 고려사항

  • 메뉴바에서의 반응형 디자인 적용

    • 여기에서도 마찬가지로 vw, vh를 활용하여 반응형 디자인을 적용해 줌.
    • 다른 ui 요소들은 vw를 많이 사용했는데, 메뉴바에서는 vh를 많이 사용함
      • 이는 메뉴바가 너비의 영향을 덜 받는 것이 좋기 때문임
      • 프로그램의 특성상 사용자들이 가로로 긴 모니터로 길게 펼쳐서 보는 경우가 있는데, 이 때 메뉴바가 너비의 영향을 받아 너무 커져버릴 수가 있기 때문에 vw 대신 vh를 이용한 것임.
  • 메뉴바도 다양한 기능들을 포함하고 있어서 많은 테스트가 필요했음.

배우고 느낀 점

  • 메뉴바를 통해 핵심 기능들을 밖으로 꺼내놓았더니 내보내기 지표가 이전 달 대비 60% 정도 증가해서 뿌듯했다.
  • 이번 메뉴바도 그랬던 것처럼 clickable한 요소들이 많으면 사용자의 입장이서 더욱 편해지는 것 같다. 앞으로도 기능 구현 시 이런 점들까지 다양하게 고려하도록 노력해야겠다.
profile
넓고 깊은 지식을 보유한 개발자를 꿈꾸고 있습니다:) 기억 혹은 공유하고 싶은 내용들을 기록하는 공간입니다

0개의 댓글