오프라인 및 고성능을 위한 Google Base 및 Google Gear 사용

'Google API로 더 나은 Ajax 애플리케이션 빌드' 시리즈의 첫 번째 자료

Dion Almaer, Pamela Fox, Google
2007년 6월

편집자 주: Google Gear API는 더 이상 제공되지 않습니다.

소개

Google Base와 Google Gear를 결합하여 오프라인에서 사용할 수 있는 애플리케이션을 만드는 방법을 보여줍니다. 이 도움말을 읽으면서 Google Base API에 좀 더 익숙해지고, Google Gear를 사용하여 사용자 환경설정과 데이터를 저장 및 액세스하는 방법을 알게 될 것입니다.

앱 이해하기

이 앱을 이해하려면 먼저 Google Base에 대해 잘 알고 있어야 합니다. Google Base는 기본적으로 제품, 리뷰, 레시피, 이벤트 등 다양한 카테고리에 걸쳐 있는 항목의 대규모 데이터베이스입니다.

각 항목에는 제목, 설명, 데이터의 원본 소스 링크 (있는 경우), 카테고리 유형에 따라 다른 추가 속성이 주석으로 추가됩니다. Google Base는 같은 카테고리에 속한 아이템이 공통된 속성 세트를 공유한다는 사실을 활용합니다. 예를 들어 모든 레시피에는 재료가 있습니다. Google Base 항목이 Google 웹 검색이나 Google 제품 검색의 검색결과에 표시되는 경우도 있습니다.

데모 앱 Base with Gear를 사용하면 Google Base에서 '초콜릿'(yum)으로 레시피를 찾아보거나 '해변에서 산책'(로맨틱한 산책)을 한 개인 광고를 찾는 등 일반적인 검색어를 저장하고 표시할 수 있습니다. 이는 'Google Base Reader'라고 생각하면 됩니다. "Google Base Reader"라고 하면 여러분이 앱을 다시 방문하거나, 15분마다 업데이트된 피드를 찾으려고 할 때 업데이트된 결과를 확인할 수 있습니다.

앱을 확장하려는 개발자는 검색결과에 새로운 검색결과가 포함되어 있을 때 사용자에게 시각적으로 알림을 보내고, 사용자가 즐겨찾는 항목 (오프라인) 및 온라인을 북마크에 추가 (별표 표시)하고, Google Base와 같은 카테고리별 속성 검색을 실행하도록 하는 등 더 많은 기능을 추가할 수 있습니다.

Google Base 데이터 API 피드 사용

Google Base는 Google Data API 프레임워크를 준수하는 Google Base 데이터 API를 사용하여 프로그래매틱 방식으로 쿼리할 수 있습니다. Google Data API 프로토콜은 웹에서 읽고 쓸 수 있는 간단한 프로토콜을 제공하며 Picasa, 스프레드시트, Blogger, 캘린더, 노트북 등 다양한 Google 제품에서 사용됩니다.

Google Data API 형식은 XML 및 Atom Publishing 프로토콜을 기반으로 하므로 대부분의 읽기/쓰기 상호작용이 XML에 있습니다.

Google Data API를 기반으로 하는 Google Base 피드의 예는 다음과 같습니다.
http://www.google.com/base/feeds/snippets/-/products?bq=digital+camera

snippets 피드 유형은 공개적으로 사용 가능한 항목의 피드를 제공합니다. -/products를 사용하면 피드를 제품 카테고리로 제한할 수 있습니다. bq= 매개변수를 사용하면 '디지털 카메라'라는 키워드를 포함하는 결과로만 피드를 추가로 제한할 수 있습니다. 이 피드를 브라우저에서 보면 일치하는 결과와 함께 <entry> 노드가 포함된 XML이 표시됩니다. 각 항목에는 일반적인 작성자, 제목, 콘텐츠, 링크 요소가 포함되지만 추가 카테고리별 속성 (예: 제품 카테고리의 경우 '가격')이 함께 제공됩니다.

브라우저의 XMLHttpRequest에 대한 교차 도메인 제한으로 인해 Google은 자바스크립트 코드에서 www.google.com의 XML 피드를 직접 읽을 수 없습니다. XML을 읽고 앱과 동일한 도메인의 위치에 다시 배포하도록 서버 측 프록시를 설정할 수 있지만 서버 측 프로그래밍을 모두 피하고 싶습니다. 다행히 대안이 있습니다.

다른 Google 데이터 API와 마찬가지로 Google Base 데이터 API에는 표준 XML 외에 JSON 출력 옵션이 있습니다. 앞서 JSON 형식으로 본 피드의 출력은 다음과 같습니다.
http://www.google.com/base/feeds/snippets/-/products?bq=digital+camera&alt=json

JSON은 계층적 중첩과 다양한 데이터 유형을 허용하는 경량 교환 형식입니다. 그러나 무엇보다도 JSON 출력은 네이티브 자바스크립트 코드 자체이므로 스크립트 도메인에서 참조하기만 하면 교차 도메인 제한을 우회하여 웹페이지에 로드할 수 있습니다.

Google Data API를 사용하면 JSON이 로드된 후 실행할 콜백 함수를 사용하여 'json-in-script' 출력을 지정할 수 있습니다. 이렇게 하면 페이지에 스크립트 태그를 동적으로 추가하고 각각에 다른 콜백 함수를 지정할 수 있으므로 JSON 출력을 더 쉽게 사용할 수 있습니다.

따라서 Base API JSON 피드를 페이지에 동적으로 로드하려면 피드 URL (altcallback 값 추가)이 있는 스크립트 태그를 만들어 페이지에 추가하는 다음 함수를 사용하면 됩니다.

function getJSON() {
  var script = document.createElement('script');

  var url = "http://www.google.com/base/feeds/snippets/-/products?bq=digital+camera";
  script.setAttribute('src', url + "&alt=json-in-script&callback=listResults");
  script.setAttribute('type', 'text/JavaScript');
  document.documentElement.firstChild.appendChild(script);
}

따라서 이제 콜백 함수 listResults가 JSON으로 전달된 유일한 매개변수로 반복되고 글머리기호 목록에 있는 각 항목의 정보를 표시할 수 있습니다.

  function listTasks(root) {
    var feed = root.feed;
    var html = [''];
    html.push('<ul>');
    for (var i = 0; i < feed.entry.length; ++i) {
      var entry = feed.entry[i];
      var title = entry.title.$t;
      var content = entry.content.$t;
      html.push('<li>', title, ' (', content, ')</li>');
    }
    html.push('</ul>');

    document.getElementById("agenda").innerHTML = html.join("");
  }

Google Gear 추가

이제 Google Data API를 통해 Google Base와 통신할 수 있는 애플리케이션이 있으므로 이 애플리케이션을 오프라인에서 실행할 수 있도록 하려고 합니다. 바로 이 지점에서 Google Gear를 사용할 수 있습니다.

오프라인으로 전환할 수 있는 애플리케이션을 작성하는 것과 관련된 다양한 아키텍처 옵션이 있습니다. 애플리케이션이 오프라인에서 어떻게 작동하는지에 관해 자문해 보는 것이 좋습니다. 예를 들어 완전히 똑같은가요? 검색과 같은 일부 기능이 사용 중지되었나요? 동기화를 어떻게 처리하나요?)

저희의 경우, 톱니바퀴가 없는 브라우저를 사용하는 사용자도 이 앱을 계속 사용할 수 있도록 하고 있으며, 플러그인을 가지고 있는 사용자는 오프라인 사용과 더 빠른 응답 UI의 이점을 누리길 원했습니다.

아키텍처는 다음과 같습니다.

  • Google은 검색어를 저장하고 이러한 쿼리의 결과를 반환하는 자바스크립트 객체를 보유하고 있습니다.
  • Google Gear가 설치된 경우 모든 정보가 로컬 데이터베이스에 저장되는 톱니바퀴 버전이 생성됩니다.
  • Google Gear가 설치되지 않은 경우 쿼리가 쿠키에 저장되고 전체 검색결과가 전혀 저장되지 않는 버전 (응답이 약간 느려짐)을 얻습니다. 결과가 너무 커서 쿠키에 저장할 수 없기 때문입니다.
이 아키텍처의 장점은 매장 전체에서 if (online) {}를 확인하지 않아도 된다는 점입니다. 대신, 애플리케이션에 하나의 Gear 검사가 적용된 다음 올바른 어댑터가 사용됩니다.


Gears 로컬 데이터베이스 사용

Gear의 구성요소 중 하나는 바로 사용할 수 있는 로컬 SQLite 데이터베이스입니다. 이전에 MySQL 또는 Oracle과 같은 서버 측 데이터베이스용 API를 사용한 적이 있다면 간단한 데이터베이스 API가 익숙할 것입니다.

로컬 데이터베이스를 사용하는 단계는 매우 간단합니다.

  • Google Gear 객체 초기화
  • 데이터베이스 팩토리 객체 가져오기 및 데이터베이스 열기
  • SQL 요청 실행 시작

이 내용을 빠르게 살펴보겠습니다.


Google Gear 객체 초기화

애플리케이션은 /gears/samples/gears_init.js의 콘텐츠를 직접 읽거나 코드를 자신의 자바스크립트 파일에 붙여넣어 읽어야 합니다. <script src="..../gears_init.js" type="text/JavaScript"></script>가 시작되면 google.gears 네임스페이스에 액세스할 수 있습니다.


데이터베이스 팩토리 객체 가져오기 및 데이터베이스 열기
var db = google.gears.factory.create('beta.database', '1.0');
db.open('testdb');

이 호출을 한 번 수행하면 데이터베이스 스키마를 열 수 있는 데이터베이스 객체가 제공됩니다. 데이터베이스를 열면 동일한 출처 정책 규칙을 통해 범위가 지정되므로 'testdb'가 'testdb'와 충돌하지 않습니다.


SQL 요청 실행 시작

이제 데이터베이스에 SQL 요청을 보낼 준비가 되었습니다. 'select' 요청을 전송하면 원하는 데이터에 대해 반복할 수 있는 결과 세트가 반환됩니다.

var rs = db.execute('select * from foo where name = ?', [ name ]);

다음 메서드를 사용하여 반환된 결과 세트에서 작업할 수 있습니다.

booleanisValidRow()
voidnext()
voidclose()
intfieldCount()
stringfieldName(int fieldIndex)
variantfield(int fieldIndex)
variantfieldByName(string fieldname)

자세한 내용은 Database Module API 문서를 참고하세요. (편집자 주: Google Gear API는 더 이상 사용할 수 없습니다.)


GearsDB를 사용하여 낮은 수준의 API 캡슐화

일반적인 데이터베이스 작업을 캡슐화하고 더 편리하게 만들고 싶었습니다. 예를 들면 다음과 같습니다.

  • 애플리케이션을 디버깅할 때 생성된 SQL을 로깅하는 좋은 방법을 원했습니다.
  • 모든 지역에서 try{}catch(){}를 처리하는 대신 한 곳에서 예외를 처리하려고 했습니다.
  • 데이터를 읽거나 쓸 때 결과 집합 대신 자바스크립트 객체를 처리하려고 했습니다.

이러한 문제를 일반적인 방식으로 처리하기 위해 Google에서는 데이터베이스 객체를 래핑하는 오픈소스 라이브러리인 GearsDB를 만들었습니다. 이제 GearDB를 사용하는 방법을 보여드리겠습니다.

초기 설정

window.onload 코드에서 사용하는 데이터베이스 테이블이 제자리에 있는지 확인해야 합니다. 다음 코드가 실행될 때 사용자가 Gear를 설치한 경우 GearsBaseContent 객체를 만듭니다.

content = hasGears() ? new GearsBaseContent() : new CookieBaseContent();

그런 다음 데이터베이스를 열고 테이블이 아직 없으면 만듭니다.

db = new GearsDB('gears-base'); // db is defined as a global for reuse later!

if (db) {
  db.run('create table if not exists BaseQueries' +
         ' (Phrase varchar(255), Itemtype varchar(100))');
  db.run('create table if not exists BaseFeeds' + 
         ' (id varchar(255), JSON text)');
}

이제 쿼리와 피드를 저장할 테이블이 있습니다. new GearsDB(name) 코드는 지정된 이름의 데이터베이스 열기를 캡슐화합니다. run 메서드는 하위 수준의 execute 메서드를 래핑하지만 콘솔에 대한 디버깅 출력과 트래핑 예외도 처리합니다.


검색어 추가

앱을 처음 실행하면 검색이 되지 않습니다. 사용자가 닌텐도 Wii를 검색하려고 하면 이 검색어가 Base쿼리 테이블에 저장됩니다.

addQuery 메서드의 Gear 버전은 입력을 받고 insertRow를 통해 저장하는 방식으로 이를 수행합니다.

var searchterm = { Phrase: phrase, Itemtype: itemtype };
db.insertRow('BaseQueries', searchterm); 

insertRow는 자바스크립트 객체 (searchterm)를 가져와서 테이블에 삽입하는 방법을 자동으로 처리합니다. 또한 제약 조건을 정의할 수도 있습니다 (예: 둘 이상의 '밥'을 고유성 블록 삽입). 그러나 대부분의 경우 이러한 제약은 데이터베이스 자체에서 처리됩니다.


모든 검색어 가져오기

이전 검색 목록을 채우려면 selectAll라는 적절한 선택 래퍼를 사용합니다.

GearsBaseContent.prototype.getQueries = function() {
  return this.db.selectAll('select * from BaseQueries');
}

그러면 데이터베이스의 행과 일치하는 자바스크립트 객체 배열이 반환됩니다 (예: [ { Phrase: 'Nintendo Wii', Itemtype: 'product' }, { ... }, ...]).

이 경우 전체 목록을 반환하는 것은 괜찮습니다. 그러나 데이터가 많다면 선택 호출에서 콜백을 사용하여 반환된 각 행에서 작업할 수 있도록 하는 것이 좋습니다.

 db.selectAll('select * from BaseQueries where Itemtype = ?', ['product'], function(row) {
  ... do something with this row ...
});

다음은 GearDB에서 유용한 다른 선택 메서드입니다.

selectOne(sql, args)첫 번째/하나의 일치하는 자바스크립트 객체 반환
selectRow(table, where, args, select)일반적으로 SQL을 무시하기 위해 간단한 경우에 사용됩니다.
selectRows(table, where, args, callback, select)selectRow와 동일하지만 여러 결과에 적용됩니다.

피드 로드

Google Base에서 결과 피드를 받으면 데이터베이스에 저장해야 합니다.

content.setFeed({ id: id, JSON: json.toJSONString() });

... which calls ...

GearsBaseContent.prototype.setFeed = function(feed) {
  this.db.forceRow('BaseFeeds', feed);
}

먼저 JSON 피드를 가져와 toJSONString 메서드를 사용하여 문자열로 반환합니다. 그런 다음 feed 객체를 만들어 forceRow 메서드에 전달합니다. forceRow는 항목이 없는 경우 항목을 삽입하거나 기존 항목을 업데이트합니다.


검색결과 표시

앱은 페이지의 오른쪽 패널에 특정 검색어에 대한 결과를 표시합니다. 검색어와 관련된 피드를 가져오는 방법은 다음과 같습니다.

GearsBaseContent.prototype.getFeed = function(url) {
  var row = this.db.selectRow('BaseFeeds', 'id = ?', [ url ]);
  return row.JSON;
}

이제 행의 JSON이 있으므로 eval()하여 객체를 다시 가져올 수 있습니다.

eval("var json = " + jsonString + ";");

본격적으로 시작하기에 앞서 JSON에서 페이지로 내부 HTML 콘텐츠를 시작할 수 있습니다.


오프라인 액세스에 Resource Store 사용

로컬 데이터베이스에서 콘텐츠를 가져오기 때문에 이 앱도 오프라인으로 작동해야 합니다.

아닙니다. 이 앱을 실행하려면 자바스크립트, CSS, HTML, 이미지 등의 웹 리소스를 로드해야 합니다. 현재 상황에서 사용자가 온라인 시작, 일부 검색, 브라우저 닫지 말기, 오프라인으로 전환 단계를 수행했다면 앱이 계속 작동할 수 있습니다. 항목이 브라우저의 캐시에 남아 있을 수 있기 때문입니다. 그렇지 않다면 어떻게 해야 할까요? 사용자가 처음부터 또는 재부팅 후에 앱에 액세스할 수 있기를 바랍니다.

이를 위해 LocalServer 구성요소를 사용하고 리소스를 캡처합니다. 애플리케이션을 실행하는 데 필요한 HTML 및 자바스크립트와 같은 리소스를 캡처하면 톱니바퀴가 이러한 항목을 저장하지 않으며, 브라우저로부터의 반환 요청도 반환합니다. 로컬 서버는 교통 경찰 역할을 하고 스토어에서 저장된 콘텐츠를 반환합니다.

ResourceStore 구성요소도 사용하므로 캡처할 파일을 시스템에 직접 알려야 합니다. 많은 경우 애플리케이션의 버전을 지정하고 트랜잭션 방식으로 업그레이드를 허용하려고 합니다. 리소스 집합은 하나의 버전을 정의하며 새로운 리소스 세트를 출시할 때 사용자가 파일을 원활하게 업그레이드할 수 있도록 해야 합니다. 이 모델에서는 ManagedResourceStore API를 사용합니다.

리소스를 캡처하기 위해 GearBaseContent 객체는 다음을 수행합니다.

  1. 캡처가 필요한 파일 배열 설정
  2. LocalServer 만들기
  3. 새 ResourceStore 열기 또는 만들기
  4. 페이지를 매장으로 캡처하여 호출
// Step 1
this.storeName = 'gears-base';
this.pageFiles = [
  location.pathname,
  'gears_base.js',
  '../scripts/gears_db.js',
  '../scripts/firebug/firebug.js',
  '../scripts/firebug/firebug.html',
  '../scripts/firebug/firebug.css',
  '../scripts/json_util.js',    'style.css',
  'capture.gif' ];

// Step 2
try {
  this.localServer = google.gears.factory.create('beta.localserver', '1.0');
} catch (e) {
  alert('Could not create local server: ' + e.message);
  return;
}

// Step 3
this.store = this.localServer.openStore(this.storeName) || this.localServer.createStore(this.storeName);

// Step 4
this.capturePageFiles();

... which calls ...

GearsBaseContent.prototype.capturePageFiles = function() {
  this.store.capture(this.pageFiles, function(url, success, captureId) {
    console.log(url + ' capture ' + (success ? 'succeeded' : 'failed'));
  });
}

여기서 중요한 점은 자체 도메인에 있는 리소스만 캡처할 수 있다는 것입니다. SVN 트렁크의 원본 'gears_db.js' 파일에서 GearDB 자바스크립트 파일에 직접 액세스하려고 할 때 이 제한에 도달했습니다. 솔루션은 당연히 간단합니다. 외부 리소스를 다운로드하여 도메인 아래에 배치해야 합니다. LocalServer는 200 (성공) 또는 304 (수정되지 않음) 서버 코드만 허용하므로 302 또는 301 리디렉션은 작동하지 않습니다.

이는 영향을 줄 수 있습니다. images.yourdomain.com에 이미지를 배치하면 이미지를 캡처할 수 없습니다. www1과 www2가 서로를 볼 수 없습니다. 서버 측 프록시를 설정할 수 있지만, 이렇게 하면 애플리케이션을 여러 도메인에 분할할 필요가 없습니다.

오프라인 애플리케이션 디버깅

오프라인 애플리케이션의 디버깅은 조금 더 복잡합니다. 이제 테스트할 시나리오가 더 많이 있습니다.

  • 캐시에서 앱이 완전히 실행 중인 상태입니다.
  • 온라인 상태이지만 앱에 액세스한 적이 없고 캐시에 아무 것도 없음
  • 오프라인 상태이지만 앱에 액세스함
  • 오프라인 상태여서 앱에 액세스한 적이 없음 (최적의 앱이 아님)

삶을 더 편하게 만들기 위해 다음과 같은 패턴을 사용했습니다.

  • 브라우저가 캐시에서 특정 콘텐츠를 선택하지 않도록 하려면 Firefox (또는 선택한 브라우저)에서 캐시를 사용 중지합니다.
  • Firebug (및 Firebug Lite는 다른 브라우저에서 테스트하는 데 사용)를 사용하여 디버그하고 사방에서 console.log()을 사용하며 만일에 대비해 콘솔을 감지합니다.
  • 다음을 위해 도우미 자바스크립트 코드를 추가합니다.
    • 데이터베이스를 지우고 깔끔하게 선택
    • 캡처된 파일을 삭제합니다. 따라서 새로고침하면 인터넷으로 다시 돌아와서 파일을 가져옵니다 (개발을 반복하는 경우 유용함).

디버그 위젯은 Gear가 설치된 경우에만 페이지 왼쪽에 표시됩니다. 코드에는 콜아웃이 있습니다.

GearsBaseContent.prototype.clearServer = function() {
  if (this.localServer.openStore(this.storeName)) {
    this.localServer.removeStore(this.storeName);
    this.store = null;
  }
}

GearsBaseContent.prototype.clearTables = function() {
  if (this.db) {
    this.db.run('delete from BaseQueries');
    this.db.run('delete from BaseFeeds');
  }
  displayQueries();
}

마무리

Google Gear를 사용하는 방법은 꽤 간단하다는 것을 알 수 있습니다. 데이터베이스 구성요소를 더 쉽게 만들기 위해 GearDB를 사용했고 이 예시에서는 잘 작동하는 수동 ResourceStore를 사용했습니다.

가장 많은 시간을 할애하는 영역은 데이터를 온라인상으로 가져올 시기와 오프라인으로 저장하는 방법에 대한 전략을 정의하는 것입니다. 데이터베이스 스키마를 정의하는 데 시간을 할애하는 것이 중요합니다. 향후 스키마를 변경해야 할 경우 현재 사용자가 이미 데이터베이스 버전을 사용하게 되므로 변경사항을 관리해야 합니다. 즉, 모든 db 업그레이드와 함께 스크립트 코드를 전송해야 합니다. 이 문제를 최소화하는 것이 분명히 도움이 되며, 버전을 관리하는 데 도움이 되는 작은 라이브러리인 GearShift를 사용해 볼 수 있습니다.

또한 ManagedResourceStore를 사용하여 파일을 추적할 수 있으며 다음과 같은 결과가 발생할 수 있습니다.

  • 더 나은 업그레이드를 위해 파일을 관리하고 버전을 관리합니다.
  • 다른 콘텐츠에 대한 URL의 별칭을 지정할 수 있는 ManagedResourceStore의 기능이 있습니다. 유효한 아키텍처 선택은 Gears_base.js를 Gears 버전이 아닌 것으로 지정하고, 별칭이 이를 지원하여 오프라인 지원 기능을 모두 갖춘 Gears_base_withgears.js를 다운로드하는 것입니다.
앱에서 하나의 인터페이스를 사용하고 이 인터페이스를 두 가지 방식으로 구현하는 것이 더 쉽다고 생각했습니다.

애플리케이션 준비 작업이 쉽고 재미있었기를 바랍니다. 궁금한 점이 있거나 공유할 앱이 있는 경우 Google Gear 포럼에 참여하세요.