2024. 2. 27. 19:02ㆍElectron
git: https://github.com/kingkiboots/electron-pg-example
electron은 대게 node 환경에서 돌아간다. 그러므로 어떤 라이브러리를 사용한다면 npm install 혹은 yarn add 등을 사용하면 된다.
자, 그러면 postgresql에 연결하기 위해서는 pg라는 라이브러리를 설치해야 한다.
해당 라이브러리의 공식문서는 여기이니 참고해 주시라 => Welcome – node-postgres
yarn add pg
그리고 DB에 연결하는 하나의 인스턴스를 생성하자.
아 참, 필자는 cra가 아닌, vite로 프로젝트를 생성했으므로, 모듈을 export 및 import 하고 require 하는 부분과 환경변수를 가져오는 부분에서 차이가 있을 수 있으니 참고 바란다. (그리고 cra가 deprecated 되었고 vite가 여러모로 낫다고 하니 여러분들도 vite로 갈아타심이 어떠신지 쿨럭,,,,)
나는 커넥션 풀을 사용하려 한다. 공문을 보시면 알겠지만, 이거를 사용하면 db connection을 새로 만들고 닫는 부하를 낮출 수 있으며 connection의 연결과 닫힘을 자동으로 관리해 준다. 다만 트랜잭션이 필요하다면 이 pool을 바로 사용하면 안 되고 connection을 pool로부터 하나 받아서 처리를 하고 connection pool에 반환하는 처리를 해줘야 한다고 한다. (참고 : https://node-postgres.com/features/transactions)
아래의 코드는 Suggested Project Structure – node-postgres 이 부분에 나와있는 코드를 사용한 것이다.
// https://node-postgres.com/apis/pool
const { Pool } = require('pg')
const DbClientPool = new Pool({
host: `${import.meta.env.VITE_DB_HOST}`,
database: `${import.meta.env.VITE_DB_DATABASE}`,
user: `${import.meta.env.VITE_DB_USERNAME}`,
password: `${import.meta.env.VITE_DB_PASSWORD}`,
port: import.meta.env.VITE_DB_PORT,
max: 5
})
// 트랙잭션 필요 시 추가
// 트랜잭션 docs 링크 : https://node-postgres.com/features/transactions
// getDbClient
/*
export const getClient = async () => {
const client = await pool.connect()
const query = client.query
const release = client.release
// set a timeout of 5 seconds, after which we will log this client's last query
const timeout = setTimeout(() => {
console.error('A client has been checked out for more than 5 seconds!')
console.error(`The last executed query on this client was: ${client.lastQuery}`)
}, 5000)
// monkey patch the query method to keep track of the last query executed
client.query = (...args) => {
client.lastQuery = args
return query.apply(client, args)
}
client.release = () => {
// clear our timeout
clearTimeout(timeout)
// set the methods back to their old un-monkey-patched version
client.query = query
client.release = release
return release.apply(client)
}
return client
}
*/
const dbClientQuery = async (text, params) => {
const start = Date.now()
const res = await DbClientPool.query(text, params)
const duration = Date.now() - start
console.log('executed query', { text, duration, rows: res.rowCount })
return res
}
const endDbClientPool = () => {
DbClientPool.end()
}
export { dbClientQuery, endDbClientPool }
이제 커넥션풀을 만들었으니 이거를 사용하는 함수를 만들어야 하지 않겠는가?
만든 커넥션 풀을 불러와 쿼리문을 작성하여 실행하면 해당 쿼리를 db에 날리고 auto commit까지 알아서 된다.
import { dbClientQuery } from '../../../common/db/DbClient'
// @ "findAll"
const findAllTest = async () => {
try {
const res = await dbClientQuery('SELECT * FROM test')
return res.rows
} catch (err) {
console.error('dbClientQuery err ===> ', err)
}
}
문제는 이것이다. 이거를 client에서 사용할 텐데 혹시나 사용자가 이 쿼리문을 보게 된다면 보안적으로 좋지 않을까? 화면은 f12로 그 소스를 볼 수 있기 때문이다. 적어도 웹에서는 그런데 일렉트론에서도 혹시 모르니깐 말이다.
그런 이유로 나는 이것을 화면 즉, 일렉트론의 renderer 쪽이 아닌 node 즉 main.js 쪽에 숨겨두고 싶었다.
그렇다고 express.js를 사용하자니 만약 express.js의 규모가 커지면 express.js 서버가 다 켜지기도 전에 일렉트론이 express 화면을 잡아올 수도 있다는 이야기 때문에 (https://gist.github.com/maximilian-lindsey/a446a7ee87838a62099d) express.js를 사용하기도 망설여졌다.
그래서 나는 contextBridge.exposeInMainWorld과, ipcMain를 이용하여 restApi로 특정 url에 요청을 보내면 controller를 거치고, service 등을 거쳐 로직을 처리한 후 그 응답 값을 반환하는, 즉 우리에게 익숙한 mvc 패턴을 생각하였다.
아래의 코드는 preload/index.js이다.
contextBridge.exposeInMainWorld('DbConnection', dbConnectionApi)를 통해 renderer 화면에서는 window.DbConnection을 통해 dbConnectionApi에 등록되어 있는 함수들을 사용할 수 있다.
그리고 ipcRender.invoke는 그 자체로 Promise를 반환할 수 있다. ipcRender.invoke의 첫 번째 인자인 'DB_REQUEST'는 일종의 채널이라고 보면 된다. main.js 에서 ipcMain.handle('DB_REQUEST', handleDbRequest)을 실행함으로써 renderer에 있는 화면에서 window.DbConnection.request를 호출하였을 때 어떤 함수를 실행할지를 지정할 수 있다.
import { electronAPI } from '@electron-toolkit/preload'
const dbConnectionApi = {
request: (...args) => ipcRenderer.invoke('DB_REQUEST', ...args)
}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('DbConnection', dbConnectionApi)
} catch (error) {
console.error(error)
}
} else {
window.api = dbConnectionApi
}
좋다. 그러면 이제 main.js 에서 저 'DB_REQUEST' 채널에 적용할 함수를 지정하러 가보자.
라고는 말했지만, db connection 하는 함수를 등록하는 기능만 있는 파일을 만든 후 이것을 main/index.js에서 실행하여 'DB_REQUEST' 채널에 db connection 하는 함수를 등록시킬 것이다. 코드는 아래와 같다.
getExecution이라는 함수를 이용하여 dbClientUrls에 등록되어있는 함수를 얻어와 실행한다.
// setDbConnection.js
import { ipcMain } from 'electron'
import { dbClientUrls } from '../db/dbClientUrls'
const connectDatabase = () => {
ipcMain.handle('DB_REQUEST', handleDbRequest)
}
const handleDbRequest = async (event, ...args) => {
const [url, param] = args
console.log('DB_REQUEST ACCEPTED', { url, param })
const mappedController = getExecution(url)
return await mappedController(param)
}
export { connectDatabase }
const getExecution = (url) => {
const urlArr = url.split('/')
let currentObj = dbClientUrls // 값 복사
for (const segment of urlArr) {
if (currentObj[segment] !== undefined) {
currentObj = currentObj[segment]
} else {
console.error(`404 ERROR!! Mapped URL '${segment}' not found.`)
return null // 또는 적절한 기본값/에러 처리
}
}
if (currentObj.execute !== undefined) {
return currentObj.execute
} else {
console.error('Execute property not found.')
return null
}
}
그리고 아래는 위의 setDbConnection.js에서 사용하고 있는 dbClientUrls.js이다.
Django 프레임워크를 보면 view(스프링 mvc에서는 controller의 역할)를 만들고 urls라는 파일에다가 '어떤 url은 이 view의 이 함수를 실행합니다 (가물가물하다)'라고 명시해 준다.
이것을 착안하여 TestService라는 곳에 만든 여러 함수들을 각 url 마다 등록해 주었다.
예를 들어 renderer에서 window.DbConnection.request('test/find') 이렇게 함수를 실행하면 findAllTest 함수를 실행 후 결과 값을 Promise로 반환한다.
// dbClientUrls.js
import {
findAllTest,
findNameTest,
saveName,
updateNameByName,
deleteByName
} from '../../service/db/test/TestService'
const dbClientUrls = {
test: {
find: {
execute: findAllTest
},
findName: {
execute: findNameTest
},
insert: {
execute: saveName
},
update: {
execute: updateNameByName
},
delete: {
execute: deleteByName
}
}
}
export { dbClientUrls }
그리고 아래의 코드는 TestService다.
/* @test */
import { dbClientQuery } from '../../../common/db/DbClient'
// @ "findAll"
const findAllTest = async () => {
try {
const res = await dbClientQuery('SELECT * FROM test')
return res.rows
} catch (err) {
console.error('dbClientQuery err ===> ', err)
}
}
// @ "findName"
const findNameTest = async (param) => {
try {
console.log('param :::: ', param)
const res = await dbClientQuery(
`SELECT pg_sleep(3), name FROM test WHERE name = '${param.name}'`
)
return res.rows
} catch (err) {
console.error('dbClientQuery err ===> ', err)
}
}
// @ "insert"
const saveName = async (param) => {
try {
console.log('saveName param :::: ', param)
const res = await dbClientQuery(`INSERT INTO test VALUES ('${param.name}')`)
return res.rowCount
} catch (err) {
console.error('dbClientQuery err ===> ', err)
return 0
}
}
// @ "update"
// TODO: 중복 검사를 해야할까? 중복되는 게 있으면 에러는 안남. 그리고 똑같은 걸로 update함
const updateNameByName = async (param) => {
try {
console.log('updateNameByName param :::: ', param)
const res = await dbClientQuery(
`UPDATE test SET name = '${param.newName}' WHERE name = '${param.targetName}'`
)
return res.rowCount
} catch (err) {
console.error('dbClientQuery err ===> ', err)
return 0
}
}
// @ "delete"
const deleteByName = async (param) => {
try {
console.log('deleteByName param :::: ', param)
const res = await dbClientQuery(`delete from test where name='${param.targetName}'`)
console.log('res', res)
return res.rowCount
} catch (err) {
console.error('dbClientQuery err ===> ', err)
return 0
}
}
export { findAllTest, findNameTest, saveName, updateNameByName, deleteByName }
자, 그리고 마지막으로 main/index.js이다. 여기가 어떻게 보면 electron의 핵심이겠는데, 이곳에서 renderer로 어떤 화면을 선택할지, 써드 파티 디바이스를 연결한다면 이것들과의 연결 설정 그리고 db 연결 등의 설정을 한다.
git에 올려놓긴 했으니 db connection 하는 부분을 제외하고는 대충 중략하도록 하겠다.
mainwindow가 열릴 때에 connectDatabase를 실행하여 connectionPool을 생성하고, mainwindow가 닫힐 때에 endDbClientPool()를 실행하여 connectionPool을 닫아주도록 했다는 것을 기억하자.
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import { createFileRoute, createURLRoute } from 'electron-router-dom'
import icon from '../../resources/icon.png?asset'
import { connectDatabase } from '../common/config/setDbConnection'
import { endDbClientPool } from '../common/db/DbClient'
function createWindow() {
const BASE_HREF = 'main'
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
nodeIntegration: true,
contextIsolation: true, // allow to use with Electron 12+
preload: join(__dirname, '../preload/index.js')
}
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
const devServerURL = createURLRoute(process.env['ELECTRON_RENDERER_URL'], BASE_HREF)
mainWindow.loadURL(devServerURL)
} else {
const buildFilePath = join(__dirname, '../renderer/index.html')
const fileRoute = createFileRoute(buildFilePath, BASE_HREF)
mainWindow.loadFile(...fileRoute)
}
connectDatabase()
mainWindow.on('closed', () => {
// ...
// 화면이 꺼질 때 커넥션 풀 닫기!!!!!!!
endDbClientPool()
mainWindow.destroy()
})
}
app.whenReady().then(() => {
// ...
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
얼추 back 단(?)은 완료하였고 다음 포스트에서 renderer에서 db에 crud 하는 기능을 구현해 보도록 하겠다.
정리
- pg라는 라이브러리를 사용할 때에 pool을 사용하시는 걸 추천한다. 이게 더 관리하기 쉽다.
- contextBridge.exposeInMainWorld과, ipcMain를 이용하여 restApi처럼 특정 url에 요청을 보내면 그에 맞는 함수를 실행하여 응답을 반환하는 구조를 선택
- 이는 renderer에서 window.채널명.등록한_함수_이름(인자값) 형태로 호출이 가능하다.
- service에 db 연결하는 함수들을 만들고 이를 dbConnectionUrls.js에 import 한다.
- setDbConnection.js에서 ipcMain.handle('DB_REQUEST', handleDbRequest)을 실행함으로써 renderer에 있는 화면에서 window.DbConnection.request를 호출하였을 때 해당 url에 맞는 함수를 실행 및 결과를 반환하는 함수를 만든다.
- setDbConnection.js 에 만든 함수를 main/index.js 에서 호출한다.
'Electron' 카테고리의 다른 글
[Electron + React JS] electron에서 postgresql 연결하기 (2) (0) | 2024.03.05 |
---|