앞서 copy이벤트와 context menu를 통해 받아온 데이터를 indexed DB에 저장해 보는 작업을 할 겁니다.
indexed DB를 사용하기 위해선 먼저 DB를 열고 object store(일종의 테이블)를 생성해야 합니다.
DB는 indexedDB.open 을 통해 열고, 최초 테이블 생성은 createObjectStore를 통해 수행합니다. 최초 테이블이 없을 땐 onupgradeneeded이벤트를 통해 테이블을 생성할 수 있습니다.
const req = indexedDB.open("ITEM_LIST"); // indexedDB 열기 요청
let db = null;
req.onupgradeneeded = (evt) => { // 최초 혹은 버전이 올라갈 때 실행되는 이벤트
db = evt.target.result;
db.createObjectStore("items",{ // store 생성
keyPath: "id",
autoIncrement:true
});
};
req.onsuccess = () => { // 불러온 db 반환
db = req.result;
};
onupgradeneeded가 먼저 실행되고 onsuccess가 실행됩니다.
createObjectStore의 옵션은 keyPath와 autoIncrement 두 가지입니다. keyPath는 어떤 key를 unique key로 사용할지에 대한 정보입니다. autoIncrement가 true로 되어있다면 엔티티를 추가할 때 keyPath를 1씩 자동으로 증가시킵니다.
위 코드를 promise로 감싸서 async await으로 쓸 수 있게 합니다.
const getDB = (() => {
let db = null;
return async () => {
if (!db) {
db = await new Promise((resolve, reject) => {
const req = indexedDB.open("ITEM_LIST");
req.onerror = () => {
reject(null);
};
req.onsuccess = () => {
resolve(req.result);
};
req.onupgradeneeded = (evt) => {
const _db = evt.target.result;
_db.createObjectStore("items", {
keyPath: "id",
autoIncrement: true,
});
};
});
}
return db;
};
})();
일반적인 관계형 DB처럼 transaction을 생성하고, 테이블(오브젝트 스토어)를 가져온 뒤, add 명령으로 원소를 추가하면 됩니다. 테이블은 앞으로 스토어라고 부르겠습니다.
const item = { // 추가할 컬럼(객체) 생성
created: new Date(),
content: "What is Lorem Ipsum?"
}
const db = await getDB(); // DB 가져오기
const transaction = db.transaction(["items"],"readwrite"); //items 스토어에 대한 readwrite 권한을 갖는 트랜젝션 생성
const itemStore = transaction.objectStore("items"); // items 스토어 소환
const request = itemStore.add(item); // 아이템 추가

popup의 개발자 도구를 켜서 애플리케이션 -> indexedDB를 확인하면 위와 같이 아이템이 추가된 것을 확인할 수 있습니다. id를 입력하지 않았지만 keyPath와 autoIncrement를 설정해두었기 때문에 자동으로 id가 생성되고 1 값이 할당된 것을 볼 수 있습니다.
마찬가지로 promise로 감싸고 함수화해봅시다.
function add(item) {
return new Promise(async (resolve, reject) => {
const db = await getDB();
if (!db) reject(false);
const transaction = db.transaction(["items"], "readwrite");
const itemStore = transaction.objectStore("items");
const request = itemStore.add(item);
request.onsuccess = () => {
resolve(true);
};
request.onerror = () => {
reject(false);
};
});
}
삭제는 delete 명령을 사용합니다. key를 넘겨주어 삭제합니다.
const key = 1;
const db = await getDB();
const transaction = db.transaction(["items"], "readwrite"); // 트랜젝션 생성
const itemStore = transaction.objectStore("items"); // 스토어 호출
const request = itemStore.delete(key); // 삭제
수정은 put 명령을 사용합니다. keyPath에 해당하는 property를 포함하는 객체를 수정해서 put 명령을 사용하면 값을 덮어씁니다.
const newObj = { // 변경할 예정인 객체
id:1,// keyPath에 해당하는 id가 포함되어야한다.
content:'hello' // 기존과의 차이점을 보기 위해 content를 hello로 바꾸고 created는 추가하지 않았다.
}
const db = await getDB();
const transaction = db.transaction(["items"], "readwrite");
const itemStore = transaction.objectStore("items");
const request = itemStore.put(newObj);


created가 사라지고 content도 hello로 잘 바뀐 것을 확인할 수 있습니다.
get 명령으로 값을 읽어옵니다. getAll을 통해 전부 읽기도 가능합니다.
// 원소 1개 지정해서 가져오기
const key = 1;
const db = await getDB();
const transaction = db.transaction(["items"]);
const objectStore = transaction.objectStore("items");
const request = objectStore.get(key);
// 원소 전부 가져오기
const db = await getDB();
const transaction = db.transaction(["items"]);
const objectStore = transaction.objectStore("items");
const request = objectStore.getAll();
지금까지 나온 내용을 종합해서 itemModel.js 파일을 만들어보겠습니다.
// scripts/itemModel.js
const getDB = (() => {
let db = null;
return async () => {
if (!db) {
db = await new Promise((resolve, reject) => {
const req = indexedDB.open("ITEM_LIST");
req.onerror = () => {
reject(null);
};
req.onsuccess = () => {
resolve(req.result);
};
req.onupgradeneeded = (evt) => {
const _db = evt.target.result;
_db.createObjectStore("items", {
keyPath: "id",
autoIncrement: true,
});
};
});
}
return db;
};
})();
export async function add(item) {
return new Promise(async (resolve, reject) => {
const db = await getDB();
if (!db) reject(false);
const transaction = db.transaction(["items"], "readwrite");
const itemStore = transaction.objectStore("items");
const request = itemStore.add(item);
request.onsuccess = () => {
resolve(true);
};
request.onerror = () => {
reject(false);
};
});
}
export async function remove(key) {
return new Promise(async (resolve, reject) => {
const db = await getDB();
if (!db) reject(false);
const transaction = db.transaction(["items"], "readwrite");
const itemStore = transaction.objectStore("items");
const request = itemStore.delete(key);
request.onsuccess = () => {
resolve(true);
};
request.onerror = () => {
reject(false);
};
});
}
export async function put(newObj) {
return new Promise(async (resolve, reject) => {
const db = await getDB();
if (!db) reject(false);
const transaction = db.transaction(["items"], "readwrite");
const itemStore = transaction.objectStore("items");
const request = itemStore.put(newObj);
request.onsuccess = () => {
resolve(true);
};
request.onerror = () => {
reject(false);
};
});
}
export async function get(key) {
return new Promise(async (resolve, reject) => {
const db = await getDB();
if (!db) reject(false);
const transaction = db.transaction(["items"]);
const objectStore = transaction.objectStore("items");
const request = objectStore.get(key);
request.onerror = () => {
reject(false);
};
request.onsuccess = () => {
resolve(request.result);
};
});
}
export async function getAll() {
return new Promise(async (resolve, reject) => {
const db = await getDB();
if (!db) reject(false);
const transaction = db.transaction(["items"]);
const objectStore = transaction.objectStore("items");
const request = objectStore.getAll();
request.onerror = () => {
reject(false);
};
request.onsuccess = () => {
resolve(request.result);
};
});
}
export default {
get,
getAll,
add,
remove,
put,
};
모델과 연결하기 전에 간단하게 content에서 background로 데이터를 보내는 로직을 작성합니다.
//scripts/content.js
document.addEventListener("copy", async () => {
const text = await navigator.clipboard.readText();
if (!chrome.runtime.sendMessage) return;
await chrome.runtime.sendMessage(
{
action:'NEW_CONTENT',
payload: text
}
);
});
background에서 전달받은 데이터를 model에 보내 저장하는 로직을 만들어봅시다.
//scripts/background.js
import itemModel from "/scripts/itemModel.js";
// message로 받은 내용 처리
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// message로 받은 내용중 NEW_CONTENT에 해당하는 내용은 모델에 저장합니다.
if(message.action === 'NEW_CONTENT') {
itemModel.add({ created: new Date(), content: message.payload });
sendResponse(true);
return true;
}
});
// context menu로 받은 내용 처리
chrome.contextMenus.onClicked.addListener(async (info) => {
if (info.menuItemId !== "Store") return;
if (info.selectionText) {
// text 타입을 저장하는 경우
await itemModel.add({
created: new Date(),
content: info.selectionText,
});
} else if (info.mediaType === "image") {
// image 타입을 저장하는 경우
const image = await fetch(info.srcUrl);
const blob = await image.blob();
await itemModel.add({
created: new Date(),
content: blob,
});
}
return true;
});
chrome extension에서 비동기를 사용할 때 return true를 해줘야함에 유의하세요.


텍스트와 이미지를 각각 복사했을 때

데이터가 추가된 것을 확인할 수 있습니다.
indexed DB를 통해 복사한 내용을 저장할 수 있음을 확인할 수 있었습니다.
다음 글에선 clipboard를 다뤄보며 저장한 복사본을 clipboard로 옮길 수 있는지에 대한 테스트를 해볼 것 같습니다.