Storage말고 IndexedDB는 어때요?
2025-05-21새로운 회사에서 레거시 코드와 버그를 해결하던 중 다음과 같은 상황을 맞닥뜨렸습니다
- 외부결제 모듈 사용 때문에 외부 페이지를 접속 했다가 결제가 끝나면 다시 앱으로 복귀
- 이미지로 구성된 기프트카드는 결제가 완료된 후, 결제완료정보에 묶여 카드 정보를 같이 서버로 전달
- 해당 기프트카드 정보는 그동안 Recoil로 관리되고 있어서 카드 정보가 소멸된 채 결제가 완료
처음 해당 버그를 발견했을 땐 "뭐야? Recoil대신 스토리지로 관리하면 되는거 아니야?"란 생각을 가지고 수정을 진행했습니다.
하지만... 로직을 구성하던 중 발견한 문제가 하나 더 있었으니 해당 기프트카드의 정보 중, 이미지는 Base64형태였던 거였죠
Base64가 뭔데요
Base64는 바이너리데이터를 텍스트로 인코딩하는 방식 중 하나입니다.
64개의 ASCII 문자만을 사용해 데이터를 표현하는 인코딩 방식으로
A-Z (26개), a-z (26개), 0-9 (10개), +, /
총 64개의 문자로 구성되어 있어 이름이 Base64라고 하네요.
저희 회사앱은 Next.js와 Flutter로 구성된 웹뷰형태이며, 해당 카드의 이미지는 Flutter에서 Base64로 가공하여 웹으로 전달하는 방식이였죠
Base64는 텍스트로 인코딩되기 때문에 그 길이가 무지막지하게 깁니다. 따라서 스토리지에 그대로 저장할 수 없는 문제가 있었죠.
그래서 고민하다 예전에 이론만 봤었던 IndexedDB 활용해서 해결해보자 생각했습니다.
IndexedDB ?
IndexedDB는 브라우저에서 사용 가능한 로컬 DB API입니다. NoSQL계열로 key-value형식으로 데이터를 저장할 수 있습니다.
수 MB~수 GB의 데이터를 저장 가능하고 비동기로 작동하는 특징이 있죠. 또한 트랜잭션 기반으로 작동하여 안전하고 대부분 브라우저에서 지원하고 있습니다.
CRUD
IndexedDB를 사용하려면 우선 DB부터 만들어야 합니다
const request = indexedDB.open("MyDB", 1);
위 코드처럼 첫번째 매개변수로 DB명, 두번째로는 DB의 버전을 입력합니다.
IndexedDB에서는 버전이 가장 중요한데요
해당 버전은 object store(테이블이라 생각하면 됩니다)를 만들거나 수정하면 반드시 업그레이드해야 합니다.
DB를 만들었으니 이제 object store(*테이블로 지칭하겠습니다)
를 만들어봐야겠죠?
request.onupgradeneeded = function (event) {
const db = request.result;
if (!db.objectStoreNames.contains("images")) {
db.createObjectStore("images", { keyPath: "id" });
}
};
request.onerror = () => {
console.error("IndexedDB 열기 실패");
};
request.onsuccess = () => {
console.log("IndexedDB 연결 성공");
};
db.objectStoreNames.contains('images')
로 해당 테이블이 있는지 확인하고 없다면 createObjectStore
를 사용해서 만듭니다.
여기서 keyPath 고유키라 생각하시면 됩니다.
Create
function addImage(id: string, base64: string) {
const request = indexedDB.open("MyDB");
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction("images", "readwrite");
const store = tx.objectStore("images");
store.add({ id, base64 });
tx.oncomplete = () => {
console.log("이미지 저장 완료");
};
};
}
우선 DB에 접속 후, db.transaction('images', 'readwrite');
로 images테이블에 대해 읽기/쓰기 권한을 가진 트랜잭션을 생성합니다.
이후 트랜잭션에서 images테이블을 가져와서 데이터를 Create하고 트랜잭션완료를 확인하면 Create가 끝납니다
Read
function getImage(id: string) {
const request = indexedDB.open("MyDB");
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction("images", "readonly");
const store = tx.objectStore("images");
const getRequest = store.get(id);
getRequest.onsuccess = () => {
const result = getRequest.result;
if (result) {
console.log("이미지 데이터:", result.base64);
} else {
console.log("해당 ID의 데이터 없음");
}
};
};
}
Read도 동일하게 DB연결 -> 트랜잭션 생성 -> 테이블 접근 순으로 동작하죠 여기서 DB를 생성할때 지정했던 keyPath로 데이터를 접근할 수 있습니다.
Update
function updateImage(id: string, newBase64: string) {
const request = indexedDB.open("MyDB");
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction("images", "readwrite");
const store = tx.objectStore("images");
store.put({ id, base64: newBase64 });
tx.oncomplete = () => {
console.log("이미지 업데이트 완료");
};
};
}
Update에서 중요한건 기존 정보를 덮어씌우기 때문에 데이터 관리에 주의를 기울여야합니다
Delete
function deleteImage(id: string) {
const request = indexedDB.open("MyDB");
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction("images", "readwrite");
const store = tx.objectStore("images");
store.delete(id);
tx.oncomplete = () => {
console.log("이미지 삭제 완료");
};
};
}
위 처럼 IndexedDB는 실제 DB처럼 트랜잭션도 존재하며 비동기로 동작하여 실무에서 유용하게 쓰일 경우가 많이 있습니다.
여러분도 기회가 된다면 IndexedDB를 한번 사용해보는건 어떨까요?