Electron

Electron, 간단한 메모장(simple notepad) 만들기

iKay 2019. 1. 6. 23:34
반응형

0. 들어가며

이제까지 포스팅했던 내용을 도대로 electron으로 간단히 메모장(notepad)을 만들어본다. 전에 사용했던 module인 dialog, ipc, menu등이 그대로 사용하되 main에서 renderer로 먼저 어떤 요청을 하기 ipcMain으로는 불가능해 이를 해결하기위해 win.webContents.send( )를 사용할 것이다.  

 

 

1. index.html

notepad를 구현하기 위한 껍데기를 index.html에 구현했다. notepad의 중심기능은 글을 쓰고 저장하고 읽는 것이다. 글을 쓰는 곳은 <textarea> 태그로 처리했고, 저장 또는 읽기 후 Main 쪽으로 텍스트 내용을 보내고 받는 것을 <script>태그에서 ipc 모듈로 처리했다.  

 

<textarea> tag는 웹브라우저에서 <form>의 요소 중 하나로 장문의 글을 입력받을 때 사용되는 tag이다. 따라서 notepad의 주기능인 글을 쓰는 곳의 역할을 충분히 할 수 있다. 그리고 <textarea>를 애플리케이션에서 사용할 때 보기좋도록 css도 아래와 같이 선언했다. css 부분은 어려운 점이 없고 설명 범위는 넘어가니 생략하겠다.

 

웹프로젝트였다면 <form> tag 안에 <textarea>를 넣고 <form>을 전송하거나, <textarea>만 jQuery의 ajax를 통해 서버에 데이터를 보내도록 처리했을 것이다. 하지만 electron 프로젝트에서는 데이터를 주고 받을 때 ipc를 사용하면된다. notepad 구현시 데이터를 주고 받을 때 크게 두 부분이 필요하다. 그것은 첫 째 main으로부터 text를 읽어 renderer로 보여주는 부분, 둘 째 notepad 본문을 저장해 main으로 text를 전송하는 부분이다.

 

첫 째 main으로부터 text를 읽어 renderer로 부여주기 위해 ipcRenderer.on('fileData', cb) 를 사용한다. 이것은 Renderer 쪽의 ipc가 'fileData' 채널을 등록하고 text를 받을 수 있도록 준비한다. 이 채널에 이벤트가 발생해 text를 받게되면 <textarea> tag에 그 text 내용을 보여주게( document.getElementById('text').value = data )끔 한다. 

 

둘 째 notepad 본문을 저장해 main으로 text를 전송하는 부분도 이와 비슷하다. 이 부분은 말로만 설명하겠다. Renderer 쪽의 ipc가 'reqFileData'의 채널에 의해 text를 저장하겠다는 이벤트를 감지하게 되면 <textarea>의 본문을 저장하고 이 것을 Main으로 ipc의 'sendContents' 채널을 통해 이 본문을 전송하는 것이다. 

 

아마 데이터 전송부분은 Main쪽 코드를 함께보면 더욱 이해가 쉬울 것이다.   

 

<!-- index.html-->

<html>

<head>
    <style>
        textarea {
            width: 100%;
            height: 100%;
            resize: none;
            border: none;
            outline: none;
            font-family: Consolas
        }
    </style>
</head>

<body>
    <script type="text/javascript">
        const { ipcRenderer } = require('electron');
        ipcRenderer.on('reqFileData', (event, data) => {
            const contents = document.getElementById('text').value;
            ipcRenderer.send('sendContents', contents);
        });
        ipcRenderer.on('fileData', (event, data) => {
            document.getElementById('text').value = data;
        });

    </script>
    <textarea name="" id="text"></textarea>
</body>

</html>

2. main.js

main.js 쪽 소스코드는 이전과 매우 비슷하다. 다른 점이 있다면 win 변수를 globally하게 선언했다. 왜냐하면 menu에서 text를 save하는 이벤트 발생 후 main에서 renderer로 text contents를 전송해달라는 요청을 하는 부분을 구현하기 위해서였다. ipc 모듈은 renderer에서 main으로 요청을 하는 것만 가능하지 main에서 renerer로 요청하는 것은 가능하지 않다. 물론 renderer에서 main으로 요청 후 main에서 renderer로 reply하는 것은 가능하다. 이것을 해결하기 위해 win 객체의 webContents.send( ) 가 필요했고 따라서 win 객체를 globally하게 사용할 필요가 있었다. 

 

text를 저장, 불러오기, 실행을 취소, 복사, 줌인/아웃 등등.... 의 기능을 application menu에 등록했음을 Menu.buildFromTemplate( )를 통해 알 수 있을 것이다.  

 

/* main.js */

const { app, BrowserWindow, Menu } = require("electron");
const menuTemplate = require("./menu/menuTemplate");
global.win = null;

function createWindow() {
  win = new BrowserWindow({ width: 800, height: 600 });
  /* load local .html file*/
  win.loadURL(`file://${__dirname}/index.html`);
}

const menu = Menu.buildFromTemplate(menuTemplate);

Menu.setApplicationMenu(menu);
app.on("ready", createWindow);

3. menuTemplate.js

application menu의 template을 정의한 곳이다. 

 

menuItem 중 'role'이라는 속성을 가진 것은 이미 electron에서 정의된 기능이라서 그대로 사용할 수 있다.  

 

'label'은 menu의 이름을 명명하는 것이다. 

 

'accelerator'는 단축키를 지정하는 것이다. 

 

'click()' 을 통해 메뉴의 기능을 스스로가 입맛대로 정의할 수 있는데, 여기서는 saveText( )와 openText( )이다. 이 두 함수는 menu중 'Save' 또는 'Open'이 마우스 또는 단축키로 선택됐을 때 텍스트 파일을 저장하거나 열도록 스스로가 구현한 부분이다. 함수 내용은 controllers.js에 구현했다. 

 

그 외에 menuItem에 대한 기능과 설명을 더 참고하기 싶으면 여기를 살펴보자. 

 

const { shell } = require("electron");
const { saveText, openText } = require("./contollers");

const menu = [
  {
    label: "Edit",
    submenu: [
      {
        label: "Save",
        accelerator: "CmdOrCtrl+S",
        click() {
          saveText();
        }
      },
      {
        label: "Oepn",
        accelerator: "CmdOrCtrl+O",
        click() {
          openText();
        }
      },
      { role: "undo" },
      { role: "redo" },
      { type: "separator" },
      { role: "cut" },
      { role: "copy" },
      { role: "paste" },
      { role: "pasteandmatchstyle" },
      { role: "delete" },
      { role: "selectall" }
    ]
  },
  {
    label: "View",
    submenu: [
      {
        label: "Larger",
        role: "zoomin"
      },
      {
        label: "Smaller",
        role: "zoomout"
      },
      {
        label: "Reset size",
        role: "resetzoom"
      },
      { type: "separator" }
    ]
  },
  {
    role: "window",
    submenu: [
      {
        lebel: "Full/Small",
        role: "togglefullscreen"
      },
      { role: "Minimize" },
      { role: "close" }
    ]
  },
  {
    role: "help",
    submenu: [
      {
        label: "Learn More",
        click() {
          shell.openExternal("https://github.com/hgs0426/electron");
        }
      }
    ]
  }
];

module.exports = menu;

 

4. controllers.js

menu 부분을 설명하면서 언급했던 saveText( ), openText( ) 가 있는 곳이다.  

 

openText( )는 menuItem 중 'Open'이 선택될 때 실행되는 함수이다. 제일 먼저 dialog.showOpenDialog가 실행돼 파일을 선택하는 dialog를 띄운다. 여담이지만 이렇게 dialog를 띄우는 것을 electron에서 이미 거의 모든 것을 모듈로써 제공하고 있기 때문에 하나하나 구현할 필요가 없다는 점이 편리한 점이라 생각한다. dialog를 띄우고 파일 선택 후 readFile( ) 함수가 실행된다. 여기서는 nodejs의 native 모듈인 fs를 통해 파일을 읽는다. 파일 읽기가 끝나면 win.webContents.send( )를 통해 파일 내용을 전달한다. saveText( ) 도 이와 매우 유사하기 때문에 구동되는 로직을 스스로 생각해보면 좋을 것 같다. 

 

const { dialog, ipcMain } = require("electron");
const fs = require("fs");

const saveText = () => {
  const saveFile = filePath => {
    win.webContents.send("reqFileData", null);
    ipcMain.on("sendContents", (event, contents) => {
      // console.log(contents);
      fs.writeFile(filePath, contents, err => {
        if (err) {
          alert("An error ocurred saving the file :" + err.message);
          return;
        }
        console.log("saved");
      });
    });
  };

  dialog.showSaveDialog(fileName => {
    if (fileName === undefined) {
      console.log("No file selected");
    } else {
      //console.log(fileName);
      saveFile(fileName);
    }
  });
};

const openText = () => {
  const readFile = filepath => {
    fs.readFile(filepath, "utf-8", (err, data) => {
      if (err) {
        alert("An error ocurred reading the file :" + err.message);
        return;
      }
      win.webContents.send("fileData", data);
    });
  };

  dialog.showOpenDialog(fileNames => {
    if (fileNames === undefined) {
      console.log("No file selected");
    } else {
      readFile(fileNames[0]);
    }
  });
};

module.exports = {
  saveText,
  openText
};

 

5. 마치며

아래 그림과 같이 텍스트를 입력하고 저장하고 불러올 수 있는 notepad를 electron으로 쉽게 만들었다. 이렇게 javascript를 통해 쉽게 web application(nodejs) 뿐만 아니라 desktop application(electron)을 통해 만들 수 있고 mobile application(react native)도 만들 수 있게 해준다. 이 점으로부터  javascript는 application을 쉽게 구현할 수 있게 해주는 언어로서 큰 장점이 있다고 생각한다. 아마 electron에 대한 포스팅은 당분간은 하지 않을 것 같다. 이 정도만 익혀도 앞으로 내게 필요한 데스크톱은 스스로 구현할 수 있을 것 같다.   

 

더 많은 기능을 직접 테스트 하고 싶다면, 아래 소스코드의 깃헙 주소에서 다운받아서 실행해 보길 바란다.  

 

 

6. 소스코드

 

https://github.com/hgs0426/electron/tree/master/notepad

 

 

7. 참고

 

[1] https://electronjs.org/docs

반응형