안녕하세요?
지난 시간에 이어 2편 진행토록 하겠습니다.
1편: https://cpro95.tistory.com/500
2편에서는 Electron 개발에 본격적으로 들어가 보도록 하겠습니다.
그러면 ElectronJS와 ReactJS Template가 필요한데요.
제가 예전에 올린 아래 글 보시면 최신 버전으로 ElectronJS와 ReactJS 설치하는 법이 나옵니다.
https://cpro95.tistory.com/185
1. 앱 설치
일단 다음과 같이 실행해 볼까요?
npx create-react-app electron-sqlite3-demo
그리고 electron 관련 파일을 설치하겠습니다.
npm install electron electron-builder concurrently wait-on cross-env electron-is-dev
npm install @chakra-ui/react @chakra-ui/icons @emotion/react @emotion/styled framer-motion
npm install sql.js xml2json-light
첫 번째 설치하는 패키지 각각의 설명은 본인의 "최신 버전으로 React + Typescript + Electron 개발하기" 글을 보시면 잘 설명되어 있습니다.
두 번째 설치한 거는 바로 Chakra-UI 관련입니다. 제가 요즘 빠져있는 ReactJS UI Library입니다.
세 번째 설치한 거는 백엔드에서 쓸 sql.js 와 XML 파일을 파싱 할 xml2 json-light 패키지입니다.
그럼 이제 electron.js 파일을 만들어 볼까요?
/public/electron.js
const { app, BrowserWindow } = require("electron");
const path = require("path");
const isDev = require("electron-is-dev");
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 900,
height: 680,
minWidth: 370,
minHeight: 470,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
devTools: isDev,
nativeWindowOpen: true,
},
});
mainWindow.loadURL(
isDev
? "http://localhost:3000"
: `file://${path.join(__dirname, "../build/index.html")}`
);
if (isDev) {
mainWindow.webContents.openDevTools({ mode: "detach" });
}
mainWindow.setResizable(true);
mainWindow.on("closed", () => (mainWindow = null));
mainWindow.focus();
}
app.on("ready", createWindow);
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (mainWindow === null) {
createWindow();
}
});
/package.json
{
"name": "electron-sqlite3-demo",
"version": "0.1.0",
"private": true,
"author": "cpro95",
"main": "public/electron.js",
"homepage": "./",
"build": {
"productName": "electron-test-app",
"asar": true,
"appId": "org.cpro95.electron-test-app"
},
"scripts": {
"react-start": "react-scripts start",
"react-build": "react-scripts build",
"react-test": "react-scripts test",
"react-eject": "react-scripts eject",
"start": "concurrently \"cross-env NODE_ENV=development BROWSER=none yarn react-start\" \"wait-on http://localhost:3000 && electron .\"",
"build": "yarn react-build && electron-builder",
"release": "yarn react-build && electron-builder --publish=always"
},
"dependencies": {
"@chakra-ui/icons": "^1.0.15",
"@chakra-ui/react": "^1.6.7",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"electron-is-dev": "^2.0.0",
"framer-motion": "^4.1.17",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"sql.js": "^1.6.1",
"web-vitals": "^1.0.1",
"xml2json-light": "^1.0.6"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"concurrently": "^6.2.1",
"cross-env": "^7.0.3",
"electron": "^14.0.1",
"electron-builder": "^22.11.7",
"wait-on": "^6.0.0"
}
}
package.json 파일을 보시면 npm start로 개발서버를 돌리는 걸 볼 수 있습니다.
일단 기본적으로 Create-React-App이 만든 스타일 관련 파일을 다 지워주시고, 시작하겠습니다.
/src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { ChakraProvider } from "@chakra-ui/react";
import Layout from "./components/Layout";
ReactDOM.render(
<React.StrictMode>
<ChakraProvider resetCSS>
<Layout>
<App />
</Layout>
</ChakraProvider>
</React.StrictMode>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
/src/App.js
import { Flex, VStack, Heading, Text, Button } from "@chakra-ui/react";
function App() {
return (
<Flex w="auto" align="center" justify="center" p={[0, 10, 20]}>
<VStack>
<Heading>Welcome</Heading>
<Text>Electon + React + Sqlite3 + Chakra-UI</Text>
<Button size="md" colorScheme="whatsapp">
Welcome
</Button>
</VStack>
</Flex>
);
}
export default App;
/src/components/Layout.js
import React from "react";
import { Container } from "@chakra-ui/react";
import Header from "./Header";
export default function LayoutPage(props) {
return (
<Container maxW="container.lg">
<Header />
{props.children}
</Container>
);
}
/src/components/Header.js
import { Flex, Text, Box, Stack, Link, Heading } from "@chakra-ui/react";
import { useState } from "react";
import { DarkModeSwitch } from "./DarkModeSwitch";
import { CloseIcon, HamburgerIcon } from "@chakra-ui/icons";
const MenuItem = ({ children, onClick, to = "/" }) => {
return (
<Text
ms={{ base: 2, sm: 2, md: 2, xl: 2 }}
mr={{ base: 2, sm: 2, md: 2, xl: 2 }}
display="block"
onClick={onClick}
>
<Link colorScheme="facebook" to={to}>{children}</Link>
</Text>
);
};
const Header = () => {
const [show, setShow] = useState(false);
const toggleMenu = () => setShow(!show);
return (
<Flex
p={2}
as="nav"
align="center"
alignItems="center"
justify="space-between"
wrap="wrap"
w="100%"
>
<Box w="200px">
<Heading>
<Link colorScheme="facebook" to="/">Home</Link>
</Heading>
</Box>
<Box display={{ base: "block", md: "none" }} onClick={toggleMenu}>
{show ? <CloseIcon /> : <HamburgerIcon />}
</Box>
<Box
display={{ base: show ? "block" : "none", md: "block" }}
flexBasis={{ base: "100%", md: "auto" }}
>
<Stack
spacing={8}
align="center"
justify={["center", "space-between", "flex-end", "flex-end"]}
direction={["column", "row", "row", "row"]}
pt={[4, 4, 0, 0]}
>
<MenuItem onClick={toggleMenu} to={"/link1"}>
Link1
</MenuItem>
<MenuItem onClick={toggleMenu} to={"/link2"}>
Link2
</MenuItem>
<DarkModeSwitch />
</Stack>
</Box>
</Flex>
);
};
export default Header;
/src/components/DarkModeSwitch.js
import { useColorMode, IconButton } from "@chakra-ui/react";
import { MoonIcon, SunIcon } from "@chakra-ui/icons";
export const DarkModeSwitch = () => {
const { colorMode, toggleColorMode } = useColorMode();
return (
<IconButton
aria-label="Toggle Dark Switch"
icon={colorMode === "dark" ? <SunIcon /> : <MoonIcon />}
onClick={toggleColorMode}
ms={{ base: 2, sm: 2, md: 2, xl: 2 }}
mr={{ base: 2, sm: 2, md: 2, xl: 2 }}
display="block"
w={10}
h={10}
/>
);
};
Layout이랑 Components 들은 제 React 관련 글 중 아래 글에 잘 나와 있습니다.
https://cpro95.tistory.com/493
이제 실행 화면을 볼까요?
우리의 ElectronJS 앱이 잘 실행되고 있네요.
오른쪽 상단 Light Mode를 클릭하면
이렇게 나옵니다. 정말 멋지지 않나요? Chakra-UI의 멋진 기능입니다.
이제 본격적으로 ElectronJS의 백엔드 부분에 대해 알아보겠습니다.
우리가 지난 시간에 만들었던 db 관련 파일을 db 폴더에 옮기겠습니다.
/db/getdata.js
const fs = require("fs");
const initSqlJs = require("sql.js");
const dbFileName = require('./dbconfig');
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;
};
const sqlFilterXml2Json = (movies) => {
// movies.c08, c20 is the type of xml
// movies is array
// if movies is object, below is error
const parser = require("xml2json-light");
movies.forEach((row) => {
if (row.c08 !== "") {
var poster = parser.xml2json(row.c08);
// console.log(poster);
var poster_temp = [];
if (Array.isArray(poster.thumb)) {
poster.thumb.map((i) => {
if (i.aspect === "poster") {
poster_temp.push(i);
} else if (i.aspect === undefined) {
poster_temp.push(i);
}
// console.log(poster_temp);
});
} else {
poster_temp.push(poster.thumb);
}
row.c08 = poster_temp[0].preview;
}
if (row.c20 !== "") {
var fanart = parser.xml2json(row.c20);
// console.log(fanart);
var fanart_temp = [];
if (Array.isArray(fanart.fanart.thumb)) {
fanart.fanart.thumb.map((i) => {
if (i.aspect === "fanart") {
fanart_temp.push(i);
} else if (i.aspect === undefined) {
fanart_temp.push(i);
}
});
} else {
fanart_temp.push(fanart.fanart.thumb);
}
// console.log(fanart_temp);
row.c20 = fanart_temp[0].preview;
}
});
if (Object.keys(movies).length === 0) {
console.log("No data found");
} else {
// console.log('return movies');
return movies;
}
};
const findAll = (stmt, 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(dbFileName);
var res = db.exec(stmt);
// console.log(res.legth);
if (res.length === 0) callback([{ error: "No Data found!" }]);
else {
res = _rowsFromSqlDataArray(res[0]);
res = sqlFilterXml2Json(res);
callback(res);
db.close();
}
});
};
const findAll2 = (stmt, 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(dbFileName);
var res = db.exec(stmt);
// console.log(res.legth);
if (res.length === 0) callback([{ error: "No Data found!" }]);
else {
res = _rowsFromSqlDataArray(res[0]);
callback(res);
db.close();
}
});
};
const getData = (query) => {
// var stmt = `select idMovie, c00, c01, c03, c08, c19, c20, premiered, strPath,rating, uniqueid_value from movie_view where c00 like '%${query}%' order by idMovie desc`;
// console.log(query);
return new Promise((resolve, reject) => {
findAll(query, (res, err) => {
if (err) reject(err);
else resolve(res);
});
});
};
/*
* getData2 : query without poster, thumbnail (no sqlFilterXml2Json function)
*/
const getData2 = (query) => {
return new Promise((resolve, reject) => {
findAll2(query, (res, err) => {
if (err) reject(err);
else resolve(res);
});
});
};
module.exports = { getData, getData2 };
위 코드에서 dbFileName을 새로 정했습니다.
/db/dbconfig.js
const path = require('path');
const isDev = require('electron-is-dev');
const Store = require('electron-store');
const store = new Store();
var dbFileName;
if (store.has('dbFileName')) {
// console.log(store.get('dbFileName'));
// store.get returns array, so (xxx)[]0
dbFileName = store.get('dbFileName')[0];
module.exports = dbFileName;
} else {
if (isDev && process.argv.indexOf('--noDevServer') === -1) {
dbFileName = path.join(
path.dirname(__dirname),
'extraResources',
'MyVideos116.db'
);
} else {
dbFileName = path.join(
process.resourcesPath,
'extraResources',
'MyVideos116.db'
);
}
module.exports = dbFileName;
}
dbconfig.js 파일은 우리의 DB 파일을 불러오는데요.
일단 MyVideos116.db 파일을 electron-store를 통해 저장하고 그 위치를 나중에 읽어오는 로직입니다.
우리는 MyVideos116.db 파일을 ElectronJS 앱에 통합하려고 extraResources 폴더에 저장하겠습니다.
/extraResources/MyVideos116.db
자, 이제 DB 관련 파일도 모두 완성되었습니다.
2. IPC 이해하기
이제 ElectronJS에서 db 관련 함수를 호출해 보겠습니다.
일단 App.js 파일을 수정해 보겠습니다.
import { Flex, VStack, Heading, Text, Button } from "@chakra-ui/react";
import { getData, getData2 } from "../db/getdata";
뭔가 이상하죠?
네. 맞습니다.
ReactJS 앱은 src 폴더를 벗어날 수 없습니다.
그럼 우리의 db 폴더를 src 폴더 안으로 넣을까요?
아닙니다. 그럴 필요 없습니다. 원래부터 안 되는 거니까요?
우리의 sql.js 부분은 NodeJS 로직입니다.
ElectronJS가 불러온 ReactJS앱 상에서는 작동하지 않습니다.
그럼 왜 그럴까요?
ElectronJS의 구조에 대해 먼저 이해하는 게 필요합니다.
ElectronJS는 두 개의 Process로 실행되는데요.
바로 main process와 renderer process입니다.
우리가 public 폴더에 만든 electron.js 파일이 바로 main process에 해당됩니다.
main process에서 mainWindow.loadURL 함수를 통해 renderer process를 불어왔는데요.
이 renderer process에서 ReactJS 앱을 로딩시키고 있습니다.
main process는 그냥 NodeJS 앱입니다.
그래서 import 대신 require 방식으로 모듈을 불러와야 되고, 모든 NodeJS API를 사용할 수 있습니다.
그래서 우리가 만든 db 모듈인 getData, getData2는 바로 이 main process에서 사용해야만 합니다.
그러면 main process에서 돌린 함수의 결과값을 어떻게 renderer process에 전달할까요?
바로 ElectronJS에서 제공하는 IPC를 통해서입니다.
IPC는 inter process communication의 약자인데 초창기 버전의 ElectronJS에는 IPC 한 개의 모듈만 있었는데 최근에는 main process 용 ipcMain과 renderer process 용 ipcRenderer 모듈이 있습니다.
그럼 이 두 개의 모듈을 이용해서 ElectronJS가 main process와 renderer process 간 커뮤니케이션하는 방법에 대해 알아보겠습니다.
// In main process.
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.reply('asynchronous-reply', 'pong')
})
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.returnValue = 'pong'
})
// In renderer process (web page).
// NB. Electron APIs are only accessible from preload, unless contextIsolation is disabled.
// See https://www.electronjs.org/docs/tutorial/process-model#preload-scripts for more details.
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log(arg) // prints "pong"
})
ipcRenderer.send('asynchronous-message', 'ping')
ElectronJS 홈페이지에 있는 예제입니다.
커뮤니케이션은 Renderer process에서 main process로 이동합니다.
보시면 비동기 방식(Asynchronous)과 동기 방식(Synchronous)이 있습니다.
먼저, 동기 방식입니다.
Renderer Process에서 ipcRenderer.sendSync('synchronous-message', 'ping');라고 보내면 바로 main process에서 ipcMain.on 함수를 통해 그 결괏값을 리턴합니다.
ipcMain.on 함수는 event와 arg 인수를 갖는데 event의 returnValue 값을 통해서 다시 Renderer process로 값을 전달할 수 있습니다.
당연히 arg 인수는 renderer Process에서 보낸 신호가 되겠죠.
동기방식은 sendSync 하면 바로 리턴될 때까지 프로세스가 멈춰있습니다.
그래서 동기 방식은 특별한 경우가 아니면 잘 쓰이지 않고요.
바로 비동기 방식이 가장 많이 쓰입니다. 비동기 방식이야 말로 웹 방식에 가장 적합하기 때문입니다.
비동기 방식에 대해 알아보겠습니다.
비동기 방식은 ipcRenderer.send('asynchronous-message', 'ping')처럼 send 함수를 사용합니다.
그러면 main process에서는 똑같은 함수인 ipcMain.on 함수를 이용하는데 이때 리턴은 Promise를 리턴해야 하기 때문에 reply 함수를 쓰고 event.reply('asynchronous-reply', 'pong')처럼 메시지 이름을 지정할 수 있습니다.
여기서 메시지 이름을 지정한 이유는 바로 이 메시지를 받아서 Renderer Process에서 처리해야 하기 때문이죠.
renderer process에서는 바로 ipcRenderer.on('asynchronous-reply', (event, arg) => { console.log(arg) // prints "pong" })처럼 ipcRenderer.on 함수를 씁니다.
ipcMain.on 함수와 비슷합니다.
그럼, 일렉트론의 IPC에 대해 살펴보았는데 본격적으로 우리가 만든 db 모듈을 적용해 보겠습니다.
3. IPC 적용하기
먼저, public 폴더의 electron.js 파일을 수정하겠습니다.
const { app, BrowserWindow, ipcMain } = require("electron");
const path = require("path");
const isDev = require("electron-is-dev");
const { getData, getData2 } = require("../db/getdata");
맨 윗부분입니다.
getData , getData2 모듈을 불러왔습니다.
그리고 electron.js 파일 맨 아래에 아래 코드를 넣어주세요.
ipcMain.on("latest-query", (event, arg) => {
console.log("query from renderer : ", arg);
getData2(arg)
.then((res) => event.sender.send("sql-return-latest", res))
.catch((error) => console.log(error));
});
ipcMain.on 함수에서 메시지는 'latest-query'입니다.
즉, renderer process에서 ipcRenderer.send('latest-query', arg)를 실행한 게 됩니다.
ipcRenderer.send('latest-query', arg)에서 arg 부분에 query를 주게 되면 main Process에서 ipcMain.on에 의해 getData2 함수는 query가 인수가 되어 실행됩니다.
그러면 그 결괏값을 다시 event.sender.send 함수로 'sql-return-latest' 메시지로 renderer process에 보내게 됩니다.
그럼 renderer process에서는
ipcRenderer.on('sql-return-latest', (event, arg) => {
setData(arg);
ipcRenderer.removeAllListeners('sql-return-latest');
});
위 코드처럼 ReactJS의 useState를 이용해서 setData에 저장하고 그 state를 이용해서 ReactJS에 뿌려주게 됩니다.
그럼 한번 만들어 볼까요?
/src/App.js
const [data, setData] = useState([]);
useEffect(() => {
ipcRenderer.send(
'latest-query',
'select idMovie, c00, premiered, rating from movie_view order by premiered desc'
);
}, []);
ipcRenderer.on('sql-return-latest', (event, arg) => {
setData(arg);
ipcRenderer.removeAllListeners('sql-return-latest');
});
return (
<div>
<UnorderedList>
{data &&
data.map((i) => <ListItem key={i.idMovie}>{i.c00}</ListItem>)}
</UnorderedList>
</div>
);
실행해 보면 에러가 납니다.
왜냐하면 ipcRenderer 이 없다고 합니다.
그러고 보니 ReactJS 쪽에서 ipcRenderer를 import 안 했네요.
import { ipcRenderer } from 'electron';
이렇게 하면 될 거 같은데 역시나 안됩니다.
ElectronJS 작년 버전까지는 다음과 같이 하면 가능했었습니다.
const { ipcRenderer } = window.require('electron');
window라는 글로벌 객체에서 로드하는 방식이죠.
그런데 최신 버전 ElectronJS에서는 이 방법도 에러가 납니다.
보안에 취약하다는 의미이기 때문입니다. ipc 커뮤니케이션 간에 data가 탈취될 수 있다고 보기 때문입니다.
그럼 어떻게 해야 할까요?
ElectronJS에서 추천하는 방법은 contextBridge API를 이용하는 방법입니다.
contextBridge API는 바로 main process에서 일정 API 만 renderer process에 연결시켜주는 역할을 합니다.
먼저 /public/electron.js 코드를 조금 고쳐야 합니다.
function createWindow() {
mainWindow = new BrowserWindow({
width: 900,
height: 680,
minWidth: 370,
minHeight: 470,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
devTools: isDev,
nativeWindowOpen: true,
preload: path.join(app.getAppPath(), "/public/preload.js"),
},
});
BrowserWindow에 preload 옵션을 넣어줘야 합니다.
preload: path.join(app.getAppPath(), "/public/preload.js"),
이 preload 옵션은 ElectronJS가 브라우저 윈도를 만들 때 미리 로드한다는 뜻입니다.
이 preload.js에서 contextBridge API 관련 일을 처리해 줘야 합니다.
그럼 /public/preload.js 파일을 만들어 봅시다.
/public/preload.js
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 = ["latest-query"];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
let validChannels = ["sql-return-latest"];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
removeListeners: (channel) => {
let validChannels = ["sql-return-latest"];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.removeAllListeners(channel);
}
},
});
contextBridge API의 exposeInMainWorld 함수를 이용해 renderer process에 보여줄 자신만의 api를 만드는 겁니다.
위 코드에서 보면 자신만의 api 이름은 myApi이고, 이 api는 객체 안에서 함수를 설정하는 방식으로 작성되어 있습니다.
즉, myApi.send 함수, myApi.receive 함수, myApi.removeListeners 함수가 설정되어 있는 겁니다.
먼저, myApi.send 함수를 볼까요?
send: (channel, data) => {
// whitelist channels
let validChannels = ["latest-query"];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
channel이랑 data를 인자로 받고 만약 channel이 validChannels에 있으면 ipcRenderer.send() 함수를 사용하라는 뜻입니다.
이렇게 validChannels 방식을 쓰면 해킹에 안전할 수 있습니다.
validChannels 배열에는 앞으로 우리가 추가할 channel 이름을 추가하면 됩니다.
myApi.receive 함수도 마찬가지입니다.
그럼 이제 App.js 파일을 알아보겠습니다.
/src/App.js
import React, { useState, useEffect } from "react";
import {
Flex,
VStack,
Heading,
Text,
UnorderedList,
ListItem,
} from "@chakra-ui/react";
function App() {
const [data, setData] = useState();
useEffect(() => {
window.myApi.send(
"latest-query",
"select idMovie, c00, premiered, rating from movie_view order by premiered desc limit 2"
);
}, []);
window.myApi.receive("sql-return-latest", (data) => {
console.log(`Received data from main process`);
console.table(data);
setData(data);
window.myApi.removeListeners("sql-return-latest");
});
return (
<Flex w="auto" align="center" justify="center" p={[0, 10, 20]}>
<VStack>
<Heading>Welcome</Heading>
<Text>Electon + React + Sqlite3 + Chakra-UI</Text>
<UnorderedList>
{data &&
data.map((i) => <ListItem key={i.idMovie}>{i.c00}</ListItem>)}
</UnorderedList>
</VStack>
</Flex>
);
}
export default App;
useEffect 훅을 써서 App.js 가 실행되자마자 main process로 채널을 보냅니다.
이때 우리가 preload.js 에서 설정한 contextBridge를 사용하는데요.
window.myApi.send() 형식으로 사용하면 됩니다.
window.myApi.send(
"latest-query",
"select idMovie, c00, premiered, rating from movie_view order by premiered desc limit 2"
);
"latest-query" 채널로 SQL 쿼리를 보냈습니다.
그러면, electron.js 파일의 맨 밑에 있는 코드를 볼까요?
ipcMain.on("latest-query", (event, arg) => {
console.log("query from renderer : ", arg);
getData2(arg)
.then((res) => event.sender.send("sql-return-latest", res))
.catch((error) => console.log(error));
});
"latest-query" 채널로 뭔가가 오면 ipcMain.on() 함수가 작동하게 됩니다.
여기서 getData2 함수로 DB 데이터를 event.sener.send()로 보내게 되죠. 어디로 보낼까요? 바로 renderer process입니다.
renderer process로 보낼 때 채널을 "sql-return-latest"로 했습니다.
그럼 다시, preload.js 파일에서 이 부분을 처리하는 코드를 볼까요?
receive: (channel, func) => {
let validChannels = ["sql-return-latest"];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
myApi.receive 함수에 지정되어 있습니다.
이 함수에서 다시 ipcRenderer.on으로 인수를 넘겨주게 됩니다.
그러면 다시 App.js 파일로 가보면
window.myApi.receive("sql-return-latest", (data) => {
console.log(`Received data from main process`);
console.table(data);
setData(data);
window.myApi.removeListeners("sql-return-latest");
});
이와 같은 코드를 볼 수 있는데, 이 코드의 역할은 바로 main process에서 넘어온 data가 있으면 window.myApi.receive의 contextBridge로 인해서 ipcRenderer.on이 실행되며, 그 리턴 값이 data로 지정되고 또, 우리가 지정한 setData(data)로 인해서 ReactJS 앱의 State인 data에 저장됩니다.
그리고 UI 부분에서 이 data를 data.map 함수로 뿌려주게 되는 거죠.
그럼 마지막에 있는 window.myApi.removeListerners 는 뭔가요?
main Process에서 채널이 한번 왔으면 그 채널을 수신하는 걸 더 이상 하지 말라는 뜻입니다.
그래야 data 값이 한 번만 저장되게 되는 거죠.
그럼 preload.js에서 removeListeners 부분을 볼까요?
removeListeners: (channel) => {
let validChannels = ["sql-return-latest"];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.removeAllListeners(channel);
}
},
간단합니다.
ipcRenderer.removeAllListeners를 실행하는 겁니다.
스크린숏을 살펴보겠습니다.
터미널 창에서 마지막 부분이 query from renderer로 출력되는 console.log()가 보입니다.
그리고 Renderer Process 부분에서 console.table(data) 한 부분이 보입니다.
어떻습니까?
정말 복잡하죠?
보안 문제가 계속 이슈가 되면서 ElectronJS도 점점 더 IPC 커뮤니케이션이 어려워지고 있습니다.
그래도 완벽한 보안을 위해 우리가 좀 더 수고해야 하지 않을까요?
그럼. 다음 시간에는 우리의 앱을 완성해 보겠습니다.
'코딩 > React' 카테고리의 다른 글
카카오 로그인 구현 React(리액트) Nextjs NextAuth kakao login (1) | 2021.10.04 |
---|---|
ElectronJS 일렉트론 강좌 3편 SQLite3 sql.js (5) | 2021.09.23 |
ElectronJS 일렉트론 강좌 1편 SQLite3 sql.js (4) | 2021.09.18 |
NextJS MongoDB 5편 SSR, SSG, ISG 예제 (0) | 2021.09.15 |
NextJS와 MongoDB 4편 Chakra-UI 적용하기 (0) | 2021.09.15 |