안녕하세요?
지난 시간에 이어 3편 진행토록 하겠습니다.
1편: https://cpro95.tistory.com/500
2편 : https://cpro95.tistory.com/185
3편에서는 우리의 일렉트론 앱을 마무리해 보겠습니다.
일단 React에서 라우팅 하기 위한 react-router-dom을 설치하겠습니다.
npm i react-router-dom
1. 라우팅 설정하기
React 앱의 라우팅은 react-roter-dom을 이용하는데요.
일반적인 Browser라면 BrowserRouter를 이용하면 되지만 일렉트론 앱에서는 HashRouter를 이용해야 합니다.
index.js파일에서 전체 컴포넌트를 HashRouter로 감싸도록 합시다.
/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";
import { HashRouter } from "react-router-dom";
ReactDOM.render(
<React.StrictMode>
<HashRouter>
<ChakraProvider resetCSS>
<Layout>
<App />
</Layout>
</ChakraProvider>
</HashRouter>
</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();
그러고 나서 App.js 파일에 라우팅을 넣어보겠습니다.
/src/App.js
import React from "react";
import { Switch, Route } from "react-router-dom";
// Screens
import Home from "./screens/Home";
import Latest from "./screens/Latest";
import Search from "./screens/Search";
import Ratings from "./screens/Ratings";
import Details from "./screens/Details";
const App = () => (
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/search" component={Search} />
<Route exact path="/latest" component={Latest} />
<Route exact path="/ratings" component={Ratings} />
<Route exact path="/details/:id" component={Details} />
</Switch>
);
export default App;
react-router-dom을 이용해서 Switch와 Route를 이용해서 전체적인 앱의 라우팅 구조를 짰습니다.
Home 컴포넌트가 root가 되겠죠.
그리고, 각각의 컴포넌트를 Screen이라고 보고 screens 폴더 밑에 하나씩 만들어 봅시다.
먼저 Home 컴포넌트입니다.
/src/screens/Home.js
import React from "react";
import { Link } from "react-router-dom";
import { Flex, VStack, Heading, Stack, Button } from "@chakra-ui/react";
function Home() {
return (
<Flex w="auto" justify="center" p={[0, 10, 10]}>
<VStack>
<Heading>Welcome Home!</Heading>
<Heading>Electon + React + Sqlite3 + Chakra-UI</Heading>
<Flex h="25vh" align="center" justify="center">
<Stack direction="row" spacing={10}>
<Link to="/search">
<Button colorScheme="facebook">Search</Button>
</Link>
<Link to="/latest">
<Button colorScheme="whatsapp">Latest</Button>
</Link>
<Link to="/ratings">
<Button colorScheme="twitter">Ratings</Button>
</Link>
</Stack>
</Flex>
</VStack>
</Flex>
);
}
export default Home;
Home 컴포넌트는 원하시는 UI로 멋지게 만들어 보시기 바랍니다.
저는 Coding에 주력하다 보니까 각 Screen으로의 링크 버튼으로 Home 컴포넌트를 마무리했습니다.
그럼 먼저, Search 컴포넌트를 알아보겠습니다.
/src/screens/Search.js
import React, { useState } from "react";
import { Link } from "react-router-dom";
import {
Flex,
VStack,
Heading,
Text,
Input,
HStack,
Box,
Icon,
Badge,
} from "@chakra-ui/react";
import { CheckIcon } from "@chakra-ui/icons";
function Search() {
const [search, setSearch] = useState("");
const [data, setData] = useState();
const handleSearch = (e) => {
setSearch(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
window.myApi.send("search-query", search);
setSearch("");
};
window.myApi.receive("sql-return-search", (data) => {
// console.log(`Received data from main process`);
// console.table(data);
setData(data);
window.myApi.removeListeners("sql-return-search");
});
return (
<Flex w="auto" justify="center" p={[0, 10, 10]}>
<VStack spacing={4}>
<Heading>Search Movies</Heading>
<Text>Search Movies with query.</Text>
<form onSubmit={handleSubmit}>
<Input
w="300px"
type="text"
value={search}
onChange={handleSearch}
placeholder="Input query and press enter."
></Input>
</form>
{data ? <Text>Total - {data.length}</Text> : <></>}
{data &&
data.map((i) => (
<HStack key={i.idMovie} align={"top"} p={2}>
<Box color={"green.400"} px={2}>
<Icon as={CheckIcon} />
</Box>
<HStack align={"start"}>
<Link to={`/details/${i.idMovie}`}>
<Text fontWeight={600}>{i.c00}</Text>
</Link>
<Badge>{i.rating}</Badge>
</HStack>
</HStack>
))}
</VStack>
</Flex>
);
}
export default Search;
Search 컴포넌트는 input form을 이용해서 SQLite Data를 Query 하고 그 내용을 리스트로 나타내는 컴포넌트입니다.
star라고 쳤을 때 나타나는 Data입니다.
그럼 어떻게 sql query 처리했을까요?
form의 handleSubmit 함수에서 window.myApi.send("search-query", search)로 일렉트론의 Main Process에 명령어 실행을 요청했습니다.
2편에서 봤듯이 preload.js 파일에 window.myApi.send 명령어가 있는데요.
전체적인 preload.js 파일입니다.
contextBridge.exposeInMainWorld 기능을 알고 싶으시면 2편 마지막 부분을 보시면 됩니다.
/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 = [
"search-query",
"latest-query",
"ratings-query",
"id-query",
];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
let validChannels = [
"sql-return-search",
"sql-return-latest",
"sql-return-ratings",
"sql-return-id",
];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
removeListeners: (channel) => {
let validChannels = [
"sql-return-search",
"sql-return-latest",
"sql-return-ratings",
"sql-return-id",
];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.removeAllListeners(channel);
}
},
});
preload.js 파일을 보시면 Main Process로 SQL 명령어 실행을 요청하는 메시지가 각각,
"search-query",
"latest-query",
"ratings-query",
"id-query",
입니다.
이름을 보시면 각 컴포넌트에서 사용한다고 추측할 수 있는데요. 마지막 id-query 메시지는 Details 컴포넌트에서 사용할 예정입니다.
특정 id의 Data 한 개만 가져오는 SQL 명령어를 요청하는 거죠.
그럼 /public/electron.js 파일의 Main Process에서의 코드를 볼까요?
ipcMain.on("search-query", (event, arg) => {
// console.log('query from renderer : ', arg);
var stmt = `select idMovie, c00, c01, c03, c08, c16, c19, c20, premiered, strPath,rating, uniqueid_value from movie_view where c00 like '%${arg}%' order by idMovie desc`;
getData(stmt)
.then((res) => event.sender.send("sql-return-search", res))
.catch((error) => console.log(error));
});
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("ratings-query", (event, arg) => {
// console.log('query from renderer : ', arg);
getData(arg)
.then((res) => event.sender.send("sql-return-ratings", res))
.catch((error) => console.log(error));
});
ipcMain.on("id-query", (event, arg) => {
// console.log('query from renderer : ', arg);
var stmt = `select idMovie, c00, c01, c03, c08, c16, c19, c20, premiered, strPath,rating, uniqueid_value from movie_view where idMovie=${arg}`;
getData(stmt)
.then((res) => event.sender.send("sql-return-id", res))
.catch((error) => console.log(error));
});
각각의 query에 대해 ipcMain.on으로 대응했습니다.
그리고 Renderer Process로 리턴하는 메시지는 각각,
"sql-return-search",
"sql-return-latest",
"sql-return-ratings",
"sql-return-id",
입니다.
코드를 보시면 쉽게 이해할 수 있을 겁니다.
다음으로 Latest 컴포넌트를 알아보겠습니다.
/src/screens/Latest.js
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import {
Flex,
VStack,
Heading,
Text,
HStack,
Icon,
Badge,
Box,
} from "@chakra-ui/react";
import { CheckIcon } from "@chakra-ui/icons";
function Latest() {
const [data, setData] = useState();
useEffect(() => {
window.myApi.send(
"latest-query",
"select idMovie, c00, premiered, rating from movie_view order by premiered desc"
);
}, []);
window.myApi.receive("sql-return-latest", (data) => {
// data && console.log(`Received data from main process`);
// console.table(data);
setData(data);
window.myApi.removeListeners("sql-return-latest");
});
return (
<Flex w="auto" justify="center" p={[0, 10, 10]}>
<VStack>
<Heading>Latest Movies</Heading>
<Text>Total : {data && data.length}</Text>
{data &&
data.map((i) => (
<HStack key={i.idMovie} align={"top"} p={2}>
<Box color={"green.400"} px={2}>
<Icon as={CheckIcon} />
</Box>
<HStack align={"start"}>
<Link to={`/details/${i.idMovie}`}>
<Text fontWeight={600}>{i.c00}</Text>
</Link>
<Badge>{i.rating}</Badge>
</HStack>
</HStack>
))}
</VStack>
</Flex>
);
}
export default Latest;
Latest 컴포넌트는 SQL DB에서 가장 최근 걸로 요청한 코드입니다.
sql query는 바로 다음과 같습니다.
"select idMovie, c00, premiered, rating from movie_view order by premiered desc"
order by로 정렬시켰는데요, premiered 항목으로 정렬했고 desc로 최근께 앞으로 나오게 했습니다. 반대로 하시려면 asc라고 넣으시면 됩니다.
다른 코드도 Search.js 코드랑 비슷하니 쉽게 이해하실 수 있을 겁니다.
다음으로 Ratings 컴포넌트로 넘어가 보겠습니다.
/src/screens/Ratings.js
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import {
Flex,
VStack,
Heading,
Text,
HStack,
Icon,
Badge,
Box,
} from "@chakra-ui/react";
import { CheckIcon } from "@chakra-ui/icons";
function Ratings() {
const [data, setData] = useState();
useEffect(() => {
window.myApi.send(
"ratings-query",
"select idMovie, c00, c01, c08, c20, rating from movie_view order by rating desc limit 30"
);
}, []);
window.myApi.receive("sql-return-ratings", (data) => {
// console.log(`Received data from main process`);
// console.table(data);
setData(data);
window.myApi.removeListeners("sql-return-ratings");
});
return (
<Flex w="auto" justify="center" p={[0, 10, 10]}>
<VStack>
<Heading>Ratings</Heading>
<Text>Total : {data && data.length}</Text>
{data &&
data.map((i) => (
<HStack key={i.idMovie} align={"top"} p={2}>
<Box color={"green.400"} px={2}>
<Icon as={CheckIcon} />
</Box>
<HStack align={"start"}>
<Link to={`/details/${i.idMovie}`}>
<Text fontWeight={600}>{i.c00}</Text>
</Link>
<Badge>{i.rating}</Badge>
</HStack>
</HStack>
))}
</VStack>
</Flex>
);
}
export default Ratings;
역시 코드의 형태는 비슷합니다.
여기서는 SQL Query가 다음과 같습니다.
"select idMovie, c00, c01, c08, c20, rating from movie_view order by rating desc limit 30"
order by를 rating로 desc 방식으로 정렬했고 limit 30을 써서 30개만 나오게 했습니다.
limit 30을 빼면 전체 다 나옵니다.
그리고 마지막으로 Details 컴포넌트인데요.
Search, Latest, Ratings 컴포넌트에서 영화 타이틀을 누르면 넘어가는 페이지가 Details 페이지입니다.
라우팅은 아래처럼 /details/:id로 DB의 idMovie를 id로 넘겼습니다.
<Route exact path="/details/:id" component={Details} />
/src/screens/Details.js
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import {
Flex,
VStack,
Heading,
Text,
Image,
Tag,
Divider,
} from "@chakra-ui/react";
function Details() {
const { id } = useParams();
const [data, setData] = useState();
useEffect(() => {
window.myApi.send("id-query", id);
}, [id]);
window.myApi.receive("sql-return-id", (data) => {
// console.log(`Received data from main process`);
// console.log(data[0]);
setData(data[0]);
window.myApi.removeListeners("sql-return-id");
});
return (
<Flex w="auto" justify="center" p={[0, 10, 10]}>
<VStack spacing={4}>
<Flex direction={["column", "column", "row"]} spacing={4}>
<Image
objectFit={"contain"}
w={200}
src={data && data.c08}
mr={8}
alt="poster"
/>
<VStack spacing={4}>
<Heading>{data && data.c00}</Heading>
<Divider />
<Text m={2}>{data && data.c01}</Text>
<Divider />
<Tag colorScheme="facebook">Date: {data && data.premiered}</Tag>
<Tag colorScheme="whatsapp">Rating: {data && data.rating}</Tag>
</VStack>
</Flex>
<Image src={data && data.c20} objectFit={"cover"} alt="fanart" />
</VStack>
</Flex>
);
}
export default Details;
Details 컴포넌트는 라우팅의 파라미터로 id를 넘겨오는데요.
React에서 어떻게 이 id를 받아올까요?
바로 useParams 훅을 이용하시면 됩니다.
import { useParams } from "react-router-dom";
function Details() {
const { id } = useParams();
....
....
....
}
그러면 id를 이용해서 DB에서 정보를 가져와야겠죠.
근데 Details 컴포넌트가 로드될 때 바로 id가 넘어오는 게 아니고, useParams()를 실행했을 때 id 값이 넘어옵니다.
그 이전까지는 데이터가 없는 상태이죠.
그래서 useEffect 훅을 이용해서 id값이 변할 때마다 실행하는 아래 코드를 이용해서 DB에서 데이터를 가져오게 만들었습니다.
useEffect(() => {
window.myApi.send("id-query", id);
}, [id]);
이렇게 하면 id 값이 변할 때마다 window.myApi.send 명령어로 Main Process에게 SQL 명령어를 요청할 수 있게 됩니다.
UI 부분은 Flex를 이용해서 좀 멋지게 만들어 봤는데 영 아니네요.
프로그래머가 UI까지 하려고 하니 어렵네요.
이제 다 완성됐습니다.
그럼 패키지를 빌드해 볼까요?
패키지 빌드하기 전에 일렉트론 빌더를 설치해야 합니다.
제 글을 보셨으면 일렉트론 빌더는 벌써 설치하셨을 텐데요.
그럼 일렉트론 빌더의 옵션을 package.json 파일에 추가해보겠습니다.
{
"name": "electron-sqlite3-demo",
"description": "ElectronJS Sqlite3 Sql.js example",
"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",
"files": [
"dist",
"db/*",
"package.json"
],
"extraResources": [
"./extraResources/**"
]
},
"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",
"electron-store": "^8.0.1",
"framer-motion": "^4.1.17",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.3.0",
"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.js 파일입니다. 위쪽에 build 항목이 있습니다.
"build": {
"productName": "electron-test-app",
"asar": true,
"appId": "org.cpro95.electron-test-app",
"files": [
"dist",
"db/*",
"package.json"
],
"extraResources": [
"./extraResources/**"
]
},
이게 일렉트론 빌더가 옵션으로 사용하는 부분입니다.
여기서 중요한 부분이 있습니다.
React 앱은 build 폴더에 컴파일되는데요.
React 앱과 상관없는 파일은 직접 추가해 줘야 합니다.
그 부분이 files 부분인데요.
여기서 "db/*"를 살펴보시면 우리의 db 폴더를 포함시키겠다는 뜻입니다.
이렇게 하면 일렉트론의 resources 폴더 밑 app 폴더에 자동으로 복사하게 됩니다.
그리고 마지막에 extraResources 항목이 있는데요.
강좌 1편에 보시면 우리의 MyVideos116.db 파일을 extraResources 폴더에 넣었습니다.
폴더 이름은 다르게 해도 됩니다.
단지 package.json 에 extraResources 항목에 그 폴더를 위와 같이 추가하면 일렉트론 빌더가 그 폴더를 통째로 복사하게 됩니다.
자 그럼, 위 그럼처럼 "npm run build"를 실행해 보시면 dist 폴더에 우리의 앱이 완성되어 있는 걸 볼 수 있습니다.
실행해 볼까요?
dist 폴더 밑에 mac 폴더가 있고 거기에 앱이 있습니다.
Search 컴포넌트도 잘 작동됩니다.
이상으로 일렉트론 강좌를 마치도록 하겠습니다.
PS. 지난 강의 중 잘못된 부분 정정합니다.
/public/electron.js 파일에서 preload.js를 불러오는 항목이 있는데요.
Development 모드일 때는 아무 문제없는데 Production 모드일 때 에러가 생겼었습니다.
그래서 다음과 같이 바꿔 주시면 됩니다.
/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.resolve(__dirname, "preload.js"),
},
});
여기서 preload 부분을 path.resolve(__dirname, "preload.js")로 수정했습니다.
끝.
'코딩 > React' 카테고리의 다른 글
네이버 로그인 구현 React(리액트) Nextjs NextAuth naver login (0) | 2021.10.04 |
---|---|
카카오 로그인 구현 React(리액트) Nextjs NextAuth kakao login (1) | 2021.10.04 |
ElectronJS 일렉트론 강좌 2편 SQLite3 sql.js (0) | 2021.09.18 |
ElectronJS 일렉트론 강좌 1편 SQLite3 sql.js (4) | 2021.09.18 |
NextJS MongoDB 5편 SSR, SSG, ISG 예제 (0) | 2021.09.15 |