[Electron + React JS] electron에서 postgresql 연결하기 (2)

2024. 3. 5. 16:27Electron

 

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