IndexedDB를 활용하기 위한 React Provider
IndexedDB를 사용하기 위한 Provider입니다.IndexedDBProvider.tsx
import {
createContext,
PropsWithChildren,
useContext,
useEffect,
useRef,
} from 'react'
type StoreName = 'storeName'
interface IndexedDBContext {
getData: <T>({
storeName,
key,
}: {
storeName: StoreName
key: IDBValidKey
}) => Promise<T | undefined>
setData: (
storeName: StoreName,
{
key,
value,
}: {
key: IDBValidKey
value: any
},
) => Promise<unknown>
deleteData: ({
storeName,
key,
}: {
storeName: StoreName
key: IDBValidKey
}) => Promise<boolean | undefined>
}
const Context = createContext<IndexedDBContext>({
getData: () => new Promise(() => {}),
setData: () => new Promise(() => {}),
deleteData: async () => new Promise(() => {}),
})
const INDEXED_DB_NAME = 'DBname'
const INDEXED_DB_STORES: {
name: string
options?: IDBObjectStoreParameters
}[] = [{ name: 'storeName' }]
export const IndexedDBProvider = ({ children }: PropsWithChildren<unknown>) => {
const connection = useRef<{ [version: string]: IDBOpenDBRequest }>({})
//* DB 생성 및 스토어 업데이트
const initIndexedDB = async () => {
const db = await _openIndexedDB()
const notAddedStores = INDEXED_DB_STORES.filter(
({ name }) => !db.objectStoreNames.contains(name),
)
if (notAddedStores.length) {
db.close()
//* 새로운 버전으로 open 시 DB가 업데이트가 된다.
const updatedDB = await _openIndexedDB({
version: db.version + 1,
//* 스토어 추가는 DB 업데이트 시에만 가능하다.
onUpdate: (_db) => {
notAddedStores.forEach(({ name, options }) => {
_db.createObjectStore(name, options)
})
},
})
updatedDB.close()
}
}
useEffect(() => {
initIndexedDB()
}, [])
const _openIndexedDB = async (options?: {
version?: number
onUpdate?: (db: IDBDatabase) => void
}): Promise<IDBDatabase> => {
const version = options?.version
if (connection.current[String(version)]) {
//* 병렬적으로 indexedDB open 실행시 open 이 여러차레 호출시 open이 멈출 수 있다.
//* 최초 open 이후 open은 최초 open의 response를 기다리도록한다.
if (connection.current[String(version)].readyState === 'pending') {
return await new Promise((resolve) => {
const handleSuccess = (event: Event) => {
const { result } = event.target as IDBRequest<IDBDatabase>
removeListener()
resolve(result)
}
const handleError = (event: Event) => {
const { result } = event.target as IDBRequest<IDBDatabase>
removeListener()
resolve(result)
}
const removeListener = () => {
connection.current[String(version)]?.removeEventListener(
'success',
handleSuccess,
)
connection.current[String(version)]?.removeEventListener(
'error',
handleError,
)
}
connection.current[String(version)].addEventListener(
'success',
handleSuccess,
)
connection.current[String(version)].addEventListener(
'error',
handleError,
)
})
}
if (
connection.current[String(version)].readyState === 'done' &&
connection.current[String(version)].result
) {
return connection.current[String(version)].result
}
}
//* version 이 undefined 라면 최신 db를 가져온다
const request = window.indexedDB.open(INDEXED_DB_NAME, version)
connection.current[String(version)] = request
const result = await new Promise<IDBDatabase>((resolve, reject) => {
request.onsuccess = (event) => {
const { result } = event.target as IDBRequest<IDBDatabase>
connection.current[result.version] = request
if (connection.current.undefined) {
delete connection.current.undefined
}
resolve(result)
}
request.onupgradeneeded = (event) => {
const { result } = event.target as IDBRequest<IDBDatabase>
options?.onUpdate?.(result)
resolve(result)
}
request.onerror = (event) => {
const { error } = event.target as IDBRequest<IDBDatabase>
reject(error)
}
})
return result
}
const _getStore = async (
storeName: StoreName,
db: IDBDatabase,
mode: IDBTransactionMode,
) => {
const transaction = db.transaction(storeName, mode)
const objectStore = transaction.objectStore(storeName)
return objectStore
}
const _editItemInStore = async (
store: IDBObjectStore,
value: any,
key: IDBValidKey,
) => {
const request = store.put(value, key)
return await new Promise<boolean>((resolve, reject) => {
request.onsuccess = () => {
resolve(true)
}
request.onerror = (event) => {
const { error } = event.target as IDBRequest<IDBObjectStore>
reject(error)
}
})
}
const _addItemToStore = async (
store: IDBObjectStore,
value: any,
key: IDBValidKey,
) => {
const request = store.add(value, key)
return await new Promise((resolve, reject) => {
request.onsuccess = () => {
resolve(true)
}
request.onerror = (event) => {
const { error } = event.target as IDBRequest<IDBObjectStore>
reject(error)
}
})
}
const _getItem = async <T,>(store: IDBObjectStore, key: IDBValidKey) => {
const request = store.get(key)
return await new Promise<T>((resolve, reject) => {
request.onsuccess = (event) => {
const { result } = event.target as IDBRequest<T>
resolve(result)
}
request.onerror = (event) => {
const { error } = event.target as IDBRequest<T>
reject(error)
}
})
}
const _deleteItem = async (store: IDBObjectStore, key: IDBValidKey) => {
const request = store.delete(key)
return await new Promise<boolean>((resolve, reject) => {
request.onsuccess = () => {
resolve(true)
}
request.onerror = (event) => {
const { error } = event.target as IDBRequest
reject(error)
}
})
}
const getData = async <T,>({
storeName,
key,
}: {
storeName: StoreName
key: IDBValidKey
}) => {
const db = await _openIndexedDB()
const store = await _getStore(storeName, db, 'readonly')
const item = await _getItem<T>(store, key)
db.close()
return item
}
const setData = async (
storeName: StoreName,
{
key,
value,
}: {
key: IDBValidKey
value: any
},
) => {
const db = await _openIndexedDB()
const storeExists = db.objectStoreNames.contains(storeName)
if (!storeExists) {
throw Error(`IndexedDB: failed To load store - ${storeName}`)
}
const store = await _getStore(storeName, db, 'readwrite')
const itemExists = await _getItem(store, key)
if (itemExists) {
_editItemInStore(store, value, key)
db.close()
return
}
_addItemToStore(store, value, key)
db.close()
}
const deleteData = async ({
storeName,
key,
}: {
storeName: StoreName
key: IDBValidKey
}) => {
const db = await _openIndexedDB()
const store = await _getStore(storeName, db, 'readwrite')
const result = await _deleteItem(store, key)
db.close()
return !!result
}
const value = {
getData,
setData,
deleteData,
}
return <Context.Provider value={value}>{children}</Context.Provider>
}
export const useIndexedDBContext = () => {
return useContext(Context)
}