ElectronJS 일렉트론 강좌 5편 SQLite3 sql.js
안녕하세요?
일렉트론 강좌를 끝냈었는데요. 댓글로 CRUD 예제를 원하시는 분이 계셔서 5편까지 오게 되었네요.
일단 지난 시간 강좌는 참고로 링크 걸어 두겠습니다.
https://cpro95.tistory.com/500
https://cpro95.tistory.com/501
https://cpro95.tistory.com/502
https://cpro95.tistory.com/632
4편에서는 sql.js를 이용한 CRUD 테스트를 했는데요.
오늘은 CRUD 테스트 부분을 일렉트론 main process와 renderer process에 접목하는 방법에 대해 알아보겠습니다.
일단 Users 테이블을 만들어서 사용자 등록, 삭제, 수정을 할 예정인데요.
작성하는 방법에 주안점을 두기 위해 UI 부분은 신경 쓰지 않았습니다.
일단 우리의 일렉트론 앱의 Header 부분에 Users 부분을 추가시키겠습니다.
/src/components/Header.js
.....
<MenuItem onClick={toggleMenu} to={"/search"}>
Search
</MenuItem>
<MenuItem onClick={toggleMenu} to={"/latest"}>
Latest
</MenuItem>
<MenuItem onClick={toggleMenu} to={"/ratings"}>
Ratings
</MenuItem>
<MenuItem onClick={toggleMenu} to={"/users"}>
Users
</MenuItem>
.....
Users 메뉴를 헤더 부분에 추가했고요.
라우팅은 /users라고 쓰기로 했습니다.
그럼 라우팅을 추가하기 위해 app.js 부분도 수정할까요?
src/app.js
......
import Users from "./screens/Users";
const App = () => (
<Switch>
......
<Route exact path="/users" component={Users} />
......
</Switch>
);
......
리액트 라우팅까지 설정했으니까 이제 Users.js를 screens 폴더에 만듭시다.
본격적인 sql.js CRUD를 일렉트론에 접목해 보겠습니다.
1. Create
먼저 Create 부분입니다.
일단 /public/preload.js 에서 Create 부분을 일렉트론 메시지 생성을 해야 합니다.
const { contextBridge, ipcRenderer } = require("electron");
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld("myApi", {
send: (channel, data) => {
// whitelist channels
let validChannels = [
....
"db-create",
];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
let validChannels = [
.....
"db-return-create",
];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
removeListeners: (channel) => {
let validChannels = [
......
"db-return-create",
];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.removeAllListeners(channel);
}
},
});
위 코드에서 보듯이 db-create라고 만들었고 리턴되는 메시지는 db-return.create라고 만들었습니다.
이제 /public/electron.js파일에서 db-create 부분을 처리합시다.
ipcMain.on("db-create", (event, arg) => {
console.log("db-create => query from renderer : ", arg);
createDB()
.then((res) => event.sender.send("db-return-create", res))
.catch((error) => console.log(error));
});
위 코드를 맨 마지막에 추가하시면 됩니다.
그럼 일렉트론에서 main process에 명령을 하는데요.
바로 db-create 메시지일 때 createDB 함수를 실행하라고 합니다.
그럼 createDB를 만들어야겠죠.
기존 Node 백엔드 파일은 /db 폴더에 getData.js였는데요.
CRUD부분을 위해 /db 폴더에 crud.js 파일을 만듭시다.
const fs = require("fs");
const initSqlJs = require("sql.js");
const dbFileName2 = require("./dbconfig2");
let _rowsFromSqlDataArray = function (object) {
let data = [];
let i = 0;
let j = 0;
for (let valueArray of object.values) {
data[i] = {};
j = 0;
for (let column of object.columns) {
Object.assign(data[i], { [column]: valueArray[j] });
j++;
}
i++;
}
return data;
};
function create(callback) {
if (!fs.existsSync(dbFileName2)) {
initSqlJs().then((SQL) => {
const db = new SQL.Database();
let sqlstr =
"CREATE TABLE Users (id integer primary key, name text not null, email text unique, password text)";
db.exec(sqlstr);
const data = db.export();
const buffer = new Buffer.from(data);
fs.writeFileSync(dbFileName2, buffer);
callback([{ success: "created Table!" }]);
});
} else callback([{ error: "DB Exists" }]);
}
function createDB() {
return new Promise((resolve, reject) => {
create((res, err) => {
if (err) reject(err);
else resolve(res);
});
});
}
module.exports = { createDB };
create 관련 Nodejs 백엔드 코드는 지난 시간에 작성한 걸 그대로 적용했습니다.
참고로 dbFileName2를 위해서 dbconfig2.js 파일을 새로 만들었습니다.
왜냐하면 파일 이름을 새롭게 하기 위해서죠.
/db/dbconfig2.js
const path = require("path");
const isDev = require("electron-is-dev");
const Store = require("electron-store");
const store = new Store();
var dbFileName2;
if (store.has("dbFileName")) {
// console.log(store.get('dbFileName'));
// store.get returns array, so (xxx)[]0
dbFileName2 = store.get("dbFileName2")[0];
module.exports = dbFileName2;
} else {
if (isDev && process.argv.indexOf("--noDevServer") === -1) {
dbFileName2 = path.join(
path.dirname(__dirname),
"extraResources",
"crudtest.db"
);
} else {
dbFileName2 = path.join(
process.resourcesPath,
"extraResources",
"crudtest.db"
);
}
module.exports = dbFileName2;
}
crudtest.db 파일 이름으로 지정했습니다.
이제 백엔드 부분은 완성했으니까요.
일렉트론에서 UI 부분을 작성해 볼까요?
/src/screens/Users.js
<Flex w="auto" justify="center" p={[0, 10, 10]}>
<VStack>
<Heading>Users Admin Dashboard!</Heading>
<Heading>Test of sql.js CRUD(create read update delete)</Heading>
<Flex p={2} align="center" justify="center">
<Stack direction="row" spacing={10}>
<Button
colorScheme="facebook"
onClick={() => window.myApi.send("db-create", "Create Table")}
>
Create Table
</Button>
</Stack>
</Flex>
위 그림처럼 Create Table 이란 버튼을 만들었고 onClick 이벤트일 때 window.myApi.send("db-create", "Create Table")라고 main process에 명령을 보냈습니다.
버튼을 누르면 우리의 DB 파일인 crudtest.db가 생성될 겁니다.
DB가 생성만 됐지 아직은 데이터가 없네요.
2. Read
CRUD의 두 번째인 Read 부분을 알아보겠습니다.
Read 메시지는 "db-read"와 "db-return-read"로 정했습니다.
이 메시지를 preload.js에 추가하시고 그다음 electron.js에서 "db-read" 메시지를 캐치하는 코드를 만들어 봅시다.
ipcMain.on("db-read", (event, arg) => {
console.log("db-read => query from renderer : ", arg);
readDB()
.then((res) => event.sender.send("db-return-read", res))
.catch((error) => console.log(error));
});
위 코드를 보면 crud.js 파일에서 readDB 함수가 필요하겠네요.
function read(callback) {
initSqlJs().then((SQL) => {
SQL.dbOpen = function (databaseFileName) {
try {
return new SQL.Database(fs.readFileSync(databaseFileName));
} catch (error) {
console.log("Can't open database file.", error.message);
return null;
}
};
let db = SQL.dbOpen(dbFileName2);
let res = db.exec("SELECT * FROM Users;");
// console.log(res.legth);
if (res.length === 0) callback([{ error: "No Data found!" }]);
else {
res = _rowsFromSqlDataArray(res[0]);
callback(res);
}
});
}
function readDB() {
return new Promise((resolve, reject) => {
read((res, err) => {
if (err) reject(err);
else resolve(res);
});
});
}
module.exports = { createDB, readDB };
Read를 하려면 데이터가 있어야 하는데요.
여기서 Insert 부분도 같이 작성해 보겠습니다.
preload.js에서 "db-insert"와 "db-return-insert"부분을 추가하고 electron.js에 다음 코드를 작성합시다.
ipcMain.on("db-insert", (event, { name, email, password }) => {
console.log("db-insert => query from renderer : ", name, email, password);
insertDB(name, email, password)
.then((res) => event.sender.send("db-return-insert", res))
.catch((error) => console.log(error));
});
insertDB 부분을 crud.js 파일에 추가합시다.
function insert(name, email, password, callback) {
initSqlJs().then((SQL) => {
SQL.dbOpen = function (databaseFileName) {
try {
return new SQL.Database(fs.readFileSync(databaseFileName));
} catch (error) {
console.log("Can't open database file.", error.message);
return null;
}
};
let db = SQL.dbOpen(dbFileName2);
db.exec(
`INSERT INTO Users(name, email, password) VALUES("${name}","${email}","${password}");`
);
const data = db.export();
const buffer = new Buffer.from(data);
fs.writeFileSync(dbFileName2, buffer);
callback([{ success: "created User!" }]);
});
}
function insertDB(name, email, password) {
return new Promise((resolve, reject) => {
insert(name, email, password, (res, err) => {
if (err) reject(err);
else resolve(res);
});
});
}
module.exports = { createDB, readDB, insertDB };
이제 Users.js 부분에 UI 부분을 추가합시다.
import React, { useState, useEffect } from "react";
import {
Flex,
VStack,
Heading,
Stack,
Button,
HStack,
Box,
Text,
Input,
} from "@chakra-ui/react";
function Users() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
useEffect(() => {
window.myApi.send("db-read", "Read Users Table");
}, [loading]);
window.myApi.receive("db-return-read", (data) => {
// console.log(`Received data from main process : db-return-read`);
// console.table(data);
setData(data);
window.myApi.removeListeners("db-return-read");
});
window.myApi.receive("db-return-create", (data) => {
// console.log(`Received data from main process : db-return-create`);
// console.table(data);
window.myApi.removeListeners("db-return-create");
setLoading(!loading);
});
function handleSubmit() {
// console.log(name, email, password);
window.myApi.send("db-insert", { name, email, password });
setName("");
setEmail("");
setPassword("");
setLoading(!loading);
}
return (
<Flex w="auto" justify="center" p={[0, 10, 10]}>
<VStack>
<Heading>Users Admin Dashboard!</Heading>
<Heading>Test of sql.js CRUD(create read update delete)</Heading>
<Flex p={2} align="center" justify="center">
<Stack direction="row" spacing={10}>
<Button
colorScheme="facebook"
onClick={() => window.myApi.send("db-create", "Create Table")}
>
Create Table
</Button>
<Button colorScheme="twitter" onClick={() => setLoading(!loading)}>
Read Table
</Button>
</Stack>
</Flex>
<Heading p={2}>User Lists</Heading>
{data.length !== 0 ? (
data[0].error !== undefined ? (
<span>{data[0].error}</span>
) : (
<></>
)
) : (
<></>
)}
{data.length === 0 ? (
<></>
) : data[0].error === undefined ? (
data.map((i) => (
<HStack key={i.id} align={"top"} p={2}>
<HStack align={"center"}>
<Text fontWeight={600}>
{i.name} / {i.email}
</Text>
</HStack>
<Box color={"green.400"} px={2}>
<Button
color="white"
bgColor="red.500"
mx={2}
onClick={() => handleDeleteButton(i.id)}
>
Delete
</Button>
</Box>
</HStack>
))
) : (
<></>
)}
<Heading>Add Users</Heading>
<Flex align="center" justify="center">
<Stack direction="row" spacing={4}>
<form onSubmit={handleSubmit}>
<Input
placeholder="name"
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
my={1}
/>
<Input
placeholder="email"
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
my={1}
/>
<Input
placeholder="password"
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
my={1}
/>
<Button colorScheme="whatsapp" mx="2" w="40" type="submit">
Add
</Button>
</form>
</Stack>
</Flex>
</VStack>
</Flex>
);
}
export default Users;
이제 추가해 볼까요?
Add 버튼을 누르면 위에 Form에 적었던 내용대로 DB에 저장이 됩니다.
한번 볼까요?
crudtest.db 파일에 실제 저장되고 있습니다.
3. Delete
update를 하기 전에 Delete 부분을 먼저 구현해 보겠습니다.
preload.js에 "db-delete"와 "db-return-delete"를 추가하고 electron.js에 다음 코드를 추가합시다.
ipcMain.on("db-delete", (event, arg) => {
console.log("db-delete => query from renderer : ", arg);
deleteItemDB(arg)
.then((res) => event.sender.send("db-return-delete", res))
.catch((error) => console.log(error));
});
이제 crud.js 파일에 다음 함수를 추가합니다.
function deleteItem(id, callback) {
initSqlJs().then((SQL) => {
SQL.dbOpen = function (databaseFileName) {
try {
return new SQL.Database(fs.readFileSync(databaseFileName));
} catch (error) {
console.log("Can't open database file.", error.message);
return null;
}
};
let db = SQL.dbOpen(dbFileName2);
db.exec(`DELETE FROM Users WHERE id = "${id}";`);
const data = db.export();
const buffer = new Buffer.from(data);
fs.writeFileSync(dbFileName2, buffer);
callback([{ success: "deleted User!" }]);
});
}
function deleteItemDB(id) {
return new Promise((resolve, reject) => {
deleteItem(id, (res, err) => {
if (err) reject(err);
else resolve(res);
});
});
}
module.exports = { createDB, readDB, insertDB, deleteItemDB };
이제 UI 부분을 볼까요?
아까 위에서 본 Insert 할 때의 Users.js 파일에 Delete 관련 코드가 있습니다.
<Button
color="white"
bgColor="red.500"
mx={2}
onClick={() => handleDeleteButton(i.id)}
>
Delete
</Button>
즉, 아이템 옆에 있는 Delete 버튼을 클릭하면 handleDeleteButton 함수를 실행하는 건데요.
이제 이 handleDeleteButton 함수를 만들어 볼까요?
function handleDeleteButton(id) {
// console.log(id);
window.myApi.send("db-delete", id);
setLoading(!loading);
}
되게 간단합니다.
삭제되는지 테스트해 보십시오.
4. Update
이제 마지막으로 Update입니다.
preload.js에 "db-update"와 "db-return-update" 부분을 추가하고 electron.js에 다음 코드를 추가합시다.
ipcMain.on("db-update", (event, arg) => {
console.log("db-update => query from renderer : ", arg);
updateItemDB(arg)
.then((res) => event.sender.send("db-return-update", res))
.catch((error) => console.log(error));
});
이제 crud.js 파일에 update관련 함수를 작성합시다.
function updateItem(updateID, name, email, password, callback) {
initSqlJs().then((SQL) => {
SQL.dbOpen = function (databaseFileName) {
try {
return new SQL.Database(fs.readFileSync(databaseFileName));
} catch (error) {
console.log("Can't open database file.", error.message);
return null;
}
};
let db = SQL.dbOpen(dbFileName2);
db.exec(
`UPDATE Users SET name = "${name}", email = "${email}", password = "${password}" WHERE id = "${updateID}"`
);
const data = db.export();
const buffer = new Buffer.from(data);
fs.writeFileSync(dbFileName2, buffer);
callback([{ success: "updated User!" }]);
});
}
function updateItemDB({ updateID, name, email, password }) {
return new Promise((resolve, reject) => {
updateItem(updateID, name, email, password, (res, err) => {
if (err) reject(err);
else resolve(res);
});
});
}
module.exports = { createDB, readDB, insertDB, deleteItemDB, updateItemDB };
이제 UI 부분을 볼까요?
import React, { useState, useEffect } from "react";
import {
Flex,
VStack,
Heading,
Stack,
Button,
HStack,
Box,
Text,
Input,
} from "@chakra-ui/react";
function Users() {
const [data, setData] = useState([]);
const [isUpdate, setIsUpdate] = useState(false);
const [updateID, setUpdateID] = useState(0);
const [loading, setLoading] = useState(true);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
useEffect(() => {
window.myApi.send("db-read", "Read Users Table");
}, [loading]);
window.myApi.receive("db-return-read", (data) => {
// console.log(`Received data from main process : db-return-read`);
// console.table(data);
setData(data);
window.myApi.removeListeners("db-return-read");
});
window.myApi.receive("db-return-create", (data) => {
// console.log(`Received data from main process : db-return-create`);
// console.table(data);
window.myApi.removeListeners("db-return-create");
setLoading(!loading);
});
window.myApi.receive("db-return-update", (data) => {
// console.log(`Received data from main process : db-return-update`);
// console.table(data);
window.myApi.removeListeners("db-return-update");
setLoading(!loading);
});
function handleSubmit() {
if (!isUpdate) {
// console.log(name, email, password);
window.myApi.send("db-insert", { name, email, password });
setName("");
setEmail("");
setPassword("");
setLoading(!loading);
} else {
window.myApi.send("db-update", { updateID, name, email, password });
setName("");
setEmail("");
setPassword("");
setLoading(!loading);
setIsUpdate(!isUpdate);
}
}
function handleDeleteButton(id) {
// console.log(id);
window.myApi.send("db-delete", id);
setLoading(!loading);
}
function handleUpdateButton(id, name, email, password) {
setIsUpdate(!isUpdate);
setUpdateID(id);
setName(name);
setEmail(email);
setPassword(password);
}
return (
<Flex w="auto" justify="center" p={[0, 10, 10]}>
<VStack>
<Heading>Users Admin Dashboard!</Heading>
<Heading>Test of sql.js CRUD(create read update delete)</Heading>
<Flex p={2} align="center" justify="center">
<Stack direction="row" spacing={10}>
<Button
colorScheme="facebook"
onClick={() => window.myApi.send("db-create", "Create Table")}
>
Create Table
</Button>
<Button colorScheme="twitter" onClick={() => setLoading(!loading)}>
Read Table
</Button>
</Stack>
</Flex>
<Heading p={2}>User Lists</Heading>
{data.length !== 0 ? (
data[0].error !== undefined ? (
<span>{data[0].error}</span>
) : (
<></>
)
) : (
<></>
)}
{data.length === 0 ? (
<></>
) : data[0].error === undefined ? (
data.map((i) => (
<HStack key={i.id} align={"top"} p={2}>
<HStack align={"center"}>
<Text fontWeight={600}>
{i.name} / {i.email}
</Text>
</HStack>
<Box color={"green.400"} px={2}>
<Button
color="white"
bgColor="red.500"
mx={2}
onClick={() => handleDeleteButton(i.id)}
>
Delete
</Button>
{!isUpdate ? (
<Button
color="white"
bgColor="yellow.500"
onClick={() =>
handleUpdateButton(i.id, i.name, i.email, i.password)
}
>
Update
</Button>
) : (
<Button
color="white"
bgColor="cyan.500"
onClick={() => {
setName("");
setEmail("");
setPassword("");
setIsUpdate(!isUpdate);
}}
>
Cancel Update
</Button>
)}
</Box>
</HStack>
))
) : (
<></>
)}
<Heading>Add Users</Heading>
<Flex align="center" justify="center">
<Stack direction="row" spacing={4}>
<form onSubmit={handleSubmit}>
<Input
placeholder="name"
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
my={1}
/>
<Input
placeholder="email"
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
my={1}
/>
<Input
placeholder="password"
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
my={1}
/>
<Button colorScheme="whatsapp" mx="2" w="40" type="submit">
Add
</Button>
<Button
color="white"
bgColor="teal.600"
w="40"
type="submit"
disabled={!isUpdate}
onClick={() => setIsUpdate(true)}
>
Update
</Button>
</form>
</Stack>
</Flex>
</VStack>
</Flex>
);
}
export default Users;
위 코드가 최종 Users.js 코드입니다.
delete 버튼 옆에 있는 update 버튼을 누르면 위 그림과 같이 작동합니다.
그리고 kihun2라고 고치고 그 아래 update 버튼을 누르면 update가 진행됩니다.
kihun2라고 바뀐 게 보이시죠.
Update 부분도 잘 작동되네요.
이상으로 SQL CRUD 부분을 일렉트론에서 구현해 봤습니다.
그럼.