일렉트론으로 앱 개발시 실제 view를 맡게되는 renderer process에서는 제약이 많다. 쿠키 값도 가질수 없고 다른 일렉트론 라이브러리들도 대부분 main process에서 실행되어야한다.

그래서 이번엔 todo앱을 만들어보면서 renderer process와 main process사이에서 통신을 하는 과정을 학습해보고자 한다.

우선은 main process에서 BrowserWindow를 생성하자.

let mainWindow;
function createWindow () {
  mainWindow = new BrowserWindow({width: 400, height: 760, icon: path.join(__dirname, 'app/img/icon.png')});

  mainWindow.loadURL(url.format({
    pathname: path.join(__dirname, 'app/index.html'),
    protocol: 'file:',
    slashes: true
  }));

  mainWindow.on('closed', function () {
    mainWindow = null;
  });
  mainWindow.setResizable(false);
  mainWindow.setMenu(null);
}

app에 위치한 index.html이 todo App이 될것이며 angularjs를 사용하여 작성하였다.

<!DOCTYPE html>
<!--index.html-->
<html ng-app="todoApp">
<head>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
  <link rel="stylesheet" href="app.css">
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.7/angular.min.js"></script>
  <script src="app.js"></script>
</head>

<body>
  <div id="todo" ng-controller="TodoListController as todoList">
    <div class="todoHeader">
      <h2>Todo</h2>
      <form ng-submit="todoList.addTodo()">
        <div class="input-group">
          <input class="todoInput form-control" type="text" ng-model="todoList.todoText" placeholder="add new todo here">
          <div class="input-group-addon"><input class="btn btn-primary" type="submit" value="add"></div>
        </div>
      </form>
    </div>
    <ul class="todoList">
      <li class="done-" ng-repeat="todo in todoList.todos | filter:statusFilter">
        <div class="input-group">
          <div class="input-group-addon">
            <input class="itemChk" type="checkbox" ng-model="todo.done" ng-change="todoList.doneTodo(todo)">
          </div>
          <span></span>
          <span></span>
          <div class="input-group-addon">
              <button type="button" class="btn btn-danger" ng-click="todoList.removeTodo(todo)">X</button>
          </div>
        </div>
      </li>
    </ul>
    <div class="todoFooter">
        <button type="button" class="btn btn-primary" ng-click="statusFilter={done:false}">TODO</button>
        <button type="button" class="btn btn-success" ng-click="statusFilter={done:true}">DONE</button>
        <button type="button" class="btn btn-info" ng-click="statusFilter={}">ALL</button>
    </div>
  </div>
</body>
</html>

html문서를 ng-app으로 사용하고 div#todo에 ng-controller로 TodoListController를 사용한다.

TodoListController 내부에 처음 시작시 main process와 통신하여 저장된 todo 목록을 불러오도록 작성한다.

main process와 renderer process가 통신하는 방법은 ipcRenderer와 ipcMain을 사용하면된다.

사용 방법은 아래와 같다.

// main 프로세스안에서
const {ipcMain} = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
  console.log(arg)  // "ping"이 출력됩니다.
  event.sender.send('asynchronous-reply', 'pong')
})

ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg)  // "ping"이 출력됩니다.
  event.returnValue = 'pong'
})

// renderer 프로세스(웹 페이지)안에서
const {ipcRenderer} = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // "pong"이 출력됩니다.

ipcRenderer.on('asynchronous-reply', (event, arg) => {
  console.log(arg) // "pong"이 출력됩니다.
})
ipcRenderer.send('asynchronous-message', 'ping')

이제 ipcRenderer를 통해서 loadTodo라는 channel 이름으로 main process와 통신한다.

// app.js -> renderer process
let todoList = this;
todoList.todos=[];
angular.forEach(ipcRenderer.sendSync('loadTodo'), function(todo) {
    todoList.todos.push(todo);
});

//main.js -> main process
ipcMain.on('loadTodo', (event, arg) => {
    console.log("start");
    storage.getAll(function(error, data) {
        if (error) throw error;
        event.returnValue = data;
    });
});

main process에서는 renderer process의 요청이 있을시 저장된 json 데이터들을 불러와서 전송한다.

todo 목록을 저장할때 json방식으로 저장하는데 이때 electron-json-storage를 사용한다.

storage를 통해 todo 목록을 내부 로컬스토리지에 저장하거나 불러올 수 있다.

원래는 서버에서 mongodb를 사용하여 저장하고 불러오는 방법을 사용하려 했으나 서버 없이 독자적으로 구동되는 앱을 만들기로 생각을 바꿨다.

서버와 통신하여 todo 목록을 저장하고 싶다면 로그인을 구현하여 main process에서 세션 키를 사용하여 서버와 통신하면 될 것 같다.

다시 본론으로 돌아가서 저장 및 삭제를 하는 부분을 확인해보자.

// app.js -> renderer process
todoList.addTodo = function() {
    todo={text:todoList.todoText, date:Date.now(), done:false};
    ipcRenderer.send('addTodo', todo);
    todoList.todos.push(todo);
    todoList.todoText = '';
};
todoList.removeTodo = function(todo){
    let idx = todoList.todos.findIndex(function(item){
        return item == todo;
    });
    if(idx>-1) {
        todoList.todos.splice(idx,1);
        ipcRenderer.send('removeTodo', todo.date);
    }
};
todoList.doneTodo = function(todo){
    doneTodo={text:todo.text, date:todo.date, done:todo.done};
    ipcRenderer.send('addTodo', doneTodo);
};

//main.js -> main process
ipcMain.on('addTodo', (event, arg) => {
    storage.set(String(arg.date), arg, function(error) {
        if (error) throw error;
    });
});

ipcMain.on('removeTodo', (event, arg) => {
    storage.remove(String(arg), function(error) {
        if (error) throw error;
    });
});

renderer process에서는 addTodo와 removeTodo, doneTodo가 있다.

loadTodo 통신을 통해 처음 앱 구동시 todo 목록을 불러와서 내부 json 배열로 저장한다. 그렇기에 addTodo, removeTodo 호출시 내부 배열에 대한 추가 및 삭제를 따로 한 뒤에 main process와 통신하여 로컬 스토리지에 저장된 json 데이터를 저장 및 삭제한다.

저장시 json 파일이름을 timestamp로 설정한다.

doneTodo는 todo의 done 상태가 변경됨을 저정하는 함수인데 체크 박스를 ng-model=”todo.done”로 설정했기에 변경된 상태만 내부 로컬스토리지에 저장하면 된다. 이때 addTodo 채널로 통신하는데 그 이유는 그냥 같은 이름의 json파일에 변경된 상태를 덮어쓰기 위함이다.

앱 실행 시 로컬 스토리지에 json으로 todo 목록들이 잘 저장되는것을 확인할 수 있다.

앞으로는 일렉트론의 os기능들을 더 공부해봐야겠다.