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

Raymond Camden
Raymond Camden

소개

IndexedDB는 클라이언트 측에 데이터를 저장하는 강력한 방법입니다. 아직 확인하지 않으셨다면 이 주제에 관한 유용한 MDN 튜토리얼을 읽어 보시기 바랍니다. 이 도움말에서는 API 및 기능에 대한 기본적인 지식이 있다고 가정합니다. IndexedDB를 사용해 본 적은 없더라도 이 도움말의 데모를 통해 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에는 세 가지 주요 측면이 있습니다. 첫 번째는 자동 완성에 사용되는 'name' 필드입니다. 사용 중지된 상태로 로드되며 나중에 JavaScript를 통해 사용 설정됩니다. 그 옆에 있는 스팬은 초기 시드 중에 사용자에게 업데이트를 제공하는 데 사용됩니다. 마지막으로 자동 완성에서 직원을 선택할 때 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>

그런 다음 다음과 같이 JavaScript에서 다시 컴파일됩니다.

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

이제 IndexedDB로 작업을 시작해 보겠습니다. 먼저 파일을 엽니다.

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

IndexedDB에 대한 연결을 열면 데이터를 읽고 쓸 수 있지만, 그러기 전에 objectStore가 있는지 확인해야 합니다. objectStore는 데이터베이스 테이블과 같습니다. 하나의 IndexedDB에는 여러 개의 objectStore가 있을 수 있으며, 각 objectStore는 관련 객체의 컬렉션을 보유합니다. 이 데모는 간단하며 'employee'라는 하나의 objectStore만 있으면 됩니다. indexedDB가 처음 열리거나 코드에서 버전을 변경하면 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를 확인하여 employee가 포함되어 있는지 확인합니다. 그렇지 않은 경우 이를 실행하도록 합니다. createIndex 호출이 중요합니다. 데이터 검색에 사용할 키 외에 어떤 메서드를 IndexedDB에 알려야 합니다. searchkey라는 키를 사용합니다. 이에 대한 설명은 조금 후에 설명해 드리겠습니다.

onungradeneeded 이벤트는 스크립트를 처음 실행할 때 자동으로 실행됩니다. 실행되거나 향후 실행에서 건너뛴 후 onsuccess 핸들러가 실행됩니다. 간단하고 보기 흉한 오류 핸들러를 정의한 다음 handleSeed를 호출합니다.

계속하기 전에 어떤 일이 일어나고 있는지 빠르게 살펴보겠습니다. 데이터베이스를 엽니다. 객체 스토어가 있는지 확인합니다. 없으면 생성합니다. 마지막으로handleSeed라는 함수를 호출합니다. 이제 데모의 데이터 시드 부분으로 집중해 보겠습니다.

Gimme Some Data!

이 문서의 도입부에서 언급했듯이 이 데모는 알려진 모든 직원의 사본을 저장해야 하는 인트라넷 스타일의 애플리케이션을 다시 만드는 것입니다. 일반적으로 직원 수를 반환하고 레코드 일괄 항목을 검색하는 방법을 제공할 수 있는 서버 기반 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의 비동기 특성으로 인해 추가를 추적하는 간단한 변수 done을 설정했습니다. 가짜 사람을 삽입하기 위해 반복합니다. 이 함수의 소스는 다운로드에서 사용할 수 있지만 다음과 같은 객체를 반환합니다.

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

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

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

이는 클라이언트별 수정사항이므로 백엔드 서버(또는 이 경우 가상의 백엔드 서버)가 아닌 여기에서 실행됩니다.

성능이 우수한 방식으로 데이터베이스 추가를 실행하려면 모든 일괄 쓰기에 트랜잭션을 재사용해야 합니다. 쓰기마다 새 트랜잭션을 만들면 브라우저에서 각 트랜잭션에 대해 디스크 쓰기가 발생할 수 있으며, 많은 항목을 추가하면 성능이 저하될 수 있습니다 (예: '1, 000개 객체를 작성하는 데 1분 소요' - 끔찍함).

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

자동 완성 만들기

이제 재미있는 부분입니다. jQuery UI 자동 완성 플러그인을 연결합니다. 대부분의 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를 숨깁니다. 이 div는 개별 직원을 표시하고 이전에 직원이 로드된 경우 이를 삭제하는 데 사용됩니다. 이제 검색을 시작할 수 있습니다.

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

입력과 일치하는 항목을 찾기 위해 StackOverflow의 사용자인 퐁완 차우의 팁을 활용해 보겠습니다. 입력을 하한 경계로, 입력과 문자 z를 기반으로 하는 색인 범위를 상한 경계로 사용합니다. 또한 입력한 소문자 데이터와 일치하도록 용어를 소문자로 표시합니다.

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

이만으로도 IndexedDB 일치의 결과를 자동 완성에 반환할 수 있습니다. 하지만 일치 항목이 선택되었을 때 일치 항목의 세부정보 보기를 표시하는 기능도 지원해야 합니다. 앞의 Handlebars 템플릿을 사용하는 자동 완성을 만들 때 선택 핸들러를 지정했습니다.