조금 더 구체적으로 정리해보면 다음과 같다.
비동기적
으로 동작하는데, Promise가 아닌 이벤트 핸들러 기반
이다. idb
, Dexie.js
같은 wrapper 라이브러리 사용해야함key-value
pair를 저장하는 데이터베이스다.트랜잭션
기반 데이터베이스 모델로 만들어졌다.type
, target
사용)IDBKeyRange
우선 데이터베이스를 생성하고 접근하는 코드를 작성해보면 다음과 같다.
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
로 데이터베이스에 접근할 수 있다.
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
이벤트에서만 수행할 수 있는데, 이 이벤트는 데이터베이스를생성
했거나버전을 업데이트
했을 때 트리거 된다.
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 이벤트에서 값을 받을 수 있다.
제대로 지워졌으면 result
가 undefined
가 된다고 한다.
db
.transaction(["customers"], "readwrite")
.objectStore("customers")
.get("444-44-4444").onsuccess = (event) => {
console.log(event.target.resut)
}
IDBKeyRange
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 미만
IDBKeyRange
는 openCursor()
에 넘겨지는 등 범위로써 사용될 수 있다.
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 생성은 데이터베이스의 구조 및 수정
과 관련있는 작업이고, 즉 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로 접근도 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)
})
앞에서 봤던 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()
}
}