KyuHyuk Lee

KyuHyuk Lee

Software Developer

© 2020

[Electron] SQLite3를 사용한 간단한 메모장 만들기

이번 시간에는 SQLite3를 사용한 간단한 메모장을 만들어보겠습니다.

다음과 같은 파일들이 필요합니다.

├── app.css
├── app.js
├── database.db
├── index.html
├── main.js
└── package.json

우선 package.json 부터 작성해보겠습니다. electron, electron-rebuild, sqlite3 패키지가 필요합니다.
scriptsrebuild는 왜 등록했는지는 뒤에서 설명하도록 하겠습니다.
아래의 내용을 모두 작성하고 npm install 명령을 실행합니다.

{
  "name": "elememo",
  "version": "1.0.0",
  "description": "Electron Memo Application",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "rebuild": "electron-rebuild -w sqlite3 -p"
  },
  "author": "KyuHyuk Lee",
  "license": "MIT",
  "devDependencies": {
    "electron": "^8.0.0",
    "electron-rebuild": "^1.10.0"
  },
  "dependencies": {
    "sqlite3" : "^4.1.1"
  }
}

main.js는 아래와 같이 작성합니다.

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

function createWindow () {
  // 브라우저 창을 생성합니다.
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  })

  // index.html을 응용프로그램에 띄웁니다.
  win.loadFile('index.html')
}

// 이 메소드는 Electron의 초기화가 완료되고
// 브라우저 윈도우가 생성될 준비가 되었을때 호출됩니다.
// 어떤 API는 이 이벤트가 나타난 이후에만 사용할 수 있습니다.
app.whenReady().then(createWindow)

// 모든 윈도우가 닫히면 종료된다.
app.on('window-all-closed', () => {
  // macOS에서는 사용자가 명확하게 Cmd + Q를 누르기 전까지는
  // 애플리케이션이나 메뉴 바가 활성화된 상태로 머물러 있는 것이 일반적입니다.
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  // macOS에서는 dock 아이콘이 클릭되고 다른 윈도우가 열려있지 않았다면
  // 앱에서 새로운 창을 다시 여는 것이 일반적입니다.
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})

index.html은 아래와 같이 우선 app.js를 실행할 정도로만 작성합니다.

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>EleMemo</title>
    <!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>

<body>
    <script src="app.js"></script>
</body>

</html>

이제 우리가 작성한 메모가 작성될 데이터베이스 파일(SQLite3)을 생성합니다.
memo 테이블을 생성하고, contentsdata를 추가합니다.

$ sqlite3 database.db
sqlite> CREATE TABLE `memo` ( `contents` TEXT, `date` TEXT );
sqlite> .exit

SQLite3 Database Create

이제 app.js를 작성해봅시다. 위에서 만든 database.db에 데이터를 기록하고 출력하는 코드부터 작성해봅시다.

const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('database.db');

function addMemo(contents, date) {
    const query = `INSERT INTO memo(contents, date) VALUES ('${contents}', '${date}')`;
    db.serialize();
    db.each(query);
}

function listMemo() {
    db.serialize(function () {
        db.each("SELECT rowid AS id, contents, date FROM memo", function (err, row) {
            console.log(row.id + " - " + row.contents + "[" + row.date + "]");
        });
    });
}

addMemo("토요일에 어디서 놀지 생각하기", "2020-02-11");
addMemo("오후 6시 30분에 여자친구 만나기", "2020-02-12");
listMemo();

npm start로 실행해봅시다.

그런데 무슨 일일까요? Uncaught Error: Cannot find module 에러를 보이고 있습니다. 우리가 소스코드를 잘못 입력한 것일까요? 아닙니다. 아까 위에서 package.json을 작성할 때, rebuild를 넣은 게 기억나시나요? electron-rebuild 패키지로 Electron에 대해 모듈을 다시 빌드 해야 sqlite3 패키지를 사용할 수 있습니다.

EleMemo Error

npm run rebuild 명령을 한 뒤, npm start로 다시 실행해봅시다.

실행하면 아래와 같이 우리가 추가한 메모가 정상적으로 출력되는 것을 확인할 수 있습니다.

EleMemo

listMemo()를 수정하여 화면에 메모 내용이 출력되게 수정해봅시다. 우선 index.html에 메모가 띄워질 template를 만들어놓습니다.

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>EleMemo</title>
    <!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
    <link rel="stylesheet" href="app.css" />
</head>

<body>
    <div id="main-area">
        <template id="card-template">
            <div class="card">
                <div class="container">
                    <h4><b><p class="date">2020-01-01</p></b></h4>
                    <p class="contents">Elememo</p>
                </div>
            </div>
        </template>
    </div>
    <script src="app.js"></script>
</body>

</html>

그리고 app.css는 아래와 같이 작성합니다.

.card {
    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
    transition: 0.3s;
    float: left;
    width: 40%;
    margin: 20px;
}

.card:hover {
    box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
}

.container {
    padding: 2px 16px;
}

app.js에는 displayMemo()를 추가하고, listMemo()를 아래와 같이 수정합니다.

const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('database.db');

const mainArea = document.getElementById('main-area');
const template = document.querySelector('#card-template');

function addMemo(contents, date) {
    const query = `INSERT INTO memo(contents, date) VALUES ('${contents}', '${date}')`;
    db.serialize();
    db.each(query);
}

function displayMemo(contents, date) {
    let clone = document.importNode(template.content, true);
    clone.querySelector('.contents').innerText = contents;
    clone.querySelector('.date').innerText = date;
    mainArea.appendChild(clone);
}

function listMemo() {
    db.serialize(function () {
        db.each("SELECT rowid AS id, contents, date FROM memo", function (err, row) {
            displayMemo(row.contents, row.date);
        });
    });
}

listMemo();

실행하면 아래와 같이 메모 목록이 예쁘게(?) 출력됩니다!

EleMemo

메모를 추가하는 메뉴도 추가해야겠죠?
index.html을 아래와 같이 수정합니다.

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>EleMemo</title>
    <!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
    <link rel="stylesheet" href="app.css" />
</head>

<body>
    <div id="header">
        <input id="input-memo" type="text" placeholder="메모">
        <button onclick="addMemo()">등록하기</button>
    </div>
    <br>
    <div id="main-area">
        <template id="card-template">
            <div class="card">
                <div class="container">
                    <h4><b><p class="date">2020-01-01</p></b></h4>
                    <p class="contents">Elememo</p>
                </div>
            </div>
        </template>
    </div>

    <script src="app.js"></script>
</body>

</html>

inputbutton도 예쁘게 나오도록 app.css도 아래와 같이 수정합니다.

.card {
    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
    transition: 0.3s;
    float: left;
    width: 40%;
    margin: 20px;
}

.card:hover {
    box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
}

.container {
    padding: 2px 16px;
}

input {
    float: left;
    font-size: 18px;
    padding: 10px 10px 10px 5px;
    display: block;
    width: 50%;
    border: none;
    border-bottom: 1px solid #757575;
    margin: 20px;
}

input:focus {
    outline: none;
}

button {
    background: #1AAB8A;
    color: #fff;
    border: none;
    position: relative;
    height: 40px;
    font-size: 18px;
    padding: 0 1.5em;
    cursor: pointer;
    transition: 800ms ease all;
    outline: none;
    margin: 20px;
}

button:hover {
    background: #fff;
    color: #1AAB8A;
}

button:before, button:after {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    height: 2px;
    width: 0;
    background: #1AAB8A;
    transition: 400ms ease all;
}

button:after {
    right: inherit;
    top: inherit;
    left: 0;
    bottom: 0;
}

button:hover:before, button:hover:after {
    width: 100%;
    transition: 800ms ease all;
}

우리는 ‘등록하기’ 버튼을 누르면 addMemo()가 실행되도록 만들었습니다. input에 있는 내용을 저장할 수 있도록 app.js에 있는 addMemo()를 수정해봅시다.

const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('database.db');

const mainArea = document.getElementById('main-area');
const template = document.querySelector('#card-template');
const contents = document.getElementById('input-memo');

/* 날짜를 YYYY-MM-DD로 출력하기 위해 사용됩니다. */
Date.prototype.yyyymmdd = function() {
    var yyyy = this.getFullYear().toString();
    var mm = (this.getMonth() + 1).toString();
    var dd = this.getDate().toString();
    return  yyyy + "-" + (mm[1] ? mm : "0" + mm[0]) + "-" + (dd[1] ? dd : "0" + dd[0]);
}

function addMemo() {
    date = new Date().yyyymmdd();
    const query = `INSERT INTO memo(contents, date) VALUES ('${contents.value}', '${date}')`;
    db.serialize();
    db.each(query);
    contents.value = ""; /* input에 입력된 값을 없앱니다. */
    listMemo(); /* 메모가 추가되면, 화면에 새로 업데이트해줍니다. */
}

function displayMemo(contents, date) {
    let clone = document.importNode(template.content, true);
    clone.querySelector('.contents').innerText = contents;
    clone.querySelector('.date').innerText = date;
    mainArea.appendChild(clone);
}

function listMemo() {
    mainArea.innerHTML = ""; /* 메모 목록을 보여주기 전에 mainArea를 비웁니다. */
    db.serialize(function () {
        db.each("SELECT rowid AS id, contents, date FROM memo", function (err, row) {
            displayMemo(row.contents, row.date);
        });
    });
}

listMemo();

EleMemo

거의 다 완성이 되어가고 있습니다! 보니까 최신 메모가 아래에 있습니다. 최신 메모를 위로 올려주기 위해 listMemo()의 쿼리에 ORDER BY id DESC를 추가해줍니다.

function listMemo() {
    mainArea.innerHTML = ""; /* 메모 목록을 보여주기 전에 mainArea를 비웁니다. */
    db.serialize(function () {
        db.each("SELECT rowid AS id, contents, date FROM memo ORDER BY id DESC", function (err, row) {
            displayMemo(row.contents, row.date);
        });
    });
}

EleMemo

EleMemo가 완성되었습니다! 메모 수정, 삭제 기능은 각자 알아서 구현해봅시다. :smiley: