[Copy Stack] 크롬익스텐션 데모 만들기 - indexed DB 사용하기

dev2820·2022년 12월 18일

프로젝트: Copy Stack

목록 보기
7/28

앞서 copy이벤트와 context menu를 통해 받아온 데이터를 indexed DB에 저장해 보는 작업을 할 겁니다.

indexed DB 사용법

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;
};

https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB#structuring_the_database

onupgradeneeded가 먼저 실행되고 onsuccess가 실행됩니다.

createObjectStore의 옵션은 keyPathautoIncrement 두 가지입니다. keyPath는 어떤 key를 unique key로 사용할지에 대한 정보입니다. autoIncrementtrue로 되어있다면 엔티티를 추가할 때 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를 입력하지 않았지만 keyPathautoIncrement를 설정해두었기 때문에 자동으로 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);

before

after

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();

model 파일 생성

지금까지 나온 내용을 종합해서 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,
};

model과 연결하기

모델과 연결하기 전에 간단하게 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로 옮길 수 있는지에 대한 테스트를 해볼 것 같습니다.

profile
공부,번역하고 정리하는 곳

0개의 댓글