본문 바로가기
Frontend/JavaScript

JavaScript : 웹 스토리지 (2) - IndexedDB란?

by 코딩쥐 2024. 8. 18.

Cookie와 Local Storage, Session Storage에 대해 공부했었다. 웹 스토리지의 경우에는 소량의 단순한 데이터를 효율적으로 저장하는데 적합하다. 대량의 구조적 데이터를 처리하기 위해서는 IndexedDB라는 API를 사용하여 대규모의 복잡한 데이터를 관리할 수 있다. indexedDB는키-값 쌍으로 데이터를 저장하며, 비동기 방식(함수 호출 시 결과를 반환하지 않는 대신, 함수를 종료할 때 동작할 콜백함수를 전달함. 요청한 동작이 종료되면 success, error와 같은 이벤트를 받아 다음 동작을 진행)으로 동작한다. 

 

IndexedDB은 다음과 같이 사용한다.

  1. 데이터베이스 생성 및 열기
  2. 데이터베이스에 objectStore(객체저장소) 생성 : objectStore에 저장된 객체들은 keypath로 구분한다.
  3. Transaction 설정 (데이터 읽기, 쓰기, 제거 등의 작업 요청) 

 

IndexedDB 사용 방법

1. Database 생성 및 열기

브라우저에서 여러 개의 데이터베이스를 만들 수 있으며, 데이터베이스는 버전 정보를 가지고 있다. 데이터베이스를 수정했을 시에는 버전도 수정해주어야 한다. 버전은 위로 올라가는 것만 가능하다. 버전의 경우 ObjectStore를 새로 생성하거나, ObjectStore에서 인덱스를 만들거나 삭제할 때, 데이터베이스의 구조를 변경할 때에 변경해야 한다. 데이터베이스를 생성할 때 발생하는 이벤트는 success, error, upgradedneeded, blocked이 있다. 기존에 있는 버전보다 요청하는 버전이 높을경우 upgradeneeded 이벤트가 발생한다.

  • window.indexedDB.open(DB이름, DB버전)
    데이터베이스와의 연결을 요청하는 함수다. 호출 결과로 IDBOpenDBRequest 타입의 객체를 반환한다.

  • deleteDatabase(DB이름);
    데이터베이스 삭제를 요청하는 함수다.
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        const db_name  = "reservationDB";
        const db_version = 1;
        
        // window.indexedDB.open 에서 window는 생략이 가능하다. 
        const request = indexedDB.open(db_name, db_version);

        // indexedDB를 처음 열었을 때 / db버전을 올렸을 때 업그레이드가 시행된다. 
        request.onupgradeneeded = ()=>{
            console.log("indexedDB 업그레이드 시행");
        };

        // indexedDB 여는 것을 성공했을 때
        request.onsuccess = ()=>{
            console.log("indexedDB 여는 것 성공");
        };

        // indexedDB 여는 것을 실패했을 때
        request.onerror = () => {
            console.log("indexedDB를 여는 것을 실패했습니다.")
        };
    </script>
</body>

 

처음 생성이 되면 업그레이드(onupgradeneeded)가 시행되고, 그 다음 데이터베이스를 여는 것을 성공했을때(onsuccess)와 관련된 로직이 시행된 것을 볼 수 있다. 그리고 나서 indexedDB에 들어가 확인해보면, 설정해놓은 DB이름(reservationDB)과 DB버전(1)을 가진 데이터베이스가 생성이 된 것을 볼 수 있다.

 

2. Database에 ObjectStore(객체저장소) 생성하기

ObjectStore은 객체로 이루어진 데이터를 담는 공간이다. 여러 개의 key와 value를 가진 레코드를 가지고 있다. ObjectStore의 이름은 고유해야하며, 각 객체의 유일성을 부여하기 위해 keyPath를 정의해야 한다. 

  • IDBOpenDBRequest.createObjectStore("객체저장소이름", {optionalParameter});
    optionalParameter에는 keypath(일종의 기본키)와 autoIncrement(true일 경우 key가 자동으로 공급된다)를 지정할 수 있다.

  • IDBOpenDBRequest.deleteObjectStore("객체저장소이름");
    해당하는 ObjectStore를 삭제한다. 
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        const db_name  = "reservationDB";
        const db_version = 1;
        
        const request = indexedDB.open(db_name, db_version);

        request.onupgradeneeded = (e)=>{
            // 데이터베이스를 열고 그 안에 객체저장소를 생성한다. 
            const db = e.target.result;
            if(!db.objectStoreNames.contains("customer")){
                db.createObjectStore("customer", {
                    // id를 keypath로 지정하고 자동으로 1씩 오르게 함
                    keypath: "id",
                    autoIncrement: true
                })
            }
        };

        request.onsuccess = (e)=>{
            console.log("indexedDB를 여는 것을 성공했습니다.")
        };

        request.onerror = () => {
            console.log("indexedDB를 여는 것을 실패했습니다.")
        };
    </script>
</body>

</html>

 

IndexedDB안에 reservationDB라는 데이터베이스가 생성이되고, 그 안에  customer이라는 ObjectStore가 저장된 모습을 볼 수 있다.

 

ObjectStore의 함수

ObjectStore의 함수들은 transaction을 통해서 수행된다. 

함수 설명
add(item, optionalKey) item의 복제본을 새로운 데이터로 객체저장소에 저장한다. optionalKey가 이미 객체 안에 지정되어 있으면 오류가 발생한다. 
put(item, optionalKey) item의 복제본을 새로운 데이터로 객체저장소에 저장한다. optionalKey가 이미 객체 안에 지정되어 있으면 수정으로 동작하고, 없다면 추가로 동작한다. transaction 모드가 readwrite여야 한다. 
delete(recordKey) keypath에서 recordKey와 동일한 자료를 삭제한다. 
get(key) 주어진 key로 객체저장소에서 동일한 keypath를 가진 자료를 조회한다. 조회결과는 result 속성으로 확인한다.  
getAll([query, count]) IDBKeyRange 타입의 객체로 조회결과를 필터링할 때 사용한다. 
clear() 객체저장소에 저장된 모든 자료를 삭제한다.
getAllKeys([query, count]) 객체저장소에 저장된 모든 객체들의 키를 반환한다. 
createIndex(이름, 속성, optionsObj) 인덱스를 생성한다. name은 인덱스의 이름, property는 인덱스로 사용할 객체의 속성, optionsObj는 해당 인덱스가 가질 속성을 설정하는데 unique: true;일 경우 인덱스 값의 중복을 허용하지 않는다. versionChange 트랜잭션 모드에서 호출되어야 해서, 일반적으로 upgradeneeded 이벤트에서 호출된다. 
deleteIndex(name) name으로 등록된 인덱스를 삭제한다. 
index(name) name으로 등록된 인덱스를 반환한다.
openCursor([keyRang e, direction]) 처음 파라미터는 검색조건을 나타내고 두번째 파라미터는 데이터의 정렬조건을 나타낸다.
count([keyRange]) IDBKeyRange의 범위에 적합한 데이터의 개수가 반환되거나 파라미터가 없을 경우 전체데이터의개수를 반환한다.

 

3. Transaction 설정

Transaction은 데이터베이스의 상태를 변화시키기 위해 수행하는 작업 단위를 의미한다.

  • IDBDatabase.transaction(["객체저장소1", "객체저장소2", ..], transaction모드);
    transaction 모드에는 versionchange, readonly, readwrite가 있다. ObjectStore의 레코드를 읽을 때 readonly를 사용하고, ObjectStore의 레코드를 읽거나 레코드를 수정, 추가, 삭제 등을 시행할 때에는 readwrite모드를 사용한다. 

Transaction을 시행할 때 발생하는 이벤트는 complete, abort(연결된 데이터베이스에 접근이 실패한 경우), error(데이터베이스 접근시 오류가 발생한 경우)가 있다. 

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <label for="name">이름 : </label><input type="text" id="name">
    <label for="date">날짜 : </label><input type="date" name="date" id="date">
    <button id="register">등록</button>
    <script>
        const db_name = "reservationDB";
        const db_version = 1;
        let db;

        const request = indexedDB.open(db_name, db_version);

        request.onupgradeneeded = (e) => {
            db = e.target.result;
            if (!db.objectStoreNames.contains("customer")) {
                db.createObjectStore("customer", {
                    keypath: "id",
                    autoIncrement: true
                })
            }
        };

        request.onsuccess = (e) => {
            db = e.target.result;
            console.log("indexedDB를 여는 것을 성공했습니다.")
        };

        request.onerror = () => {
            console.log("indexedDB를 여는 것을 실패했습니다.")
        };

        // 등록 버튼을 눌렀을 때 indexedDB의 customer ObjectStore에 저장
        document.querySelector("#register").addEventListener("click", () => {
            let data = {
                name: document.querySelector("#name").value,
                date: document.querySelector("#date").value
            }

            // DB를 열고 transaction을 설정한다. 값을 집어넣어야 하기 때문에 readwrite모드로 설정한다.
            const transacObj = db.transaction(["customer"], "readwrite");

            // 트랜잭션 성공시
            transacObj.addEventListener("complete", () => {
                console.log("transaction 성공")
            });
            // 트랜잭션 접근 실패시
            transacObj.addEventListener("abort", () => {
                console.log("transaction 취소");
            });
            // 트랜잭션 접근 오류시
            transacObj.addEventListener("error", () => {
                console.log("transaction 에러");
            });
            // transaction이 설정된 객체저장소들에서 데이터를 넣을 해당 객체저장소를 불러온다.
            const objStore = transacObj.objectStore("customer");
            // 데이터를 삽입한다.
            const request = objStore.put(data);
            // 데이터 삽입 성공시
            request.onsuccess = ()=>{
                console.log("데이터삽입성공");
            }
            // 데이터 삽입 실패시
            request.onerror = ()=>{
                console.log("데이터삽입실패")
            }
        })
    </script>
</body>

</html>

 

 

indexedDB여는 것을 성공하고, transaction을 통해 데이터를 삽입하는 것 까지 성공하면 아래처럼 input태그에 작성된 value 값이 customer 객체저장소에 저장되어있는 모습을 볼 수 있다. keyPath의 경우 자동적으로 부여된 것을 볼 수 있다.

 


IndexedDB : 데이터의 조회 / 수정 / 삭제

데이터의 조회

특정 데이터 값을 조회하려면 get()을 사용해 해당하는 key값의 데이터를 조회할 수 있고, 전체 데이터를 조회하려면 cursor를 통해서 데이터를 조회할 수 있다.

<script>
	// 데이터 조회 
        document.querySelector("#check").addEventListener("click", () => {
            const transacObj = db.transaction(["customer"], "readwrite");
            const objStore = transacObj.objectStore("customer");

            // get으로 데이터 조회 (key값을 알고 있을 때)
            const objStoreget1 = objStore.get(1);
            objStoreget1.onsuccess = (e)=>{
                console.log(e.target.result); // {name: '코딩쥐', date: '2024-08-18'}
            }

            // cursor를 통해 데이터 조회 : 전체 데이터를 조회해서 #data에 p태그 삽입
            objStore.openCursor().onsuccess = (e) =>{
                let cursor = e.target.result;

                if(cursor){
                    let customer = document.createElement("p");
                    customer.textContent = `이름: ${cursor.value.name}, 날짜: ${cursor.value.date}`;
                    document.querySelector("#data").append(customer);
                    cursor.continue();
                }
            }
        });
</script>

 

조회 버튼을 눌렀을 때 현재 indexedDB에 들어있는 레코드들이 작성된 모습을 볼 수 있다.

 

데이터의 수정

put()을 사용해서 레코드 값을 수정할 수 있다. optionalKey가 이미 객체 안에 지정되어 있으면 수정으로 동작하고, 없다면 추가로 동작하기 때문에 key값을 포함해서 객체를 넘겨야 한다.   

<script>
	// 데이터 수정 
        document.querySelector("#change").addEventListener("click", () => {
            const transacObj = db.transaction(["customer"], "readwrite");
            const objStore = transacObj.objectStore("customer");
            objStore.put(
            {name: document.querySelector("#name").value,date: document.querySelector("#date").value},1);
        });
</script>

key 값이 1인 객체가 수정되도록 설정해놓았기 때문에, input에 값을 넣고 수정버튼을 넣으면 키 값이 1인 데이터가 수정이 된 모습을 볼 수 있다. 

 

데이터의 삭제

delete()를 통해 특정 키의 값을 삭제할 수 있고, clear()를 사용해서 특정 저장소의 모든 데이터를 삭제할 수 있다.

<script>
        // 데이터 삭제
        document.querySelector("#delete").addEventListener("click", () => {
            const transacObj = db.transaction(["customer"], "readwrite");
            const objStore = transacObj.objectStore("customer");
            objStore.delete(2);
        })
</script>

 

삭제를 누르고서 key 값이 2인 객체가 삭제가 된 것을 볼 수 있다.

 

<< 예제 전체 코드 >> 

더보기
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <label for="name">이름 : </label><input type="text" id="name">
    <label for="date">날짜 : </label><input type="date" name="date" id="date">
    <button id="register">등록</button><br>
    <button id="check">조회</button>
    <button id="change">수정</button>
    <button id="delete">삭제</button>
    <div id="data"></div>
    <script>
        const db_name = "reservationDB";
        const db_version = 1;
        let db;

        const request = indexedDB.open(db_name, db_version);

        request.onupgradeneeded = (e) => {
            db = e.target.result;
            if (!db.objectStoreNames.contains("customer")) {
                db.createObjectStore("customer", {
                    keypath: "id",
                    autoIncrement: true
                })
            }
        };

        request.onsuccess = (e) => {
            db = e.target.result;
            console.log("indexedDB를 여는 것을 성공했습니다.")
        };

        request.onerror = () => {
            console.log("indexedDB를 여는 것을 실패했습니다.")
        };

        // 데이터 등록
        document.querySelector("#register").addEventListener("click", () => {
            let data = {
                name: document.querySelector("#name").value,
                date: document.querySelector("#date").value
            }
            const transacObj = db.transaction(["customer"], "readwrite");
            const objStore = transacObj.objectStore("customer");
            const request = objStore.put(data);
            request.onsuccess = () => {
                console.log("데이터삽입성공");
            }
            request.onerror = () => {
                console.log("데이터삽입실패")
            }
        })

        // 데이터 조회 
        document.querySelector("#check").addEventListener("click", () => {
            const transacObj = db.transaction(["customer"], "readwrite");
            const objStore = transacObj.objectStore("customer");

            // get으로 데이터 조회 (key값을 알고 있을 때)
            const objStoreget1 = objStore.get(1);
            objStoreget1.onsuccess = (e)=>{
                console.log(e.target.result); // {name: '코딩쥐', date: '2024-08-18'}
            }

            // cursor를 통해 데이터 조회 : 전체 데이터를 조회해서 #data에 p태그 삽입
            objStore.openCursor().onsuccess = (e) =>{
                let cursor = e.target.result;

                if(cursor){
                    let customer = document.createElement("p");
                    customer.textContent = `이름: ${cursor.value.name}, 날짜: ${cursor.value.date}`;
                    document.querySelector("#data").append(customer);
                    cursor.continue();
                }
            }
        });

        // 데이터 수정 
        document.querySelector("#change").addEventListener("click", () => {
            const transacObj = db.transaction(["customer"], "readwrite");
            const objStore = transacObj.objectStore("customer");
            objStore.put({name: document.querySelector("#name").value,date: document.querySelector("#date").value}, 1);
        });

        // 데이터 삭제
        document.querySelector("#delete").addEventListener("click", () => {
            const transacObj = db.transaction(["customer"], "readwrite");
            const objStore = transacObj.objectStore("customer");
            objStore.delete(2);
        })
    </script>
</body>

</html>