IndexedDB란?
브라우저에 데이터를 영구적으로 저장하는 방법 중 하나로 HTML5부터 제공하는 기능이다. index를 설정하기 때문에 빠르게 데이터를 검색할 수 있다. 브라우저에 데이터를 영구적으로 저장하는 방법으로 Cookie, LocalSotrage도 있는데 이들과 비교하여 IndexedDB의 특징에 대해 알아보자.
Cookie는 네트워크에 포함되어 데이터가 전송되기 때문에 로그인 정보와 같은 중요한 데이터를 저장하기에는 보안성이 떨어졌다. 그리고 저장 공간도 4KB이기 때문에 데이터를 저장하기에 충분하지 않다. 이에 네트워크에 포함되지 않고 저장 공간이 더 많은 스토리지가 필요하게 되어 등장한 것이 바로 localstorage이다. localstorage는 중요 데이터가 네트워크에 포함되지 않아 보안성이 높아졌으며 저장 공간도 5MB로 Cookie와는 비교가 되지 않을 정도로 많아졌다. 하지만 localstorage 역시 Cookie와 마찬가지로 문자열 데이터만을 저장할 수 있고 작동 방식이 동기식으로 작동하기 때문에 이윽코 한계를 겪게 된다. 그리하여 등장한 것이 바로 IndexedDB이다. IndexDB는 문자열 데이터 뿐만 아니라 모든 타입의 데이터를 저장할 수 있다. 그리고 작동 방식도 비동기식으로 작동할 수 있게 되었다. 하지만 비동기식으로 작동하는 것이 무조건 좋은 것은 아니기 때문에 상항에 따라 사용할 스토리지를 구분해야한다.
Cookie | LocalStorage | IndexedDB | |
저장공간 | 4KB | 5MB | Chrome : 80% IE > 10 : 250MB, 10MB 알람 Firefox : 50% Safari : 1GB, 200MB 단위 요청 |
---|---|---|---|
네트워크에 포함 | 포함됨 | 포함안됨 | 포함안됨 |
데이터 타입 | 문자열 | 문자열 | 제한없음 |
동작방법 | 동기식 | 동기식 | 비동기식 |
난이도 | 쉬움 | 쉬움 | 어려움 |
indexedDB 구조
Database
- 브라우저는 여러개의 Database를 가질 수 있다.
- Database는 Vesion 정보가 있고 여러개의 Object Store를 가질 수 있다.
open(db_name, version)
으로 Database를 열도록 요청한다.
Object Store
- Object를 Grouping하여 담아주는 공간이다.
- 여러개의 레코드(key-value)를 가질 수 있다.
- ObjectStore의 이름은 고유해야 한다.
- Database가 생성, 수정될 때
createObjectStore(store_name, {keyPath:'id'})
함수로 생성한다.
IndexedDB 사용법
Database 생성
데이터베이스 열기
if (!window.indexedDB) {
window.alert("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.")
} else {
const dbReq = indexedDB.open('opentutorials',1);
}
open()
함수로 데이터베이스를 열수 있는데 첫번째 인자에는 데이터베이스 이름을, 두번째 인자에는 버전 정보를 넣는다.
success 이벤트로 데이터베이스 생성
if (!window.indexedDB) {
window.alert("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.")
} else {
const dbReq = indexedDB.open('opentutorials',1);
let db; // 전역적으로 데이터베이스에 접근할 수 있게 선언한다.
// 방법 1.
dbReq.onsuccess = function(evnet){
// event.target = dbReq
db = event.target.result;
}
// 방법 2.
dbReq.addEventListener('success', function(event){
db = event.target.result; // result를 통해 생성된 데이터베이스에 엑세스 할 수 있는 레퍼런스를 얻을 수 있다.
});
}
Object Store 생성
Object Store 생성하는 최적의 환경
success 이벤트는 데이터베이스를 실행할 때마다 실행되기 때문에 success 이벤트 안에서 생성하는 것보다 데이터베이스가 업데이트 될 때만 실행하는 것이 좋다.
일반적인 데이터는 서버 하나에만 저장되기 때문에 데이터를 수정, 삭제할 경우 한번에 삭제할 수 있다. 하지만 IndexedDB와 같이 브라우저에 데이터를 저장하게 되면 사용자 각각의 브라우저에서 데이터를 수정, 삭제해야 된다. 그래서 IndexedDB는 Version을 가지고 있다.
IndexedDB의 Version은 open() 함수의 두번째 인자로 전달할 수 있다. 이 두번째 인자가 변경되면 upgradeneeded 이벤트를 실행시킨다.
if (!window.indexedDB) {
window.alert("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.")
} else {
const dbReq = indexedDB.open('opentutorials',3);
let db;
dbReq.onsuccess = function(evnet){
console.log('success');
db = event.target.result;
});
// 방법 1.
dbReq.onupgradeneeded = function(event){
console.log('upgradeneeded');
}
// 방법 2.
dbReq.addEventListener('upgradeneeded', function(event){
console.log('upgradeneeded');
});
}
Version이 변경되었을 때 한번만 실행시켜주기 때문에 리로드를 하게되면 success 이벤트만 실행되는 것을 확인할 수 있다.
Object Store 생성
if (!window.indexedDB) {
window.alert("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.")
} else {
const dbReq = indexedDB.open('opentutorials',3);
let db;
dbReq.onsuccess = function(evnet){
console.log('success');
db = event.target.result;
});
dbReq.onupgradeneeded = function(event){
console.log('upgradeneeded');
db = event.target.result;
db.createObjectStore('topics', {keyPath:'id', autoIncrement:true});
}
}
createObjectStore()
로 Object Store를 생성할 수 있는데 여기서 첫번째 인자에는 Object Store의 이름을, 두번째 인자에는 옵션을 정의한다.
keyPath:'id'
: id라는 프로퍼티를 해당 Object의 식별자로 사용
autoIncrement:true
: Object Store에 Object가 추가 될 때마다 id를 자동으로 1씩 증가시켜 중복되지 않는 식별자를 만들어준다.
Transaction 시작
db.transaction('objectStoreName', 'status');
Object Store에 데이터를 넣기 위해서는 먼저 transaction을 시작해야한다. 어떤 Object Store에서 transaction을 시작할지 지정하고 해당 Object Store에 어떤 상태로 접근할지 지정한다.
Transactionstatus 종류
readWrite : Object Store에 데이터를 추가
readonly : Object Store의 데이터를 검색
Object 다루기 : 데이터베이스 작업 요청
데이터 추가
ObjectStoreName.add(data);
data는 object 형식으로 넣어준다.
데이터 검색
ObjectStoreName.get(key);
Object Store를 생성할 때 지정한 keyPath의 값으로 데이터를 검색할 수 있다.
데이터 전체 검색
ObjectStoreName.getAll();
데이터가 많은 경우 cursor로 분할해서 가지고 와야 한다.
데이터 수정
ObjectStoreName.put(data);
수정할 데이터의 keyPath의 값과 수정할 데이터(object)를 지정하여 데이터를 수정할 수 있다.
데이터 삭제
ObjectStoreName.delete(key);
Object Store를 생성할 때 지정한 keyPath의 값으로 데이터를 삭제할 수 있다.
에러 처리하기
데이터베이스 에러 처리는 onerror()
를 사용하여 처리할 수 있다. 데이터베이스를 생성할 때 뿐만 아니라 데이터의 CRUD 처리시에도 사용이 가능하다.
데이터베이스 생성시 에러 처리
// 방법 1.
dbReq.onerror = function(evnet){
const error = event.target.error;
console.log('error', error.name);
}
// 방법 2.
dbReq.addEventListener('error', function(event){
const error = event.target.error;
console.log('error', error.name);
});
데이터 CRUD 처리시 에러 처리
// 데이터 추가
ObejectStoreName.add(data).onerror = function(event){
console.log('error', error.name);
}
// 데이터 검색
ObejectStoreName.get(key).onerror = function(event){
console.log('error', error.name);
}
// 데이터 전체 검색
ObejectStoreName.getAll.onerror = function(event){
console.log('error', error.name);
}
// 데이터 수정
ObejectStoreName.put(data).onerror = function(event){
console.log('error', error.name);
}
// 데이터 삭제
ObejectStoreName.delete(key).onerror = function(event){
console.log('error', error.name);
}
버전 관리
const dbReq = indexedDB.open('opentutorials',2);
dbReq.upgradeneeded = function(evnet){
console.log('upgradeneeded');
db = event.target.result;
const oldVersion = event.oldVersion;
if(oldVersion < 1){
db.createObjectStore('topics', {keyPath:'id', autoIncrement:true});
}
if(oldVersion < 2){
db.createObjectStore('topics', {keyPath:'id', autoIncrement:true});
}
if(oldVersion < 3){
db.createObjectStore('topics', {keyPath:'id', autoIncrement:true});
}
}
open() 함수의 두번째 인자로 버전을 관리할 수 있는데 upgradeneeded() 함수로 event를 받아서 oldVersion 관리를 할 수 있다. 처음 방문하는 사용자는 3가지 if문이 모두 지나가게 되고, 예전에 방문한 적이 있는 버전이 1인 사용자는 두번째, 세번째 if문을 지나가게된다. 이러한 기능을 이용하여 버전을 관리할 수 있다.
Cursor 사용하는 방법
만약에 데이터의 양이 많은 경우 getAll()
함수로는 부하가 걸릴 수 있다. 그렇기 때문에 대용량의 데이터를 가지고 오는 경우 Cursor를 사용하는 것을 지향한다.
Cursor은 데이터베이스의 Object Store를 순회하거나 반복할 수 있다. obejctStore.openCursor()
로 생성할 수 있고 데이터베이스 생성이나 데이터 CRUD 처리 시와 마찬가지로success 이벤트까지 걸어야지 작동한다. cursor.continue()
로 해당 Object Store를 다시 순회할 수 있다.
function getDataList(){
const transaction = db.transaction('topics', 'readonly');
const objectStore = transaction.objectStore('topics');
const mCursor = objectStore.openCursor();
mCursor.onsuccess = function(event){
var cursor = event.target.result;
if(cursor){
console.log(cursor.value);
cursor.continue();
}else{
console.log('end');
}
}
}
Index 사용하는 방법
Index 는 Object Store 를 참조하는 Object Store 이다. ObjectStore.createIndex(indexName, keyPath, objectParameters})
함수로 로 만들 수 있다. Object Store 에 Index 와 관련된 레코드가 업데이트 되면 자동으로 업데이트 된다.
indexName
: 인덱스의 이름.
KeyPath
: 사용할 인덱스의 키 경로.
Object Parmeters
: 인덱스의 속정을 지정할 수 있다. 지정할 수 있는 속성으로는 unique, multiEntry, local 이 있다. (속성 사용하는 법은 여기로)
dbReq.onupgradeneeded = function(event){
console.log('upgradeneeded');
db = event.target.result;
db.createObjectStore('topics', {keyPath:'id', autoIncrement:true}).createIndex('id','id');
}
IndexedDB 예제
<!DOCTYPE html>
<html>
<body>
<script>
let dbReq = indexedDB.open('opentutorials',1);
let db;
if (!window.indexedDB) {
window.alert("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.")
} else {
dbReq.onsuccess = function(event){
console.log('success');
db = event.target.result;
}
dbReq.onerror = function(event){
const error = event.target.error;
console.log('error', error.name);
}
dbReq.onupgradeneeded = function(event){
console.log('upgradeneeded');
db = event.target.result;
db.createObjectStore('topics', {keyPath:'id', autoIncrement:true}).createIndex('id','id');
}
}
// 데이터 추가
function addData(){
let dbReq = indexedDB.open('opentutorials',1);
dbReq.onsuccess = function(event){
const transaction = db.transaction('topics', 'readwrite');
const objectStore = transaction.objectStore('topics');
const addReq = objectStore.add({
title : prompt('title?'),
body : prompt('body?')
});
addReq.onsuccess = function(event){
console.log(event);
}
addReq.onerror = function(event){
console.log('error', error.name);
}
}
}
// 데이터 검색
function getData(){
let dbReq = indexedDB.open('opentutorials',1);
dbReq.onsuccess = function(event){
const id = Number(prompt('?id'));
const transaction = db.transaction('topics', 'readonly');
const objectStore = transaction.objectStore('topics');
const getReq = objectStore.get(id);
getReq.onsuccess = function(event){
console.log(event.target.result);
}
getReq.onerror = function(event){
console.log('error', error.name);
}
}
}
// 데이터 전체 검색
function getDataList(){
let dbReq = indexedDB.open('opentutorials',1);
dbReq.onsuccess = function(event){
const transaction = db.transaction('topics', 'readonly');
const objectStore = transaction.objectStore('topics');
const mCursor = objectStore.openCursor();
mCursor.onsuccess = function(event){
var cursor = event.target.result;
if(cursor){
console.log(cursor.value);
cursor.continue();
}else{
console.log('end');
}
}
// const getAllReq = objectStore.getAll();
// getAllReq.onsuccess = function(event){
// console.log(event.target.result);
// }
// getAllReq.onerror = function(event){
// console.log('error', error.name);
// }
}
}
// 데이터 수정
function updateData(){
let dbReq = indexedDB.open('opentutorials',1);
dbReq.onsuccess = function(event){
const transaction = db.transaction('topics', 'readwrite');
const objectStore = transaction.objectStore('topics');
const putReq = objectStore.put({
id:Number(prompt('id?')),
title:prompt('title?'),
body:prompt('body?')
});
putReq.onsuccess = function(event){
console.log(event);
}
putReq.onerror = function(event){
console.log('error', error.name);
}
}
}
// 데이터 삭제
function deleteData(){
let dbReq = indexedDB.open('opentutorials',1);
dbReq.onsuccess = function(event){
const transaction = db.transaction('topics', 'readwrite');
const objectStore = transaction.objectStore('topics');
const deleteReq = objectStore.delete(Number(prompt('id?')));
deleteReq.onsuccess = function(event){
console.log(event);
}
deleteReq.onerror = function(event){
console.log('error', error.name);
}
}
}
</script>
<input type="button" value="add" onclick="addData()">
<input type="button" value="get" onclick="getData()">
<input type="button" value="list" onclick="getDataList()">
<input type="button" value="update" onclick="updateData()">
<input type="button" value="delete" onclick="deleteData()">
</body>
</html>
앵귤러 맞춤 라이브러리
ngx-indexed-db
설치하기
npm install ngx-indexed-db
yarn add ngx-indexed-db
준비하기
모듈 가져오기 및 시작
import { NgxIndexedDBModule } from 'ngx-indexed-db';
const dbConfig: DBConfig = {
name: 'MyDb',
version: 1,
objectStoresMeta: [{
store: 'people',
storeConfig: { keyPath: 'id', autoIncrement: true },
storeSchema: [
{ name: 'name', keypath: 'name', options: { unique: false } },
{ name: 'email', keypath: 'email', options: { unique: false } }
]
}]
};
@NgModule({
...
imports: [
...
NgxIndexedDBModule.forRoot(dbConfig)
],
...
})
메인 모듈에서 NgxIndexedDBModule을 가져온다. IndexedDB의 데이터베이스 이름, 버전 정보, 오브젝트 스토어 정보를 정의하고 이를 다른 모듈에서 중복 사용하지 않도록 forRoot 옵션을 한다.
마이그레이션
import { NgxIndexedDBModule, DBConfig } from 'ngx-indexed-db';
// 미리 컴파일을 하려면 팩토리에 내보낸 함수가 필요합니다.
export function migrationFactory() {
// animals 테이블이 버전 2로 추가되었지만 기존 테이블 또는 데이터가 필요하지 않습니다.
// 해당 버전의 마이그레이터가 포함되지 않도록 수정해야 합니다.
return {
1: (db, transaction) => {
const store = transaction.objectStore('people');
store.createIndex('country', 'country', { unique: false });
},
3: (db, transaction) => {
const store = transaction.objectStore('people');
store.createIndex('age', 'age', { unique: false });
}
};
}
const dbConfig: DBConfig = {
name: 'MyDb',
version: 3,
objectStoresMeta: [{
store: 'people',
storeConfig: { keyPath: 'id', autoIncrement: true },
storeSchema: [
{ name: 'name', keypath: 'name', options: { unique: false } },
{ name: 'email', keypath: 'email', options: { unique: false } }
]
}, {
// 버전 2에 추가된 animals
store: 'animals',
storeConfig: { keyPath: 'id', autoIncrement: true },
storeSchema: [
{ name: 'name', keypath: 'name', options: { unique: true } },
]
}],
// 마이그레이션 팩토리를 DBConfig로 제공
migrationFactory
};
@NgModule({
...
imports: [
...
NgxIndexedDBModule.forRoot(dbConfig)
],
...
})
마이그레이션이란?
마이그레이션이란 대량의 데이터를 옮기는 프로세스를 말한다. 마이그레이션은 효율성이나 보안을 위해 비즈니스 기술을 업그레이드 할 때 사용한다.
서비스 주입
import { NgxIndexedDBService } from 'ngx-indexed-db';
...
export class AppComponent {
constructor(private dbService: NgxIndexedDBService){
}
}
API 사용법
Object Store 생성
createObjectStore(storeSchema: ObjectStoreMeta, migrationFactory?: () => { [key: number]: (db: IDBDatabase, transaction: IDBTransaction) => void }): void
const storeSchema: ObjectStoreMeta = {
store: 'people',
storeConfig: { keyPath: 'id', autoIncrement: true },
storeSchema: [
{ name: 'name', keypath: 'name', options: { unique: false } },
{ name: 'email', keypath: 'email', options: { unique: false } },
],
};
this.dbService.createObjectStore(storeSchema);
Object Store 생성시에 필요한 정보를 모두 담아 createObjectStore()
에 한번에 담아서 Object Store를 생성한다.
데이터 추가
add(storeName: string, value: T, key?: any): Observable
this.dbService
.add('people', {
name: `Bruce Wayne`,
email: `bruce@wayne.com`,
})
.subscribe((key) => {
console.log('key: ', key);
});
add()
에 추가할 Object Store 이름과 Object 데이터를 담아서 추가한다.
데이터 검색
getByKey(storeName: string, key: IDBValidKey): Observable
this.dbService.getByKey('people', 1).subscribe((people) => {
console.log(people);
});
getByKey()
로 Obejct Store 이름과 key 정보를 가지고 데이터를 검색할 수 있다.
데이터 수정
update(storeName: string, value: T): Observable<T[]>/p>
this.dbService
.update('people', {
id: 1,
email: 'luke@skywalker.com',
name: 'Luke Skywalker',
})
.subscribe((storeData) => {
console.log('storeData: ', storeData);
});
update()
로 Object Store 이름과 Key 정보, 수정할 데이터 Object를 담아 수정할 수 있다.
데이터 삭제
deleteByKey(storeName: string, key: Key): Observable
this.dbService.deleteByKey('people', 3).subscribe((status) => {
console.log('Deleted?:', status);
});
deleteByKey()
로 Object Store 이름과 Key 정보를 가지고 데이터를 삭제할 수 있다.