IndexedDB를 사용한 데이터 결합 UI 요소

레이먼드 캠던
Raymond Camden

소개

IndexedDB는 클라이언트 측에 데이터를 저장하는 강력한 방법입니다. 아직 살펴보지 않았다면 이 주제에 관한 유용한 MDN 튜토리얼을 읽어 보세요. 이 도움말에서는 API 및 기능에 대한 기본 지식이 있다고 가정합니다. IndexedDB를 본 적이 없더라도 이 문서의 데모를 통해 어떤 작업을 수행할 수 있는지 알 수 있습니다.

이 데모는 기업을 위한 간단한 개념 증명 인트라넷 애플리케이션입니다. 이 애플리케이션을 통해 직원은 다른 직원을 검색할 수 있습니다. 더 빠르고 원활한 환경을 제공하기 위해 직원 데이터베이스가 클라이언트의 머신에 복사되고 IndexedDB를 사용하여 저장됩니다. 이 데모는 단순히 단일 직원 기록의 검색 및 표시를 자동 완성 스타일로 제공하지만, 이 데이터를 클라이언트에서 사용할 수 있게 되면 여러 다른 방식으로도 사용할 수 있다는 장점이 있습니다. 다음은 애플리케이션에서 실행해야 하는 작업의 기본 개요입니다.

  1. IndexedDB의 인스턴스를 설정하고 초기화해야 합니다. 대부분의 경우 간단하지만 Chrome과 Firefox에서 모두 작동하도록 하는 것은 약간 까다롭습니다.
  2. 데이터가 있는지 확인하고, 없으면 다운로드해야 합니다. 일반적으로 이는 AJAX 호출을 통해 이루어집니다. 이 데모를 위해 모조 데이터를 빠르게 생성할 수 있는 간단한 유틸리티 클래스를 만들었습니다. 애플리케이션은 데이터가 생성되는 시기를 인식하고 그때까지 사용자가 데이터를 사용하지 않도록 해야 합니다. 이 작업은 한 번만 하면 됩니다. 다음에 사용자가 애플리케이션을 실행할 때는 이 프로세스를 거칠 필요가 없습니다. 고급 데모는 클라이언트와 서버 간의 동기화 작업을 처리하지만, 이 데모는 UI 측면에 더 중점을 둡니다.
  3. 애플리케이션이 준비되면 jQuery UI의 자동 완성 컨트롤을 사용하여 IndexedDB와 동기화할 수 있습니다. 자동 완성 컨트롤은 기본 목록과 데이터 배열을 허용하지만 API를 통해 모든 데이터 소스를 허용합니다. 이를 사용하여 IndexedDB 데이터에 연결하는 방법을 보여줍니다.

시작하기

이 데모는 여러 부분으로 구성되어 있으므로 간단히 시작하기 위해 HTML 부분을 살펴보겠습니다.

<form>
  <p>
    <label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
    </p>
</form>

<div id="displayEmployee"></div>

많지 않죠? 이 UI에는 세 가지 중요한 측면이 있습니다. 첫 번째는 자동 완성에 사용될 '이름' 필드입니다. 로드는 사용 중지되었으며 나중에 자바스크립트를 통해 사용 설정됩니다. 옆에 있는 스팬은 초기 시드 중에 사용자에게 업데이트를 제공하는 데 사용됩니다. 마지막으로, 자동 추천에서 직원을 선택하면 ID가 displayEmployee인 div가 사용됩니다.

이제 JavaScript를 살펴보겠습니다. 여기서는 알아야 할 내용이 많으므로 단계별로 살펴보겠습니다. 전체 코드는 마지막에 제공되어 전체 코드를 확인할 수 있습니다.

먼저 IndexedDB를 지원하는 브라우저에서는 몇 가지 접두사 문제를 고려해야 합니다. 다음은 애플리케이션에 필요한 핵심 IndexedDB 구성 요소에 간단한 별칭을 제공하도록 수정된 Mozilla 문서에서 발췌한 코드입니다.

window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

다음으로, 데모 전체에서 사용할 몇 가지 전역 변수를 다음과 같습니다.

var db;
var template;

이제 jQuery document Ready 블록으로 시작해 보겠습니다.

$(document).ready(function() {
  console.log("Startup...");
  ...
});

이 데모에서는 Handlebars.js를 사용하여 직원 세부정보를 표시합니다. 이 내용은 나중에 사용할 수는 없지만 지금 템플릿을 컴파일하여 삭제할 수 있습니다. Handlebars 인식 유형으로 설정된 스크립트 블록이 있습니다. 크게 복잡하지는 않지만, 동적 HTML을 더 쉽게 표시할 수 있습니다.

<h2>, </h2>
Department: <br/>
Email: <a href='mailto:'></a>

그런 다음 아래와 같이 자바스크립트로 다시 컴파일됩니다.

//Create our template
var source = $("#employeeTemplate").html();
template = Handlebars.compile(source);

이제 IndexedDB를 사용해 보겠습니다. 먼저 그것을 엽니다.

var openRequest = indexedDB.open("employees", 1);

IndexedDB에 연결하면 데이터를 읽고 쓸 수 있는 액세스 권한이 부여되지만 그 전에 objectStore가 있는지 확인해야 합니다. objectStore는 데이터베이스 테이블과 같습니다. 하나의 IndexedDB에는 여러 objectStore가 있을 수 있으며 각 객체는 관련 객체의 모음을 보유합니다. 데모는 간단하며 'employee'라고 하는 하나의 objectStore만 있으면 됩니다. indexDB를 처음 열거나 코드에서 버전을 변경하면 onupgradeneeded 이벤트가 실행됩니다. 이를 사용하여 objectStore를 설정할 수 있습니다.

// Handle setup.
openRequest.onupgradeneeded = function(e) {

  console.log("running onupgradeneeded");
  var thisDb = e.target.result;

  // Create Employee
  if(!thisDb.objectStoreNames.contains("employee")) {
    console.log("I need to make the employee objectstore");
    var objectStore = thisDb.createObjectStore("employee", {keyPath: "id", autoIncrement: true});
    objectStore.createIndex("searchkey", "searchkey", {unique: false});
  }

};

openRequest.onsuccess = function(e) {
  db = e.target.result;

  db.onerror = function(e) {
    alert("Sorry, an unforseen error was thrown.");
    console.log("***ERROR***");
    console.dir(e.target);
  };

  handleSeed();
};

onupgradeneeded 이벤트 핸들러 블록에서 객체 저장소 배열인 objectStoreNames를 확인하여 직원이 포함되어 있는지 확인합니다. 그렇지 않은 경우에는 그렇게 하도록 만듭니다. createIndex 호출이 중요합니다. 키 외에 데이터를 검색하는 데 사용할 메서드를 IndexedDB에 알려야 합니다. 검색 키라는 것을 사용하겠습니다. 이에 대해서는 간단히 설명합니다.

스크립트를 처음 실행하면 onungradeneeded 이벤트가 자동으로 실행됩니다. 실행된 후 또는 향후 실행에서 건너뛰면 onsuccess 핸들러가 실행됩니다. 간단하고 못생긴 오류 핸들러를 정의하고 handleSeed를 호출합니다.

계속하기 전에 여기에서 어떤 일이 일어나는지 빠르게 복습해 보겠습니다. 데이터베이스를 엽니다. 객체 저장소가 있는지 확인합니다. 그렇지 않으면 Google에서 생성합니다. 마지막으로handleSeed라는 함수를 호출합니다. 이제 데모의 데이터 시드 부분을 살펴보겠습니다.

데이터 좀 주세요!

이 문서의 도입부에서 언급했듯이 이 데모는 알려진 모든 직원의 사본을 저장해야 하는 인트라넷 스타일 애플리케이션을 다시 만들고 있습니다. 일반적으로 이를 위해서는 직원 수를 반환하고 레코드 배치를 검색하는 방법을 제공할 수 있는 서버 기반 API를 만드는 작업이 포함됩니다. 시작 횟수를 지원하고 한 번에 100명을 반환하는 간단한 서비스를 상상해 보세요. 이는 사용자가 다른 작업을 하지 않는 동안 백그라운드에서 비동기식으로 실행될 수 있습니다.

데모를 위해 간단한 작업을 수행합니다. IndexedDB에 객체가 몇 개 있는지 확인합니다. 특정 수치 미만이면 가짜 사용자를 생성합니다. 그렇지 않으면 시드 부분이 완료된 것으로 간주되고 데모의 자동 완성 부분을 사용 설정할 수 있습니다. HandleSeed를 살펴보겠습니다.

function handleSeed() {
  // This is how we handle the initial data seed. Normally this would be via AJAX.

  db.transaction(["employee"], "readonly").objectStore("employee").count().onsuccess = function(e) {
    var count = e.target.result;
    if (count == 0) {
      console.log("Need to generate fake data - stand by please...");
      $("#status").text("Please stand by, loading in our initial data.");
      var done = 0;
      var employees = db.transaction(["employee"], "readwrite").objectStore("employee");
      // Generate 1k people
      for (var i = 0; i < 1000; i++) {
         var person = generateFakePerson();
         // Modify our data to add a searchable field
         person.searchkey = person.lastname.toLowerCase();
         resp = employees.add(person);
         resp.onsuccess = function(e) {
           done++;
           if (done == 1000) {
             $("#name").removeAttr("disabled");
             $("#status").text("");
             setupAutoComplete();
           } else if (done % 100 == 0) {
             $("#status").text("Approximately "+Math.floor(done/10) +"% done.");
           }
         }
      }
    } else {
      $("#name").removeAttr("disabled");
      setupAutoComplete();
    }
  };
}

첫 번째 줄은 여러 작업이 서로 연결되어 있기 때문에 약간 복잡하므로 이를 세분화해 보겠습니다.

db.transaction(["employee"], "readonly");

이렇게 하면 새 읽기 전용 트랜잭션이 생성됩니다. IndexedDB를 사용한 모든 데이터 작업에는 일종의 트랜잭션이 필요합니다.

objectStore("employee");

직원 객체 저장소를 가져옵니다.

count()

짐작할 수 있듯이 카운트를 수행하는 count API를 실행합니다.

onsuccess = function(e) {

완료되면 이 콜백을 실행합니다. 콜백 내에서 객체의 수인 결과 값을 가져올 수 있습니다. 수가 0이면 시드 프로세스를 시작합니다.

앞에서 언급한 상태 div를 사용하여 데이터를 가져오기 시작한다는 메시지를 사용자에게 표시합니다. IndexedDB의 비동기적인 특성으로 인해 추가를 추적하는 간단한 변수를 설정했습니다. 루프를 통해 가짜 사람들을 삽입합니다. 이 함수의 소스는 다운로드에서 사용할 수 있지만 다음과 같은 객체를 반환합니다.

{
  firstname: "Random Name",
  lastname: "Some Random Last Name",
  department: "One of 8 random departments",
  email: "first letter of firstname+lastname@fakecorp.com"
}

이것만으로도 사람을 정의하기에 충분합니다. 하지만 데이터를 검색할 수 있으려면 특별한 요구사항이 있습니다. IndexedDB는 대소문자를 구분하지 않는 방식으로 항목을 검색하는 방법을 제공하지 않습니다. 따라서 성 필드를 새 속성인 searchkey에 복사합니다. 기억하시겠지만 이 키는 데이터의 색인으로 만들어야 한다고 말씀드린 키입니다.

// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();

이는 클라이언트별로 수정되므로 백엔드 서버 (여기서는 가상 백엔드 서버)가 아닌 여기에서 수행됩니다.

데이터베이스 추가를 성능 기준에 맞게 수행하려면 모든 일괄 쓰기에 트랜잭션을 재사용해야 합니다. 쓰기마다 새 트랜잭션을 만들면 브라우저에서 각 트랜잭션에 대해 디스크 쓰기가 발생할 수 있으며, 이로 인해 많은 항목을 추가할 때 성능이 저하됩니다.

시드가 완성되면 애플리케이션의 다음 부분인 setupAutoComplete가 실행됩니다.

자동 완성 만들기

이제 jQuery UI Autocomplete 플러그인을 사용해 봅니다. 대부분의 jQuery UI와 마찬가지로 기본 HTML 요소로 시작하고 생성자 메서드를 호출하여 이를 개선합니다. 전체 프로세스를 setupAutoComplete이라는 함수로 추상화했습니다. 이제 코드를 살펴보겠습니다.

function setupAutoComplete() {

  //Create the autocomplete
  $("#name").autocomplete({
    source: function(request, response) {

      console.log("Going to look for "+request.term);

      $("#displayEmployee").hide();

      var transaction = db.transaction(["employee"], "readonly");
      var result = [];

      transaction.oncomplete = function(event) {
        response(result);
      };

      // TODO: Handle the error and return to it jQuery UI
      var objectStore = transaction.objectStore("employee");

      // Credit: http://stackoverflow.com/a/8961462/52160
      var range = IDBKeyRange.bound(request.term.toLowerCase(), request.term.toLowerCase() + "z");
      var index = objectStore.index("searchkey");

      index.openCursor(range).onsuccess = function(event) {
        var cursor = event.target.result;
        if(cursor) {
          result.push({
            value: cursor.value.lastname + ", " + cursor.value.firstname,
            person: cursor.value
          });
          cursor.continue();
        }
      };
    },
    minLength: 2,
    select: function(event, ui) {
      $("#displayEmployee").show().html(template(ui.item.person));
    }
  });

}

이 코드에서 가장 복잡한 부분은 소스 속성을 만드는 것입니다. jQuery UI의 자동 완성 컨트롤을 사용하면 IndexedDB 데이터까지 포함하여 가능한 모든 요구사항을 충족하도록 맞춤설정할 수 있는 소스 속성을 정의할 수 있습니다. API는 요청 (기본적으로 양식 필드에 입력된 내용) 및 응답 콜백을 제공합니다. 개발자는 결과 배열을 해당 콜백으로 다시 보내야 합니다.

가장 먼저 할 일은 displayEmployee div를 숨기는 것입니다. 이는 개별 직원을 표시하고 이전에 로드된 경우 지우는 데 사용됩니다. 이제 검색을 시작할 수 있습니다.

먼저 읽기 전용 트랜잭션, result라는 배열, 결과를 자동 완성 컨트롤에 전달하는 oncomplete 핸들러를 만듭니다.

입력과 일치하는 항목을 찾기 위해 StackOverflow 사용자 Fong-Wan Chau의 팁을 활용해 보겠습니다. 입력에 기반한 색인 범위를 하한 경계로, 입력과 문자 z를 상한 범위 경계로 사용합니다. 입력한 소문자와 일치하도록 검색어를 소문자로 표시합니다.

완료되면 커서를 열고 (데이터베이스 쿼리 실행과 유사) 결과를 반복할 수 있습니다. jQuery UI의 자동 완성 컨트롤을 사용하면 원하는 모든 유형의 데이터를 반환할 수 있지만 최소한 값 키가 필요합니다. 값을 잘 형식이 지정된 버전의 이름으로 설정합니다. 사람 전체도 반환합니다. 그 이유는 곧 알게 되실 겁니다. 먼저 자동 완성 기능이 작동하는 모습을 보여주는 스크린샷입니다. jQuery UI에는 Vader 테마를 사용하고 있습니다.

이것만으로도 Autocomplete에 IndexedDB 일치 결과를 반환하는 것으로 충분합니다. 하지만 일치 항목이 선택되면 일치 항목의 세부정보 보기도 표시하도록 지원하려고 합니다. 앞에서 Handlebars 템플릿을 사용하는 자동 완성을 만들 때 select 핸들러를 지정했습니다.