IndexedDB 간단 정리

vhv3y8·2024년 2월 23일
0

목록 보기
4/8
  • 클라이언트 사이드에서 데이터 스토리지로 쓰인다.
  • 많은 데이터를 저장할 수 있고, 오프라인에서도 사용 가능하다.
  • 다양한 값들을 저장할 수 있다.

조금 더 구체적으로 정리해보면 다음과 같다.

  • 비동기적으로 동작하는데, Promise가 아닌 이벤트 핸들러 기반이다.
    • Promise 쓰려면 idb, Dexie.js 같은 wrapper 라이브러리 사용해야함
  • key-value pair를 저장하는 데이터베이스다.
  • 트랜잭션 기반 데이터베이스 모델로 만들어졌다.
  • DOM 이벤트를 사용해서 결과를 알려준다. (이벤트 type, target 사용)

목차

  • 데이터베이스 구조 이해 및 생성하기
    • Object Store
    • 데이터베이스 생성하기
  • 데이터 조회, 추가, 수정, 삭제하기
    • Transaction
    • 데이터 조회하기
    • 데이터 추가, 수정하기
    • 데이터 삭제하기
    • 한 번에 작성하기
  • Cursor와 Index
    • Cursor
      • IDBKeyRange
    • Index
      • Index 생성하기
      • Index 접근 및 사용
      • Index에서 Cursor 열기

데이터베이스 구조 이해 및 생성하기

우선 데이터베이스를 생성하고 접근하는 코드를 작성해보면 다음과 같다.

const request = window.indexedDB.open("dbName")
request.addEventListener("success", (event) => {
  const db = event.target.result
})

window.indexedDB.open()으로 데이터베이스를 열 수 있다.

Promise가 아닌 event 기반이므로 위 코드처럼 이벤트로 작성해야하고, event.target.result 또는 request.result로 데이터베이스에 접근할 수 있다.

Object Store

IndexedDB에서는 Object Store라는 방식으로 데이터를 저장한다.

Object Store는 형태로 데이터를 갖는다.

각 IndexedDB 데이터베이스는 여러 개의 Object Store를 저장할 수 있으며, 각 Object Store는 이름을 갖는다.

는 다양한 방식으로 구성될 수 있는데, key path, key generator, 또는 직접 명시된 값으로 구성될 수 있다.

key path 방식에서는 값에 객체(object)만 올 수 있으며, 그 값에서 어떤 프로퍼티를 key로 삼을지 지정해주는 방식이다.

따라서 값 안에서 키를 정하기 때문에 in-line key 방식이라고 할 수 있다.

key generator 방식은 이름 그대로 키를 생성해주는거다.

기본 값은 1이며 데이터를 추가할 때마다 1씩 증가한다고 한다.

키가 저장하는 값 밖에 있기 때문에 out-of-line key 방식이라고 할 수 있다.

이 방식에선 값이 키랑 상관이 없기 때문에 모든 형태의 값을 받을 수 있다.

const objectStore = db.createObjectStore("toDoList", {
  keyPath: "taskTitle",
})

이런 키 구성 방식들은 Object Store를 생성할 때 지정해줄 수 있는데, keyPath(key path)와 autoIncrement(key generator)를 각각 선택적으로 받을 수 있다.

만약에 keyPath에 값을 주고 autoIncrement에도 true를 준다면, 값으로 받은 객체에다가 keyPath를 이름으로 하는 프로퍼티를 만들어서 생성된 키(숫자)를 저장해준다고 한다.

키를 직접 명시하는 방식을 쓸 때는 Object Store 생성시에는 옵션을 비워두고, 데이터를 처리할 때 직접 적어주는 식으로 작성한다고 한다.

자세한 내용은 문서 참고

데이터베이스 생성하기

간단한 예시를 보자.

const dbRequest = window.indexedDB.open("myDB", 1)
dbRequest.addEventListener("upgradeneeded", (event) => {
  const db = event.target.result

  const objectStore = db.createObjectStore("customers", { keyPath: "ssn" })
})

이름이 myDB이고 버전이 1인 데이터베이스를 열었고, 이름이 customers인 Object Store를 생성했다.

Object Store의 이름과 형태만 설정해준거고, 값은 아직 저장하지 않았다.

이처럼 Object Store를 생성하거나 삭제하는 등의 데이터베이스의 구조를 수정하는 작업은 데이터베이스를 업데이트 할 때에만 할 수 있다.

upgradeneeded 이벤트에서만 수행할 수 있는데, 이 이벤트는 데이터베이스를 생성했거나 버전을 업데이트했을 때 트리거 된다.

데이터 조회, 추가, 수정, 삭제하기

Transaction

IndexedDB에서 모든 데이터 접근은 트랜잭션을 통해서 이루어진다.

따라서 항상 트랜잭션을 먼저 시작해야한다.

const transaction = db.transaction(["customers"], "readwrite")

데이터베이스의 transaction() 메서드로 시작할 수 있다.

트랜잭션은 해당 트랜잭션에서 다룰 Object Store들의 이름, 그리고 모드 등으로 구성된다.

모드에는 "readonly", "readwrite", "versionchange"가 있다.

"versionchange" 모드는 직접적으로 설정해줄 수 없으며, upgradeneeded 이벤트 안에서 이루어지는 트랜잭션에 자동으로 부여된다고 한다.

그리고 트랜잭션은 request를 요청했을 때에만 연장되며, 그러지 않고 이벤트 루프로 돌아가게 되면 바로 끝나버린다고 한다.

트랜잭션은 error, abort, complete 3가지의 DOM event를 받을 수 있다고 한다.

만약에 요청을 했고 성공했다면(complete), 그 콜백에서 다시 요청을 해서 트랜잭션을 연장시킬 기회를 얻게 되는거라고 한다.

즉 트랜잭션을 만든 후 연속적으로 요청을 하면 계속 이어갈 수 있고, 그렇지 않으면 바로 종료되는거다.

데이터 조회하기

예시를 보자.

window.indexedDB.open("dbName").addEventListener("success", (event) => {
  const db = event.target.result

  const transaction = db.transaction(["customers"], "readonly")

  const objectStore = transaction.objectStore("customers")

  const request = objectStore.get("444-44-4444")

  request.addEventListener("success", (event) => {
    console.log(`Name for SSN 444-44-4444 is ${request.result.name}`)
  })
})

트랜잭션에서 objectStore() 메서드로 Object Store에 접근할 수 있으며, 이 Object Store에서 조회, 추가, 삭제 등의 request를 보내는 방식이다.

이벤트 핸들러 기반 방식이기 때문에 request에 이벤트 리스너를 붙여주는 식으로 사용할 수 있다.

  • get(key)
  • getAll(query?: key | IDBKeyRange, count?)
  • getKey(key)
  • getAllKeys(query?: IDBKeyrange, count?)

이 메서드들은 모두 request를 돌려주며, request의 success 이벤트에서 값을 받을 수 있다.

데이터 추가, 수정하기

// db 접근 부분 생략

const transaction = db.transaction(["customers"], "readwrite")

const objectStore = transaction.objectStore("customers")

customerData.forEach((customer) => {
  const request = objectStore.add(customer)
  request.addEventListener("success", (event) => {
    // ...
  })
})
  • add(value, key?)
  • put(item, key?) : 추가 또는 업데이트

이 메서드들은 모두 request를 돌려주며, request의 success 이벤트에서 값을 받을 수 있다.

데이터 삭제하기

// db 접근 부분 생략

const transaction = db.transaction(["customers"], "readwrite")

const objectStore = transaction.objectStore("customers")

const request = objectStore.delete("444-44-4444")
request.addEventListener("success", (event) => {
  // ...
})
  • delete(key)

이 메서드는 request를 돌려주며, request의 success 이벤트에서 값을 받을 수 있다.

제대로 지워졌으면 resultundefined가 된다고 한다.

한 번에 작성하기

db
  .transaction(["customers"], "readwrite")
  .objectStore("customers")
  .get("444-44-4444").onsuccess = (event) => {
  console.log(event.target.resut)
}

Cursor와 Index

  • Cursor
    • IDBKeyRange
  • Index
    • Index 생성하기
    • Index 접근 및 사용
    • Index에서 Cursor 열기

Cursor

Cursor는 Object Store의 값들을 반복문처럼 iterate 하게 해준다.

const objectStore = db.transaction("customers").objectStore("customers")

objectStore.openCursor().addEventListener("success", (event) => {
  const cursor = event.target.result

  if (cursor) {
    console.log(`Name for SSN ${cursor.key} is ${cursor.value.name}`)
    cursor.continue()
  }
})

커서는 openCursor()로 열 수 있는데, get()이나 put(), delete()처럼 Object Store에서 사용할 수 있으며 request와 이벤트 리스너를 사용한다.

Cursor에서는 에 접근할 수 있다.

success 이벤트의 event.target.result는 Cursor이며, 다음 커서로 넘어가려면 cursor.continue()를 사용해주면 된다.

그러면 success 이벤트가 다시 실행되면서 다음 커서를 돌려주는 식인거다.

success 이벤트에 값을 하나하나 돌려주는 식으로 반복이 수행된다고 할 수 있다.

Object Store에 있는 모든 값을 가져오는 건 getAll() 메서드로도 가능하지만, 커서를 쓰면 메모리에 모든 값을 한꺼번에 올리지 않아도 되고, 다른 트랜잭션이 동시에 수행될 수 있게 해주는 등등의 장점들이 있기 때문에 사용된다고 한다.

const customers = []

// objectStore 접근 부분 생략

objectStore.openCursor().onsuccess = (event) => {
  const cursor = event.target.result
  if (cursor) {
    customers.push(cursor.value)
    cursor.continue()
  } else {
    console.log(`Got all customers: ${customers}`)
  }
}

이 예시에서는 커서를 통해 모든 값을 customers 배열에 저장하고 있다.

IDBKeyRange

Cursor를 열 땐 방향범위를 지정해줄 수 있다.

방향은 오름차순 또는 내림차순이 될 수 있다.

범위는 IDBKeyRange를 통해 정해질 수 있는데, IDBKeyRange 객체는 IndexedDB에서 범위를 나타내는 용도로 다양하게 쓰인다.

IDBKeyRange에는 다음과 같은 static 메서드가 있다 :

  • only(value)
  • upperBound(upper, open?: boolean)
  • lowerBound(lower, open?: boolean)
  • bound(lower, upper, lowerOpen?: boolean, upperOpen?: boolean)

이 메서드들은 IDBKeyRange 객체를 만들어서 돌려준다.

const onlyDonna = IDBKeyRange.only("Donna")

const pastBill = IDBKeyRange.lowerBound("Bill", true) // Bill 초과

const uptoDonna = IDBKeyRange.upperBound("Donna", true) // Donna 미만

IDBKeyRangeopenCursor()에 넘겨지는 등 범위로써 사용될 수 있다.

Index

Index는 key가 아닌 다른 프로퍼티로 데이터베이스를 검색하게 해주는 방식이다.

const index = objectStore.index("name")

index.get("Donna").onsuccess = (event) => {
  console.log(`Donna's SSN is ${event.target.result.ssn}`)
}

예를 들어 Donna라는 이름을 가진 사람을 key로만 검색하려면 key를 기준으로 하나하나 iterate 하면서 찾는 이름을 갖고 있는지 확인해야 한다.

하지만 이 예시처럼 Index를 사용하면 name 프로퍼티에 대해 바로 get()으로 검색할 수 있다.

Index 생성하기

Index를 사용하려면 미리 생성해둬야 한다.

Index 생성은 데이터베이스의 구조 및 수정과 관련있는 작업이고, 즉 upgradeneeded 이벤트에서만 수행할 수 있다.

const openRequest = window.indexedDB.open("dbName")

openRequest.addEventListener("upgradeneeded", (event) => {
  const db = event.target.result

  const storeCustomers = db.createObjectStore("customers", { keyPath: "ssn" })

  storeCustomers.createIndex("name", "name")
})

Object Store의 createIndex() 메서드로 생성할 수 있으며, Index의 이름, keyPath 등을 받는다.

  • createIndex(indexName, keyPath, options?)
  • deleteIndex(indexName)

Index 접근 및 사용

그리고 Index로 접근도 Object Store에서 할 수 있는데, 접근은 조회, 추가, 삭제처럼 일반 트랜잭션에서 할 수 있다.

  • index(name)
// db 접근 생략

const objectStore = db
  .transaction(["customers"], "readwrite")
  .objectStore("customers")

const index = objectStore.index("name")

index.get("Donna").addEventListener("success", (event) => {
  console.log(event.target.result)
})

Index에서 Cursor 열기

앞에서 봤던 Cursor 예시는 Object Store에서 바로 열었다.

Cursor는 Index에서도 열 수 있다.

// index 접근 생략

index.openCursor().onsuccess = (event) => {
  const cursor = event.target.result
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.value is the whole object.
    console.log(
      `Name: ${cursor.key}, SSN: ${cursor.value.ssn}, email: ${cursor.value.email}`
    )
    cursor.continue()
  }
}

참고

profile
개발 기록, 미래의 나에게 설명하기

0개의 댓글