2024. 3. 5. 16:27ㆍElectron
git: https://github.com/kingkiboots/electron-pg-example
electron에서 postgresql 연결하기 두 번째 시간에 오신 여러분들을 환영한다.
저번 시간에는 node.js에서 postgresql에 crud를 수행하는 service를 만들고 이를 renderer에서 사용하기 위한 밑작업을 수행하였다. (링크: https://wheatbeingdeep-codinggiliee.tistory.com/15 )
저번 시간의 내용을 정리해 보자면,
- electron에서 postgresql와 연결하기 위해서는 pg라는 라이브러리를 설치한다. 그리고 이를 사용할 때에 pool을 사용하시는 걸 추천한다.
- electron이 제공하는 contextBridge.exposeInMainWorld과, ipcMain를 이용하여 restApi처럼 특정 url에 요청을 보내면 그에 맞는 함수를 실행하여 응답을 반환하는 구조를 선택했다.
- contextBridge.exposeInMainWorld는 proload.js 에서 사용되며 이를 통해 main.js와 소통할 채널을 정의한다. (정리 부분 바로 아래에 예시로 작성한 코드 참고)
- main.js에서 ipcMain.handle을 이용하여 DB와 연결할 채널에 실행할 함수를 정의 및 지정해주어야 한다.
- 그렇게 함수까지 등록된 채널을 renderer(화면)에서 사용하기 위해서는, contextBridge.exposeInMainWorld의 첫 번째 인자로 준 string 값과 두 번째 인자로 준 object의 property명을 조합하여 함수로 사용 가능하다.
(예시: window.DbConnection.request('test'), 아래의 예시 코드 참고)
contextBridge.exposeInMainWorld('DbConnection', {
request: (...args) => ipcRenderer.invoke('DB_REQUEST', ...args)
})
이제 저번 시간에 만든 contextBridge를 이용하여 electron 화면 컴포넌트에 postgresql에 데이터를 crud 하는 기능을 구현해 보도록 하자.
git에 있는 프로젝트에서 오늘 눈여겨봐야 할 디렉토리는 src/renderer/src/ 의 components와 hooks이다.
- components에는 view 관련 로직만 담고자 했다. 그리고 hooks에는 contextBridge를 호출하고, 그 데이터 처리를 하는 로직을 담은 custom hook을 담았다. 한 마디로 view 로직과 데이터 처리 로직을 분리하기 위함이다.
- 단, 디렉토리 구조의 통일성을 위하여 components의 dbTest라는 디렉토리에서 사용하는 custom hook의 경우 동일하게 dbTest라는 디렉토리에 해당 hook을 작성하였다.
먼저 hooks/dbTest/DbTestHook.js이다. 코드블럭 안에 주석을 작성하였으니 참고하면 더욱 좋겠다.
// DbTestHook.js
import { useEffect, useId, useRef, useState, useCallback } from 'react'
const DB_BASE_URL = 'test'
export const DbTestHook = () => {
const [data, setData] = useState([])
const [count, setCount] = useState(0)
const addInputRef = useRef(null)
const targetInputRef = useRef(null)
const updateInputRef = useRef(null)
const inputId = useId(null)
/*
* window.DbConnection.request : preload.js에 등록하였던 api
* contextBridge.exposeInMainWorld('DbConnection', {
* request: (...args) => ipcRenderer.invoke('DB_REQUEST', ...args)
* })
*/
const dbRequest = window.DbConnection.request
// find, findName등의 key는 모두 \src\common\db\dbClientUrls.js에 등록되어있다.
// 첫번째 인자로 준 key에 해당하는 함수를 실행하는 구조
const fetchAll = () => dbRequest([DB_BASE_URL, 'find'].join('/'))
const fetchByName = (param) => dbRequest([DB_BASE_URL, 'findName'].join('/'), param)
const insertTest = (param) => dbRequest([DB_BASE_URL, 'insert'].join('/'), param)
const updateTest = (param) => dbRequest([DB_BASE_URL, 'update'].join('/'), param)
const deleteTest = (param) => dbRequest([DB_BASE_URL, 'delete'].join('/'), param)
// 모든 데이터 불러오기
const fetch = useCallback(() => {
fetchAll().then((res) => {
setData(res)
})
}, [setData])
/*
* 커넥션 pool이 예상대로 작동하는지 확인하는 함수이다.
* 이 함수를 한번 실행할 때마다 fetchByName함수를 똑같이 20번 실행하며,
* 응답이 성공적으로 이루어졌을 때 setCount((prev) => prev + 1)를 실행한다.
* fetchByName 함수의 sql 문은 3초 뒤에 응답을 주도록 되어있으며
* 사용하고 있는 connnection pool의 최대 connection 수는 5개이다.
* 그리고 이 함수를 실행하는 components의 함수는 비동기로(setTimeout(()=>{}, 0)) 이 함수를 5번 실행한다.
* 결과적으로 원하는 결과는, fetchByName함수의 응답이 5개씩 3초 단위로 20번 반복되는 것이다.
*/
const testConnectionPool = useCallback(
async (who) => {
for (let i = 0; i < 20; i++) {
const name = `김길동${who}`
fetchByName({ name }).then((res) => {
console.log('==================================')
console.log('res', res)
console.log('who', who)
console.log('i', i)
setCount((prev) => prev + 1)
})
}
},
[setCount]
)
// 데이터 추가
const insertData = useCallback(
(input, name) => {
insertTest({ name }).then((res) => {
console.log('res', res)
if (!res) {
alert('no data added!')
return
}
fetch()
input.value = ''
})
},
[fetch]
)
// 데이터 수정
const updateData = useCallback(
(input, newName, targetName) => {
updateTest({ newName, targetName }).then((res) => {
console.log('res', res)
if (!res) {
alert('no data updated!')
return
}
fetch()
input.value = ''
})
},
[fetch]
)
// 데이터 삭제
const deleteData = useCallback(
(target, targetName) => {
deleteTest({ targetName }).then((res) => {
console.log('res', res)
if (!res) {
alert('no data deleted')
return
}
fetch()
target.value = ''
})
},
[fetch]
)
// 첫 렌더링 시 db의 데이터를 가져온다.(fetch 함수 실행)
useEffect(() => {
fetch()
}, [])
return {
data,
count,
addInputRef,
targetInputRef,
updateInputRef,
inputId,
testConnectionPool,
insertData,
updateData,
deleteData
}
}
다음으로 view 부분인 components/dbTest/DbTest.jsx이다.
import React from 'react'
import { isNullOrBlank } from '../../../../shared/util/CommonUtil'
import { DbTestHook } from '../../hooks/dbTest/DbTestHook'
// 화면 컴포넌트에는 VIEW 관련 코드 존재
// 데이터 처리 코드는 HOOK에
const DbTest = () => {
// hooks/dbTest/DbTestHook에 만든 커스텀 훅 임포트
const {
data,
count,
addInputRef,
targetInputRef,
updateInputRef,
inputId,
testConnectionPool,
insertData,
updateData,
deleteData
} = DbTestHook()
// 커넥션 풀을 테스트 하는 함수.
// 비동기로 testConnectionPool 함수를 5번 실행한다.
// 결과적으로 원하는 결과는, fetchByName함수의 응답이 5개씩 3초 단위로 20번 반복되는 것이다.
const handleClickCtnPoolTest = () => {
setTimeout(() => {
testConnectionPool('')
}, 0)
setTimeout(() => {
testConnectionPool(2)
}, 0)
setTimeout(() => {
testConnectionPool(3)
}, 0)
setTimeout(() => {
testConnectionPool(4)
}, 0)
setTimeout(() => {
testConnectionPool(5)
}, 0)
}
// 유효성 검증
const handleValidation = (refCurrent) => {
if (!refCurrent) {
return false
}
const { value } = refCurrent
if (isNullOrBlank(value)) {
alert('No 빈 칸 ^^>')
refCurrent.focus()
return false
}
return true
}
// 데이터 추가 버튼 클릭 시
const handleClickAdd = () => {
const input = addInputRef.current
if (!handleValidation(input)) return
const { value: name } = input
insertData(input, name)
}
// 데이터 수정 버튼 클릭 시
const handleClickUpdate = () => {
const input = updateInputRef.current
const target = targetInputRef.current
if (!(handleValidation(input) && handleValidation(target))) return
const { value: newName } = input
const { value: targetName } = target
updateData(input, newName, targetName)
}
// 데이터 삭제 버튼 클릭 시
const handleClickDelete = () => {
const target = targetInputRef.current
if (!handleValidation(target)) return
const { value: targetName } = target
deleteData(target, targetName)
}
return (
<div>
<h1>Welcome to DB Test Page!</h1>
<hr />
<div>
initial fetch :{' '}
{data.map((ele, idx) => (
<React.Fragment key={`${idx}`}>{`${ele.name}, `}</React.Fragment>
))}{' '}
</div>
<hr />
<div>
<h3>connection pool test</h3>
<p>
this button below will let 5 executers to send 20 requests per each,
<strong> Asynchronously</strong>
</p>
<button type="button" onClick={handleClickCtnPoolTest}>
EXECUTE!
</button>
<br />
<p>
watch how count goes up! As we have 5 max connections in connection pool and the queries
that executed will response after 3 seconds.
</p>
<p>to check out the fetched value, see browser console.</p>
{count}
</div>
<hr />
<div>
<h3>CUD Test</h3>
<div>
<h4>Insert</h4>
<label htmlFor={`${inputId}-insert`}>data to add : </label>
<input type="text" ref={addInputRef} id={`${inputId}-insert`} />
<button type="button" onClick={handleClickAdd}>
ADD!
</button>
</div>
<br />
<div>
<h4>Update & Delete</h4>
<label htmlFor={`${inputId}-target`}>target : </label>
<input type="text" ref={targetInputRef} id={`${inputId}-target`} />
<br />
<label htmlFor={`${inputId}-update`}>new data: </label>
<input type="text" ref={updateInputRef} id={`${inputId}-update`} />
<br />
<button type="button" onClick={handleClickUpdate}>
UPDATE!
</button>
<br />
<button type="button" onClick={handleClickDelete}>
DELETE TARGET!
</button>
</div>
</div>
</div>
)
}
export default DbTest
이제 모든 준비가 완료되었다!
renderer/main.jsx에 위에서 만든 DbTest 컴포넌트 임포트해주고 일렉트론을 실행해 주자!
// main.jsx
import './assets/main.css'
import React from 'react'
import ReactDOM from 'react-dom/client'
import DbTest from './components/dbTest/DbTest'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<DbTest />
</React.StrictMode>
)
yarn dev or npm run dev
그러면, app이 아래와 같은 화면으로 켜진다. (css는 기본 제공되는 스타일시트에서 내가 변경한 게 있을 수도 있으니 참고 바란다.)
DbTest가 첫 렌더링 될 때에 fetchAll 함수를 실행하도록 의도했었는데 예상대로 실행되어서 initial fetch 부분에 모든 데이터가 나열되어 있음을 확인할 수 있다.
두 번째로 테스트해봐야 할 것은 (뜬금없긴 하지만) connection pool이, 예상한 대로, 최대 개수로 지정한 connection만큼의 connection이 모두 작업 중이면 그 이후의 작업 요청은 대기시키는지 확인해야 한다. 정말 잘 작동되나 궁금하긴 하고 이렇게가 아니면 가시적으로 테스트할 방법이 없다고 여겨졌나,,,
예상했던 대로 5개씩 3초 간격으로 끊겨서 응답이 온다.
다음은 데이터를 추가, 수정, 삭제하는 영상이다.
다 잘 작동하는 모습을 보니 뿌듯하다.
이상으로 renderer, 여기에선 react component에서 어떻게 postgresql 데이터에 접근할 수 있는지를 알아보았다.
정리를 해보자면
- contextBridge.exposeInMainWorld(apiKey, api)에 등록되어있는 함수를 window.apiKey.[key of api]를 통해 사용 가능하다.
- 화면 단에서 window.apiKey.[key of api]로 호출할 함수를 main.js에서 ipcMain.handle(apiKey, (event,... args)=>{...}) 형태로 지정해주어야 한다.
'Electron' 카테고리의 다른 글
[Electron + React JS] electron에서 postgresql 연결하기 (1) (2) | 2024.02.27 |
---|