로컬 스토리지, 세션 스토리지 및 쿠키는 모두 최신 브라우저에서 사용할 수 있는 웹 스토리지 메커니즘이지만 목적, 수명 및 강점이 다릅니다. 다음은 이 세 가지 스토리지 옵션을 비교한 것입니다.
- 세 가지 옵션 모두 서버 측이 아닌 클라이언트 측(사용자 브라우저)에 데이터를 저장합니다.
- JavaScript를 통해 액세스할 수 있으며 페이지 로드 또는 세션 간에 데이터를 저장하는 데 사용할 수 있습니다.
- 저장할 수 있는 데이터의 양(일반적으로 몇 메가바이트)에 제한이 있습니다.
- 수명 및 지속성:
- 쿠키: 쿠키에는 만료일이 있으며 영구적(브라우저가 닫힌 후에도 유지됨) 또는 세션 기반(브라우저 세션이 종료되면 삭제됨)일 수 있습니다.
- 로컬 저장소: 로컬 저장소에 저장된 데이터는 브라우저를 닫은 후에도 지속되며 후속 세션에서 검색할 수 있습니다.
- 세션 저장소: 세션 저장소는 로컬 저장소와 유사하지만 데이터는 현재 브라우징 세션 동안만 사용할 수 있습니다. 세션이 종료되면 데이터가 지워집니다.
- 범위 및 접근성:
- 쿠키: 쿠키는 주로 서버와의 통신에 사용됩니다. 이들은 자신이 속한 도메인에 대한 모든 HTTP 요청과 함께 전송되며 서버 및 클라이언트 측 JavaScript 모두에서 액세스할 수 있습니다.
- 로컬 저장소: 로컬 저장소는 도메인에 따라 다르며 해당 도메인 내의 모든 페이지에서 클라이언트 측 JavaScript로 액세스할 수 있습니다.
- 세션 저장소: 로컬 저장소와 마찬가지로 세션 저장소도 도메인에 따라 다릅니다. 그러나 데이터는 현재 세션으로 제한되며 여러 브라우저 탭이나 창에서 공유되지 않습니다.
- 쿠키:
- 강점: 쿠키는 광범위하게 지원되고 다양한 브라우저 세션에서 작동하며 서버측 및 클라이언트측 코드 모두에서 액세스할 수 있습니다.
- 약점: 쿠키는 작은 저장 용량(일반적으로 몇 킬로바이트)을 가지며 모든 HTTP 요청과 함께 쿠키를 전송하면 성능에 영향을 미칠 수 있습니다. 또한 XSS(교차 사이트 스크립팅) 공격과 같은 보안 위험에 취약할 수 있습니다.
- 로컬 저장소:
- 장점: 로컬 저장소는 쿠키에 비해 더 큰 저장 용량(수 메가바이트)을 제공합니다. 사용하기 쉽고 클라이언트 측 JavaScript에서 효율적으로 액세스할 수 있습니다.
- 단점: 로컬 저장소가 특정 도메인으로 제한되며 쿠키와 같이 HTTP 요청마다 데이터가 자동으로 전송되지 않습니다. 서버 측 코드에 액세스할 수 없으므로 서버 측 상호 작용이 필요한 시나리오에는 적합하지 않을 수 있습니다.
- 세션 저장:
- 장점: 세션 저장소는 로컬 저장소와 비슷한 용량을 제공하며 동일한 세션 내에서 액세스할 수 있습니다. 현재 세션 이상으로 필요하지 않은 임시 데이터를 저장하는 방법을 제공합니다.
- 약점: 로컬 저장소와 마찬가지로 세션 저장소는 특정 도메인으로 제한되며 서버 측 코드에 액세스할 수 없습니다. 또한 다른 브라우저 탭이나 창 간에 공유되지 않습니다.
요약하면,
쿠키는 주로 서버와의 통신에 사용된다.
로컬 저장소 및 세션 저장소는 클라이언트측 데이터 저장소용으로 설계되었다.
로컬 스토리지는 세션 간에 지속되는 반면 세션 스토리지는 일시적이다.
로컬 저장소와 세션 저장소 모두 쿠키보다 더 많은 용량을 제공하지만 접근성과 서버 측 상호 작용에 제한이 있다.
저장 메커니즘의 선택은 응용 프로그램 또는 웹 사이트의 특정 요구 사항에 따라 다다.
23.12.20 추가
Web Storage 는 적은 양의 데이터를 저장하는데 유용하지만 많은 양의 구조화된 데이터에는 적합하지 않은데, 이런 상황에서 IndexedDB 를 사용할 수 있습니다.
브라우저에 내장된 데이터베이스로 거의 모든 타입의 값을 키에 저장할 수 있으며 색인을 사용하여 데이터에 대한 고성능 검색을 지원합니다.
IndexedDB 작업은 애플리케이션 블록을 방지하기 위해 모두 비동기로 이뤄집니다.
Same-Origin-Policy 를 따르며 온라인 오프라인 환경 모두에서 쿼리를 지원합니다.
src/asset/util/indexedDB.ts
protected getIsSupportIndexedDB() {
return 'indexedDB' in window;
}
브라우저가 indexedDB를 지원하는지 확인합니다.
protected open(dbName: string) {
return new Promise<IDBDatabase>((res, rej) => {
const validateIndexedDB = this.getIsSupportIndexedDB();
if (validateIndexedDB) {
const openRequest = indexedDB.open(dbName, INDEXED_DB_VERSION);
openRequest.onsuccess = () => {
res(openRequest.result);
};
openRequest.onerror = () => {
this.handleError?.();
rej(openRequest.error);
};
openRequest.onupgradeneeded = () => {
const db = openRequest.result;
if (!db.objectStoreNames.contains(this.objectStoreName)) {
this.createObjectStore(db);
}
};
} else {
this.handleError?.();
}
});
}
IndexedDB 패턴의 시작인 데이터베이스 접속을 요청하는 메소드 입니다.
indexedDB.open(name, version)
openRequest.onsuccess
openRequest.onupgradeneeded
onupgradeneeded
가 트리거됩니다.upgradeneeded
콜백에서 이 버전의 데이터베이스에 필요한 객체 저장소를 만들어야합니다.protected getObjectStore(mode?: IDBTransactionMode, options?: IDBTransactionOptions) {
return new Promise<IDBObjectStore>((res, rej) => {
this.validateDBOpened()
? this.dbPromise.then(db => {
if (db.objectStoreNames.contains(this.objectStoreName)) {
const transaction = db.transaction(this.objectStoreName, mode, options);
const store = transaction.objectStore(this.objectStoreName);
res(store);
}
})
: rej();
});
}
objectStoreName
이 존재하는지 확인합니다.mode
와 options
매개변수를 사용하여 IndexedDB 트랜잭션을 시작하고, 해당 트랜잭션에서 작동하는 IDBObjectStore
를 반환합니다.protected get<T>(query: string) {
return new Promise<T>((res, rej) => {
this.getObjectStore('readonly').then(store => {
const getReq = store.get(query);
getReq.onsuccess = () => res(getReq.result);
getReq.onerror = () => rej(getReq.error);
});
});
}
protected set(query: string, value) {
return new Promise((res, rej) => {
this.getObjectStore('readwrite').then(store => {
const setReq = store.put(value, query);
setReq.onsuccess = () => res(setReq.result);
setReq.onerror = () => rej(setReq.error);
});
});
}
protected remove(query: string) {
return new Promise((res, rej) => {
this.getObjectStore('readwrite').then(store => {
const deleteReq = store.delete(query);
deleteReq.onsuccess = () => res(deleteReq.result);
deleteReq.onerror = () => rej(deleteReq.error);
});
});
}
데이터베이스의 트랜잭션을 시작하여 CRUD를 비동기적으로 처리하였습니다.
src/util/applicationForm/applicationForm.class.ts
ApplicationDraftDB
클래스는 IndexedDB
를 상속하고 있습니다.class ApplicationDraftDB extends IndexedDB {
private debounceTimer: NodeJS.Timeout;
constructor(props: ApplicationDraftDBConstructor) {
const { onError, recruitmentId } = props;
super({ objectStoreName: APPLICATION_DRAFT_DB_NAME, onError });
this.init(getDraftDBName(recruitmentId));
}
}
recruitmentId
를 이름으로 데이터베이스를 오픈합니다.onError
를 바인딩 합니다.private getAnswerKey({ type, title, description }: AnswerKey) {
let key = `${type}-${title}`;
if (description) {
key += `-${description}`;
}
return key;
}
type
, title
로 조합하며 description
이 존재한다면 추가합니다.private setDebounce(query: string, value: ApplicationInputAnswer[]) {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.set(query, value);
this.setEditedAt();
}, 300);
}
setAnswer(keyReq: AnswerKey, value: ApplicationInputAnswer[]) {
const key = this.getAnswerKey(keyReq);
this.setDebounce(key, value);
}
getAnswer<T>(keyReq: AnswerKey) {
const key = this.getAnswerKey(keyReq);
return this.get<T>(key);
}
clearAnsers() {
this.clearObjectStore();
}
당장 기능 구현에는 문제가 없지만 최적화와 유지보수를 위해서 개선 사항을 정리하였습니다.
웹앱의 버전과 싱크를 맞추어 관리한다면 버전 트랙킹에 도움이 될 것 입니다.
현재 별도의 인덱스를 생성하지 않았고, 또한 외부 키 기반의 key-value 데이터 접근 방식으로 인터페이스를 구축하였습니다. 향후 코드 구조 개선과 함께 Index 및 Cursor API 를 적용한다면 데이터 검색 효율을 개선될 것 입니다.
기능이 고도화 되거나 유지보수에 어려움을 느낀다면 래퍼 라이브러리를 고려해볼 필요가 있습니다.