JavaScript 및 IndexedDB
이 글에서는 JavaScript와 IndexedDB에 대해 설명합니다.
이 튜토리얼에서는 JavaScript와 IndexedDB
에 대해 단계별로 설명하며, 각 단계마다 실용적인 샘플 코드를 제공하여 이해를 돕습니다.
YouTube Video
javascript-indexed-db.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <title>JavaScript & HTML</title>
6 <style>
7 * {
8 box-sizing: border-box;
9 }
10
11 body {
12 margin: 0;
13 padding: 1em;
14 padding-bottom: 10em;
15 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16 background-color: #f7f9fc;
17 color: #333;
18 line-height: 1.6;
19 }
20
21 .container {
22 max-width: 800px;
23 margin: 0 auto;
24 padding: 1em;
25 background-color: #ffffff;
26 border: 1px solid #ccc;
27 border-radius: 10px;
28 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
29 }
30
31 .container-flex {
32 display: flex;
33 flex-wrap: wrap;
34 gap: 2em;
35 max-width: 1000px;
36 margin: 0 auto;
37 padding: 1em;
38 background-color: #ffffff;
39 border: 1px solid #ccc;
40 border-radius: 10px;
41 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
42 }
43
44 .left-column, .right-column {
45 flex: 1 1 200px;
46 min-width: 200px;
47 }
48
49 h1, h2 {
50 font-size: 1.2rem;
51 color: #007bff;
52 margin-top: 0.5em;
53 margin-bottom: 0.5em;
54 border-left: 5px solid #007bff;
55 padding-left: 0.6em;
56 background-color: #e9f2ff;
57 }
58
59 button {
60 display: block;
61 margin: 1em auto;
62 padding: 0.75em 1.5em;
63 font-size: 1rem;
64 background-color: #007bff;
65 color: white;
66 border: none;
67 border-radius: 6px;
68 cursor: pointer;
69 transition: background-color 0.3s ease;
70 }
71
72 button:hover {
73 background-color: #0056b3;
74 }
75
76 #output {
77 margin-top: 1em;
78 background-color: #1e1e1e;
79 color: #0f0;
80 padding: 1em;
81 border-radius: 8px;
82 min-height: 200px;
83 font-family: Consolas, monospace;
84 font-size: 0.95rem;
85 overflow-y: auto;
86 white-space: pre-wrap;
87 }
88
89 .highlight {
90 outline: 3px solid #ffc107; /* yellow border */
91 background-color: #fff8e1; /* soft yellow background */
92 transition: background-color 0.3s ease, outline 0.3s ease;
93 }
94
95 .active {
96 background-color: #28a745; /* green background */
97 color: #fff;
98 box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
99 transition: background-color 0.3s ease, box-shadow 0.3s ease;
100 }
101 </style>
102</head>
103<body>
104 <div class="container">
105 <h1>JavaScript Console</h1>
106 <button id="executeBtn">Execute</button>
107 <div id="output"></div>
108 </div>
109
110 <script>
111 // Override console.log to display messages in the #output element
112 (function () {
113 // Override console.log
114 const originalLog = console.log;
115 console.log = function (...args) {
116 originalLog.apply(console, args);
117 const message = document.createElement('div');
118 message.textContent = args
119 .map(arg => (typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg)))
120 .join(" ");
121 output.appendChild(message);
122 };
123
124 // Override console.error
125 const originalError = console.error;
126 console.error = function (...args) {
127 originalError.apply(console, args);
128 const message = document.createElement('div');
129 message.textContent = args
130 .map(arg => (typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg)))
131 .join(" ");
132 message.style.color = 'red'; // Color error messages red
133 output.appendChild(message);
134 };
135 })();
136
137 document.getElementById('executeBtn').addEventListener('click', () => {
138 // Prevent multiple loads
139 if (document.getElementById('externalScript')) return;
140
141 const script = document.createElement('script');
142 script.src = 'javascript-indexed-db.js';
143 script.id = 'externalScript';
144 //script.onload = () => console.log('javascript-indexed-db.js loaded and executed.');
145 //script.onerror = () => console.log('Failed to load javascript-indexed-db.js.');
146 document.body.appendChild(script);
147 });
148 </script>
149</body>
150</html>
JavaScript 및 IndexedDB
IndexedDB
는 브라우저에 내장된 비동기식 키-값 저장소 데이터베이스입니다. 관계형 데이터베이스와 유사한 기능을 제공하며, 클라이언트 측에 대용량의 구조화된 데이터를 저장하고 검색할 수 있습니다. 특히 오프라인 기능이 있는 애플리케이션이나 PWA(Progressive Web Apps)에 유용합니다.
IndexedDB
의 특징
- 비동기적이며 이벤트 기반으로 동작합니다.
- JavaScript 객체를 오브젝트 저장소에 저장할 수 있습니다.
- 쿼리나 인덱스를 통한 검색이 가능합니다.
- 저장 용량이 매우 크며(수백 MB 이상), 쿠키나 localStorage보다 훨씬 많은 데이터를 저장할 수 있습니다.
데이터베이스 열기 및 생성하기
IndexedDB
를 사용하려면 먼저 데이터베이스를 열어야 합니다. 존재하지 않는 경우 자동으로 생성됩니다.
1const request = indexedDB.open('MyDatabase', 1); // Specify DB name and version
open
메서드는 데이터베이스를 비동기적으로 열며 다음 세 가지 이벤트를 발생시킵니다.
onsuccess
1// Fired when database is successfully opened
2request.onsuccess = (event) => {
3 const db = event.target.result; // Database instance
4 console.log('Database opened successfully:', db.name);
5};
onsuccess
이벤트는 데이터베이스가 성공적으로 열릴 때 발생합니다. 이 시점부터request.result
를 통해 후속 작업을 수행할 수 있습니다.
onerror
1// Fired when database fails to open
2request.onerror = (event) => {
3 console.error('Failed to open database:', event.target.error);
4};
onerror
이벤트는 데이터베이스를 여는 데 실패할 때 발생합니다. 여기서 오류 로그를 기록하고 처리해야 합니다.
onupgradeneeded
1// Fired when database is newly created or upgraded
2request.onupgradeneeded = (event) => {
3 const db = event.target.result;
4 console.log('Database upgrade needed (or newly created):', db.name);
5
6 // Example: Create an object store (like a table) if it doesn’t exist
7 if (!db.objectStoreNames.contains('users')) {
8 db.createObjectStore('users', { keyPath: 'id' });
9 console.log('Object store "users" created');
10 }
11};
onupgradeneeded
이벤트는 데이터베이스가 새로 생성되거나 지정된 버전이 기존 버전보다 높을 때 발생합니다. 테이블(오브젝트 저장소) 생성 및 스키마 정의는 이 시점에서 해야 합니다.
오브젝트 저장소 생성하기
먼저 onupgradeneeded
이벤트 내에서 '오브젝트 저장소'(테이블에 해당)를 생성합니다.
1request.onupgradeneeded = function (event) {
2 const db = event.target.result;
3 console.log("onupgradeneeded triggered. Database version:", db.version);
4 if (!db.objectStoreNames.contains('users')) {
5 console.log("Creating object store: users");
6 const store = db.createObjectStore('users', { keyPath: 'id' });
7 store.createIndex('name', 'name', { unique: false });
8 }
9};
여기에서는 다음과 같은 설정을 적용합니다:.
-
createObjectStore
createObjectStore
는 데이터베이스 내에서 오브젝트 저장소를 생성하는 메서드입니다. 오브젝트 저장소에 레코드를 저장할 수 있으며,keyPath
등 다양한 옵션으로 데이터 관리 방식을 정의할 수 있습니다. 이 작업은 반드시onupgradeneeded
이벤트 내에서 이루어져야 합니다. -
keyPath: 'id'
keyPath
를id
속성으로 설정하여 각 레코드를 고유하게 식별하도록 기본 키로 지정합니다. 이렇게 하면 데이터를 추가, 검색, 수정할 때 자동으로id
가 사용됩니다. -
createIndex
createIndex
메서드를 사용하여name
속성을 기반으로 한 인덱스를 생성해 검색에 활용할 수 있습니다.unique: false
로 설정하면 동일한name
값을 가진 여러 레코드를 허용할 수 있습니다.
데이터베이스 연결 성공 시 처리
데이터베이스 연결이 성공했을 때 실행할 처리는 onsuccess
이벤트에 지정합니다. 이 과정에서 데이터베이스 인스턴스를 얻어 데이터 읽기 및 쓰기를 준비합니다.
1request.onsuccess = function (event) {
2 const db = event.target.result;
3 console.log('Database opened successfully');
4 // Use db for reading and writing in subsequent operations
5};
-
이 처리는
IndexedDB
데이터베이스 연결이 정상적으로 완료될 때 실행됩니다. -
**
event.target.result
**에는 열린 데이터베이스 인스턴스(IDBDatabase
객체)가 저장되며, 트랜잭션 시작 및 오브젝트 저장소 접근에 사용합니다. -
데이터 추가, 검색, 수정, 삭제 등 실제 읽기/쓰기 작업은
db
객체를 통해 수행합니다.
이 시점에는 데이터베이스가 준비되었으므로 트랜잭션을 안전하게 시작할 수 있습니다.
데이터 추가하기
IndexedDB
에 새 데이터를 추가하는 방법입니다.
1function addUser(db, user) {
2 const transaction = db.transaction('users', 'readwrite');
3 const store = transaction.objectStore('users');
4 const request = store.add(user);
5
6 request.onsuccess = () => {
7 console.log('User added:', user);
8 };
9
10 request.onerror = () => {
11 console.error('Add failed:', request.error);
12 };
13}
14
15// Example: Add a user
16request.onsuccess = function (event) {
17 const db = event.target.result;
18 addUser(db, { id: 1, name: 'Alice' });
19 addUser(db, { id: 2, name: 'Bob' });
20 addUser(db, { id: 3, name: 'John' });
21};
db.transaction()
으로 트랜잭션을 생성하고, 동작할 오브젝트 저장소와 모드(여기서는readwrite
)를 지정합니다.store.add()
메서드로 새 데이터를 추가합니다.- 트랜잭션은 자동으로 커밋되지만, 여러 작업을 그룹으로 처리하고 싶을 때는 트랜잭션 종료 이벤트를 활용해 관리할 수 있습니다.
데이터 조회하기(기본 키 검색)
기본 키로 특정 데이터를 조회하는 방법입니다.
1function getUserById(db, id) {
2 const transaction = db.transaction('users', 'readonly');
3 const store = transaction.objectStore('users');
4 const request = store.get(id);
5
6 request.onsuccess = () => {
7 if (request.result) {
8 console.log('User found:', request.result);
9 } else {
10 console.log('User not found');
11 }
12 };
13
14 request.onerror = () => {
15 console.error('Error retrieving user:', request.error);
16 };
17}
18
19// Example: Get a user by id
20request.onsuccess = function (event) {
21 const db = event.target.result;
22 getUserById(db, 1);
23};
db.transaction()
으로 읽기 전용 트랜잭션을 생성합니다.store.get(id)
를 사용해 지정한 기본 키에 해당하는 데이터를 조회할 수 있습니다.- 조회에 성공하면
onsuccess
가 호출되고, 결과가 있으면 출력됩니다. 결과가 없는 경우 '해당 데이터 없음'으로 처리합니다.
인덱스 검색 활용하기
기본 키 이외의 속성으로 검색하려면, 미리 생성한 인덱스를 사용하세요.
1function getUserByName(db, name) {
2 const transaction = db.transaction('users', 'readonly');
3 const store = transaction.objectStore('users');
4 const index = store.index('name');
5 const request = index.get(name);
6
7 request.onsuccess = () => {
8 if (request.result) {
9 console.log('User by name:', request.result);
10 } else {
11 console.log('No user found with that name');
12 }
13 };
14
15 request.onerror = () => {
16 console.error('Error retrieving user by name:', request.error);
17 };
18}
19
20// Example: Get a user by id
21request.onsuccess = function (event) {
22 const db = event.target.result;
23 getUserByName(db, 'Alice');
24};
store.index('name')
을 사용해 미리 생성된name
인덱스에 접근합니다.index.get(value)
로 해당 값과 일치하는 첫 레코드를 조회할 수 있습니다. 동일한 값을 가진 레코드가 여러 개인 경우index.getAll(value)
로 모두 조회할 수 있습니다.
데이터 수정하기
기존 데이터를 수정(덮어쓰기)하려면 put()
메서드를 사용하세요.
일치하는 기본 키가 있으면 데이터가 갱신되고, 없으면 새 레코드가 추가됩니다.
1function updateUser(db, updatedUser) {
2 const transaction = db.transaction('users', 'readwrite');
3 const store = transaction.objectStore('users');
4 const request = store.put(updatedUser);
5
6 request.onsuccess = () => {
7 console.log('User updated:', updatedUser);
8 };
9
10 request.onerror = () => {
11 console.error('Update failed:', request.error);
12 };
13}
14
15// Example: Update user
16request.onsuccess = async (event) => {
17 const db = event.target.result;
18
19 // Test : update existing user
20 updateUser(db, { id: 3, name: 'John Updated' });
21
22 // Test : insert new user
23 updateUser(db, { id: 4, name: 'Charlie' });
24};
put()
은 갱신과 추가를 모두 지원하는 편리한 메서드입니다.- 동일한 기본 키의 데이터가 이미 존재하면 덮어써집니다.
- 갱신 전에 데이터 존재 유무를 확인하려면
get()
을 사용해 미리 확인할 수 있습니다.
데이터 삭제하기
지정한 기본 키에 해당하는 데이터를 삭제하려면 delete()
메서드를 사용하세요.
1function deleteUser(db, id) {
2 const transaction = db.transaction('users', 'readwrite');
3 const store = transaction.objectStore('users');
4 const request = store.delete(id);
5
6 request.onsuccess = () => {
7 console.log(`User with id=${id} deleted successfully`);
8 };
9
10 request.onerror = () => {
11 console.error(`Failed to delete user with id=${id}:`, request.error);
12 };
13}
14
15// Example: Delete a user by id
16request.onsuccess = function (event) {
17 const db = event.target.result;
18 deleteUser(db, 4);
19};
store.delete(id)
로 해당 기본 키의 데이터를 삭제합니다.- 데이터가 존재하지 않더라도 오류가 발생하지 않으며 성공으로 처리됩니다.
- 오류 처리를 구현하면 더 견고한 코드를 만들 수 있습니다.
모든 데이터 조회하기
getAll()
오브젝트 저장소의 모든 레코드를 조회하려면 getAll()
메서드를 사용하세요.
1function getAllUsers(db) {
2 const transaction = db.transaction('users', 'readonly');
3 const store = transaction.objectStore('users');
4 const request = store.getAll();
5
6 request.onsuccess = () => {
7 console.log('All users:', request.result);
8 };
9
10 request.onerror = () => {
11 console.error('Failed to retrieve users:', request.error);
12 };
13}
14
15// Example: Get all users
16request.onsuccess = function (event) {
17 const db = event.target.result;
18 getAllUsers(db);
19};
getAll()
로 지정된 오브젝트 저장소의 모든 레코드를 배열로 조회할 수 있습니다.- 대량의 데이터를 한 번에 조회할 때도 효율적으로 처리됩니다.
- 결과는
request.result
에 배열로 저장됩니다. - 실패에 대비해 항상 오류 처리를 추가하세요.
openCursor()
openCursor()
는 오브젝트 저장소나 인덱스의 레코드를 순차적으로 탐색하는 메서드입니다. 대량 데이터를 한 번에 조회하지 않고 하나씩 처리하고 싶을 때 유용합니다.
1function getAllUsersWithCursor(db) {
2 const transaction = db.transaction('users', 'readonly');
3 const store = transaction.objectStore('users');
4 const request = store.openCursor();
5
6 request.onsuccess = () => {
7 const cursor = request.result;
8 if (cursor) {
9 console.log('User:', cursor.value); // Process the current record
10 cursor.continue(); // Move to the next record
11 } else {
12 console.log('All users have been processed.');
13 }
14 };
15
16 request.onerror = () => {
17 console.error('Failed to open cursor:', request.error);
18 };
19}
20
21// Example: Get all users
22request.onsuccess = function (event) {
23 const db = event.target.result;
24 getAllUsersWithCursor(db);
25};
- **
openCursor()
**를 사용하면 커서를 시작하여 오브젝트 저장소의 모든 레코드를 하나씩 순회할 수 있습니다. - **
cursor.value
**를 사용해 현재 레코드의 데이터 객체를 얻습니다. - **
cursor.continue()
**로 커서를 다음 레코드로 이동시킬 수 있습니다. - **
cursor === null
**이 되면 모든 레코드를 순회한 것입니다.
openCursor()를 사용한 갱신 프로세스 예시
예를 들어, 이름이 'Alice'인 사용자의 이름을 'Alicia'로 변경하는 과정은 다음과 같습니다:.
1function updateUserName(db, oldName, newName) {
2 const transaction = db.transaction('users', 'readwrite');
3 const store = transaction.objectStore('users');
4 const index = store.index('name'); // Use the 'name' index
5 const request = index.openCursor(IDBKeyRange.only(oldName));
6
7 request.onsuccess = () => {
8 const cursor = request.result;
9 if (cursor) {
10 const user = cursor.value;
11 user.name = newName; // Update the name
12 const updateRequest = cursor.update(user);
13
14 updateRequest.onsuccess = () => {
15 console.log('Updated user:', user);
16 };
17
18 updateRequest.onerror = () => {
19 console.error('Failed to update user:', updateRequest.error);
20 };
21
22 cursor.continue();
23 } else {
24 console.log('All matching users have been updated.');
25 }
26 };
27
28 request.onerror = () => {
29 console.error('Cursor error:', request.error);
30 };
31}
32
33// Example: Update user name
34request.onsuccess = function (event) {
35 const db = event.target.result;
36 updateUserName(db, 'Alice', 'Alicia');
37};
-
IDBKeyRange.only(oldName)
IDBKeyRange.only
을 사용하면 키가 정확히oldName
과 일치하는 레코드만을 대상으로 할 수 있습니다. 특정 값을 직접 접근하고자 할 때 유용합니다. -
cursor.update()
cursor.value를 갱신한 후, update()를 호출하면 해당 레코드가 덮어써집니다. -
여러 일치 항목 처리
cursor.continue()
를 호출하면 커서를 다음 일치 레코드로 이동할 수 있습니다. 이렇게 하면 동일한 키 또는 조건에 일치하는 여러 레코드를 차례로 처리할 수 있습니다. -
오류 처리 처리에 실패했을 때 onerror에서 로그를 출력하면, 원인 조사와 운영 중의 문제 해결이 용이해집니다.
openCursor()를 사용한 삭제 프로세스 예시
예를 들어, 이름이 'Bob'인 모든 사용자를 삭제하는 과정은 다음과 같습니다:.
1function deleteUsersByName(db, targetName) {
2 const transaction = db.transaction('users', 'readwrite');
3 const store = transaction.objectStore('users');
4 const index = store.index('name');
5 const request = index.openCursor(IDBKeyRange.only(targetName));
6
7 request.onsuccess = () => {
8 const cursor = request.result;
9 if (cursor) {
10 const deleteRequest = cursor.delete();
11
12 deleteRequest.onsuccess = () => {
13 console.log('Deleted user:', cursor.value);
14 };
15
16 deleteRequest.onerror = () => {
17 console.error('Failed to delete user:', deleteRequest.error);
18 };
19
20 cursor.continue();
21 } else {
22 console.log('All matching users have been deleted.');
23 }
24 };
25
26 request.onerror = () => {
27 console.error('Cursor error:', request.error);
28 };
29}
30
31// Example: Delete user by name
32request.onsuccess = function (event) {
33 const db = event.target.result;
34 deleteUsersByName(db, 'Bob');
35};
cursor.delete()
cursor.delete()를 사용하면 현재 커서 위치의 레코드가 삭제됩니다. 결과가 비동기적으로 반환되기 때문에 onsuccess에서 과정을 확인할 수 있습니다.
트랜잭션 및 비동기 처리에 관한 주의사항
IndexedDB
는 비동기적이고 이벤트 기반으로 동작합니다. 모든 작업은 onsuccess나 onerror 이벤트로 처리해야 합니다. 여러 처리를 하나로 묶고 싶을 때는 Promise로 감싸는 것이 편리합니다.
1function openDatabase() {
2 return new Promise((resolve, reject) => {
3 const request = indexedDB.open('MyDatabase', 1);
4
5 request.onupgradeneeded = function (event) {
6 const db = event.target.result;
7
8 // Initialization process (creating object stores and indexes, etc.)
9 const store = db.createObjectStore('users', { keyPath: 'id' });
10 store.createIndex('name', 'name', { unique: false });
11 };
12
13 request.onsuccess = function (event) {
14 const db = event.target.result;
15 resolve(db);
16 };
17
18 request.onerror = function () {
19 reject(request.error);
20 };
21 });
22}
23
24function addUserAsync(db, user) {
25 return new Promise((resolve, reject) => {
26 const transaction = db.transaction('users', 'readwrite');
27 const store = transaction.objectStore('users');
28 const request = store.add(user);
29
30 request.onsuccess = () => resolve();
31 request.onerror = () => reject(request.error);
32 });
33}
34
35async function main() {
36 try {
37 const db = await openDatabase();
38 await addUserAsync(db, { id: 1, name: 'Alice' });
39 console.log('User added successfully');
40 } catch (error) {
41 console.error('Error:', error);
42 }
43}
openDatabase
나addUserAsync
와 같은 함수를 Promise로 감싸면 async/await로 비동기 처리를 직관적으로 다룰 수 있습니다.- 이렇게 하면 콜백 지옥을 피하고 코드의 가독성이 좋아집니다.
요약
IndexedDB
는 브라우저 측에서 고급 데이터 관리를 하고자 할 때 매우 강력한 기능입니다. 처음에는 이벤트 기반 비동기 처리에 혼란을 느낄 수 있지만, 구조를 이해하면 클라이언트 측에서 본격적인 데이터 조작이 가능합니다.
특히 다음과 같은 점을 기억해 두면 더욱 원활하게 사용할 수 있습니다:.
onupgradeneeded
로 초기 설정을 진행하세요.- 트랜잭션의 읽기/쓰기 모드에 유의하세요.
- 인덱스를 사용하면 효율적으로 검색할 수 있습니다.
- 데이터를 객체 형태로 저장할 수 있어 JSON과의 호환성이 높습니다.
IndexedDB
를 마스터하면 PWA와 오프라인 앱의 데이터 관리가 훨씬 쉬워집니다.
위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.