HTML5 API 강좌 #2 – Web SQL Database 와 GeoLocation

이 글은 HTML5 로 아이폰 앱 만들기 라는 제목으로 월간 W.e.b. 에 연재하는 글에서 발췌한 것입니다.
( 잡지에 기고한 글이라 원래 제 블로그의 글과 어투가 다릅니다. )

이 글 전에 먼저 “HTML5 API 강좌 #1 – Web Storage 와 Application Cache” 글을 읽어보시기 바랍니다.

지난 글에서는 모바일에서 HTML, CSS, JS 와 이미지파일을 캐슁할 수 있는 Application Cache 와 브라우저에 Key Value 값을 저장할 수 있는 Web Storage ( LocalStorage , Session Storage ) 를 사용하는 법을 배웠다. 이번 회에서는 데이터베이스 형식을 사용하여 좀 더 다양한 데이터를 저장할 수 있는 Web SQL Database 와 GeoLocation API 의 사용방법을 알아보자

Web SQL Database – http://dev.w3.org/html5/webdatabase/

이름에서 의미하듯이 Web SQL Database 는 클라이언트인 웹 브라우저에 Database 엔진을 심어서, 로컬에서 자바스크립트로 사용할 수 있도록 하는 것이다. 즉 예전에는 Database는 웹 서버 뒷 단에서 구조화된 데이터를 저장하고 웹 서버에서 이를 조회하여 페이지 구성 후 브라우저에서 불러다 보여주는 용도로만 사용했는데, 이제는 클라이언트(브라우저) 에도 이런 데이터를 저장할 수 있게 된 것이다. 이는 Offline 기능 지원 및 속도향상을 위해 다양하게 사용될 수 있다.

Web SQL Database의 실제 구현에는 SQLite 데이터베이스를 사용하고 있다. SQLite는 소스가 완전 공개된 Public Domain 오픈 소스 ( 소스코드를 가져다 마음대로 수정해서 사용해도 되고 상용으로 써도 아무런 문제가 없다 ) 로 iPhone OS 및 Android OS 에서도 기본 데이터베이스 형식으로 사용되고 있으며 간단한 Embedded Database를 구현할 때 많이 사용되는 오픈소스이다.

이를 이용해서 개발자는 브라우저 내에 테이블을 구성하고 데이터를 추가/수정/삭제할 수 있으며, 안정성을 보장하기 위해 일련의 작업을 하나의 논리적 작업으로 그룹화 해주는 트랜잭션 메커니즘까지도 지원된다.

현재 Webkit 기반 브라우저 (Safari, Chrome 및 iPhone, Android 의 브라우저) 에서는 이 Web SQL Database 를 지원하고 있지만, Firefox 및 IE 에서는 지원이 안되고 있다. 이것은 현재 HTML5 스펙을 작성하고 있는 W3C 에서 SQLite에 종속적인 SQL 언어기반의 Database 기술을 사용하는것이 웹 개발 형식과는 잘 맞지 않아서, 이 대신 B-Tree 기반의 key/value 저장소인 IndexedDB를 지원하는 것으로 가닥을 잡고 있기 때문이다. 즉 현재 모바일 브라우저들은 다 지원을 하기는 하지만, 실제 HTML5 표준스펙에서는 빠질 것으로 보인다. 하지만 현재로선 Firefox 4 베타버전을 제외한 모든 데스크탑/모바일 브라우저에서는 IndexedDB 지원이 불가능한 상태 이기 때문에, 지금은 모바일 웹 앱 개발시 Web SQL Database를 지원하도록 작성하고, 차후에 IndexedDB 지원이 완벽해지면 그때 다시 수정을 하는 방식으로 개발을 진행하는 방법이 좋을 것이다.

( 참고로 IndexedDB 의 오픈소스 구현체는 http://code.google.com/p/indexeddb/ 에서 받아서 사용해 볼수 있다. )

Web SQL Database 의 가장 중요한 메소드는 아래 3가지 이다.

  • openDatabase : 데이터 베이스 열기
  • transaction : 트랜잭션 시작
  • executeSql: SQL 문장 실행

각 함수를 좀더 자세히 살펴보도록 하자. 각 함수의 선언부에서 { } 는 생략이 가능한 인자를 의미한다.

Web SQL Database 는 스펙상 Asynchronous/Synchronous ( 비동기/동기 ) 두 가지 방식을 다 지원하지만, 현재 브라우저의 구현체들은 주로 Async 모델만을 지원한다. 즉 메소드의 리턴값으로는 적절한 객체나 결과를 얻을 수 없고, 콜백 함수를 메소드 인자로 전달하고 작업이 끝난 후 이 콜백 함수가 호출되는 형식으로 처리되어야 한다.

openDatabase

Database openDatabase(name, version, displayName, estimatedSize {, creationCallback } );

openDatabase 는 데이터베이스를 열거나 생성하는데 사용된다. 데이터베이스가 없다면 자동으로 생성된다. 이 메소드는 4개의 필수 인자와 1개의 옵션 인자를 가지고 있다.

  1. name : 데이터베이스 이름
  2. version : 버전 번호
  3. displayName : 데이터베이스 설명
  4. estimatedSize : 예상 크기
  5. { creationCallback : 생성후 호출될 콜백함수 }

버전번호는 약간 설명이 필요하다. 각 데이터 베이스는 버전번호를 가질 수 있는데, 개발자는 이 버전번호를 통해 데이터베이스의 스키마 변경 등의 작업을 지정할 수 있다. 단, 각 버전이 따로 존재 하는 것이 아니라, 항상 DB는 한 개의 버전만을 가질 수 있다. ( 실제로는 openDatabase 를 호출할 때 이 version 이 필수인자이기 때문에, DB 버전 업데이트를 자동으로 처리하기가 좀 곤란하다. 보통은 한 개의 버전번호를 고정적으로 붙여서 데이터베이스 오픈은 항상 가능하게 한 뒤, 내부에 관리자용 테이블을 만들어 데이터베이스의 스키마 버전번호를 저장하거나 하는 방식을 사용하기도 한다. )

예상크기는 데이터베이스의 크기를 결정하는 것으로, 일반적으로 브라우저 구현에 따르지만 기본값은 5MB로 되어있다. 예상크기가 5MB 보다 작다면 아무것도 묻지 않고 그냥 생성된다. 처음부터 5MB 보다 큰 예상크기를 요청하면 아래처럼 브라우저가 displayName 을 이용하여 이 크기의 디스크 공간을 사용해도 되는지를 사용자에게 묻는다. ( 사파리 브라우저만 이 확인창이 보이며 크롬 브라우저에서는 묻지 않고 그냥 생성한다. )

201011171529.jpg

아이폰 ( 모바일 사파리 ) 에서도 똑같이 확인창이 뜨게 된다.

201011171530.jpg

위의 DB 생성을 시도한 코드는 동일한 것으로 다음과 같다.

var db = openDatabase(‘mydb’, ‘1.0’, ‘guru test db’, 10 * 1024 * 1024);

데스크탑용 사파리와 모바일 사파리 둘 다, 정확히 10MB 의 크기를 요청하였지만 실제 생성되는 디스크공간이 차이가 있다. 이것은 브라우저에 따라 구현상의 차이가 있기 때문이다. 주의할 점은 데스크탑용 사파리에선 500MB, 1GB 까지의 데이터베이스도 생성이 가능하지만, 아이폰의 경우는 50MB 가 넘어가는 크기를 요청할 시 에러가 나면서 openDatabase 가 null 을 리턴한다. 안드로이드의 경우는 확인창이 뜨지않아서 정확히 체크는 불가능하지만 아이폰과 비슷할 것이므로, 모바일 웹 앱 개발시에는 무조건 DB 사이즈를 50MB 이하로 활용하도록 한다. ( 안정적으로 하려면 10메가 이상은 안 쓰는 것이 좋다. 그리고, 이것은 모바일 브라우저의 스펙에 따라 언제라도 바뀔 수 있다. )

마지막의 옵션인자인 creationCallback 은 데이터베이스가 생성될 때 호출되는 함수이다. 처음 데이터베이스가 생성 되었을 때 꼭 실행되어야 할 테이블 생성 스크립트 같은걸 처리하면 된다.

transaction / readTransaction

void transaction(callback {, errorCallback, successCallback} );

void readTransaction(callback {, errorCallback, successCallback} );

Web SQL Database 는 트랜잭션 메커니즘을 제공한다. 하나의 트랜잭션 안에서 여러 개의 SQL 문장을 실행하다가 에러가 발생했을 시에 앞에 했던 동작들을 모두 없었던 것으로 처리함으로써, 복잡한 데이터베이스 작업 도중에 발생한 에러로 데이터베이스에 문제가 생기는걸 방지해 준다. 또한 errorCallback 과 successCallback 인자를 통해 트랜잭션 전체의 에러/성공 시에 호출될 콜백함수도 지정가능하다.

transaction 함수는 보통 아래 형태로 호출된다.

var db = openDatabase(‘mydb’, ‘1.0’, ‘guru test db’, 10 * 1024 * 1024);
db.transaction(function (tx) {
   // 트랜잭션 내부
   // 롤백을 위해 SQL 함수는 tx 객체를 이용한다.
})

그리고 readTransaction 함수는 transaction 과 호출방식은 똑같지만, 트랜잭션내부에서 데이터베이스 쓰기 동작은 불가능하고 오로지 읽기 동작만 가능한 트랜잭션을 만들어준다.

executeSql

void executeSql(sqlStatement {, arguments, callback, errorCallback } );

executeSql 은 실제로 SQL 문장을 실행하는 함수로 일반적으로 Transaction 을 사용할경우 아래와 같은 형태로 호출된다.

var db = openDatabase(‘mydb’, ‘1.0’, ‘guru test db’, 10 * 1024 * 1024);
db.transaction(function (tx) {
    tx.executeSql(‘CREATE TABLE test (id INTEGER PRIMARY KEY, content TEXT)’);
})

4개의 인자가 있는데, 뒤의 2개는 성공/실패시 콜백함수를 지정하는 인자이며, 2번째 인자인arguments 는 SQL 문장에 있는 ? 문자들을 치환한 변수들의 배열이다. 즉 일반적인 INSERT 문의 경우 다음처럼 사용된다.

tx.executeSql( ‘INSERT INTO test (id,content) VALUES( ?, ? )’, [ id , contentStr ] );

SQL 문장을 + 와 같은 연산자를 이용한 스트링 연산으로 동적으로 만들 수도 있지만, SQL Injection 이라고 불리는 기법에 의해 해커의 표적이 되기 쉽다. 위와 같이 파라미터 치환자를 이용한 방식으로 하는 것이 훨씬 안전하다.

executeSql 의 3번째 인자인 Callback 에는 SQL 문장 실행후 transaction 객체와 Resultset 이 리턴된다.

tx.executeSql( ‘SELECT * FROM test’, [ ] , function ( tx, results) {
   for ( var i = 0 ; I < results.rows.length ; i++) {
      document.write(results.rows.item(i).id + ‘ - ‘ + results.rows.item(i).content);
   }
} );

Resultset 은 insertId , rowsAffected, rows 3개의 속성을 가지고 있다. insertID에는 실행한 SQL 문장이 INSERT 문 일 경우에 추가된 데이터의 Row ID 가 리턴되며, 만약 여러 개의 행이 추가되었을경우엔 마지막 ID가 리턴된다. rowsAffected 는 실행한 SQL 문장이 UPDATE 문 일경우에 변경된 Row 수가 리턴된다. rows 는 SELECT 문장일 경우 선택된 데이터들이 item 배열로 리턴 ( 실제로는 배열이 아니다. item 은 getter 함수일 뿐이다 ) 되며 length 를 통해 데이터의 개수를 알 수 있다. 위의 예제에서 보는 것처럼, test 테이블의 id 와 content 필드를 item(i).id , item(i).content 와 같이 각 테이블의 컬럼 이름을 이용해서 직접 item 객체의 속성인 것처럼 꺼내서 사용할 수 있다.

Web SQL Database 는 openDatabase, transaction , executeSql 세개의 함수만으로 로컬 브라우저상에서 유용하게 사용할 수 있는 데이터베이스를 제공한다. 실제로 Gmail 의 모바일버전 사이트는 이 Web SQL Database 를 사용하여, Offline 시에도 Inbox 의 최근 메일들을 보거나, 새로운 메일 작성 등을 가능하게 하고 있다.

Web SQL Database 역시 이전회에서 살펴본 LocalStorage 처럼 Safari Browser 의 Web Inspector 나 크롬 브라우저의 Developer Tools 를 통해 테이블과 그 내용들을 확인할수 있다.

201011171532.jpg

Chrome : Ctrl+Shift+J (윈) 또는 Command+Option+J (맥) 을 눌러 Developer Tools 실행

Safari : Ctrl+Alt+I (윈) 또는 Command+Option+I (맥) 을 눌러 Web Inspector 실행

GeoLocation API

HTML5 의 API 중 모바일환경에서 가장 많이 사용되고 있는 API 는 GeoLocation API 이다. 최근의 스마트폰들이 대부분 GPS를 내장하고 있어서, 모바일 웹에서도 위치정보에 기반한 구글맵과 같은 다양한 웹 서비스들이 이용되고 있다. 실제로 GeoLocation API 는 구글에서 제안한 것으로 HTML5 표준이 아니라 Geolocation API 라는 다른 스펙에서 정의되고 있다.

GeoLocation API 는 3개의 메소드로 이루어져 있다.

void navigator.geolocation.getCurrentPosition ( successCallback , errorCallback, options );
long navigator.geolocation.watchPosition ( successCallback , errorCallback, options );
void clearWatch ( watchID )

브라우저상에서 위치정보를 얻는 방법은 2가지인데, 첫번째는 getCurrentPoisition 함수를 이용하여 한번만 얻어오는 것이고, watchPosition 은 브라우저(디바이스)가 판단하여 위치가 바뀌었을 때마다 계속적으로 콜백 함수를 불러주는 방법이다. 전자의 방법은 주로 현재 위치의 지도를 보여주거나 근처의 POI ( Places of Interest ) 들을 불러올 때 사용하며, 후자는 사용자가 움직인 거리들을 기록하거나 지도에서 계속적으로 위치를 바꾸면서 보여주고 싶을 때 사용한다.

보통은 다음과 같은 형태로 호출한다.

navigator.geolocation.getCurrentPosition( show_position , show_error ) ;

성공시에 호출되는 successCallback 함수는 다음과 같은 위치정보를 리턴한다.

function show_position ( position ) {
var lat = position.coords.latitude;
var lon = position.coords.longitude;
}

이 위도경도 값으로 지도 API 를 호출하면 된다. 실제로 coords 는 altitude ( 고도 ) , heading ( 방향 , 360도 ) , speed ( 속도 m/s ) 와 같은 몇 개의 속성을 더 가지고 있지만, 이것은 모바일 브라우저에 따라 지원될 수 도 있고 안될 수도 있다. ( 지원 된다고 해도 부정확할 수 있다 ) 위도 경도값은 꼭 GPS 정보 뿐만이 아니라 모바일 브라우저 구현에 따라 IP 주소, WiFi/블루투스의 MAC 주소, GSM/CDMA 의 셀타워 정보등으로부터 위치정보를 얻게 된다.

Web SQL Database 를 이용한 Offline 실행지원 어플리케이션 - CheckList

이제 앞에서 배운 Application Cache 와 Web SQL Database 를 활용해서 Offline 에서도 실행이 가능한 Application 을 만들어 보자. 만들어볼 CheckList Application 은 모바일에서 활용가능한 간단한 체크리스트이다.

  • Application Cache 적용을 위해 Manifest 를 지정한다.
  • Web SQL Database 를 이용하여 아이템을 저장한다.

* 이 예제는 http://www.berttimmermans.com/2009/02/checklist/ 의 CheckList Web Application을 좀 더 간략하게 만든 것이다.

먼저 Offline 에서도 실행 가능한 Web App 의 HTML 내용은 다음과 같다.





Checklist WebApp ( Offline )


New Check List

앞서 설명한대로 간단한 체크리스트를 Offline 에서 등록/관리 하는 Application 이다. 추가버튼만 있고 모든 아이템은 items 라는

div 에 표시되도록 되어있다. 주요 기능은 app.js 에 있다. app.js 를 살펴보자.

// BUILD DATABASE ———————————————————————
var db;
// 데이터베이스를 오픈한다. 사이즈는 넉넉히 20만바이트로 지정한다.
try {
    if (window.openDatabase) {
        db = openDatabase(“Checklist”, “1.0”, “HTML5 Database API”, 200000);
        if (!db)
        alert(“DB를여는데실패했습니다.버전이틀리거나이도메인에할당된공간이꽉찻기때문입니다”);
        else
        var highestId = 0;
    } else
    alert(“Web SQL Database가지원되지않는브라우저입니다.”);
} catch (err) {
}
function loaded() {
    // 페이지가 로드되면 테이블이 있는지 확인하여 없으면 새로 테이블을 생성한다.
    // 이미 생성한 경우라면 BuildList() 함수를 호출하여 리스트에 아이템을 채운다.
    db.transaction(function (tx) {
        tx.executeSql(“SELECT COUNT( * ) FROM ToDos”, [], function (result) {
            BuildList();
        }, function (tx, error) {
            tx.executeSql(“CREATE TABLE ToDos(id REAL UNIQUE, description TEXT, status REAL)”, [], function (result) {
                BuildList();
            });
        });
    });
}
// 페이지 로드후 이벤트에 등록한다.
addEventListener(‘load’, loaded, false);
// 새 아이템 입력 —————————————————————————————
function newToDo() {
    if (document.getElementById(‘description’).value != “”) {
        highestId++;
        var description = document.getElementById(‘description’).value;
        var status = 0;
        // INSERT 문으로 테이블에 새 아이템 추가
        db.transaction(function (tx) {
            tx.executeSql(“INSERT INTO ToDos(id, description, status) VALUES(“ + highestId + ”, ‘” + description + ”’, ” + status + ”)”, [],
            function (result) {
                document.getElementById(‘description’).value = “”;
                BuildList();
            }, function (tx, error) {
                alert(error);
            }
            );
        });
    }
}
// 아이템 삭제 —————————————————————————————
function deleteToDo(id) {
    // DELETE 문으로 테이블에서 아이템 삭제
    db.transaction(function (tx) {
        tx.executeSql(“DELETE FROM ToDos WHERE id = ” + id + “;”, [],
        function (result) {
            document.getElementById(id).style.display = “none”;
        }, function (tx, error) {
            alert(error);
        }
        );
    });
}
// CHANGE STATUS —————————————————————————————
function updateToDo(id, status) {
    if (status == ‘1’) {
        status = ‘0’;
    } else {
        status = ‘1’;
    }
    // 해당 체크리스트 아이템을 수행했는지 안했는지를 체크한다. 체크한 아이템은 흐리게 만든다.
    db.transaction(function (tx) {
        tx.executeSql(“UPDATE ToDos SET status = ” + status + ”WHERE id = ” + id + ”;”, [],
        function (result) {
            if (status == ‘1’) {
                document.getElementById(id + ”box”).removeAttribute(“onclick”);
                var newfunction = document.createAttribute(“onclick”);
                newfunction.nodeValue = “updateToDo(” + id + “, ” + status + “)”;
                document.getElementById(id + ”box”).setAttributeNode(newfunction);
                document.getElementById(id).style.opacity = ‘0.2’;
            }
            if (status == ‘0’) {
                document.getElementById(id + ”box”).removeAttribute(“onclick”);
                var newfunction = document.createAttribute(“onclick”);
                newfunction.nodeValue = “updateToDo(” + id + “, ” + status + “)”;
                document.getElementById(id + ”box”).setAttributeNode(newfunction);
                document.getElementById(id).style.opacity = ‘1’;
            }
        }, function (tx, error) {
            alert(error);
        }
        );
    });
}
// BUILD LIST —————————————————————————-
function BuildList() {
    // DB 에서 전체 체크리스트 아이템을 읽어온다.
    document.getElementById(‘items’).innerHTML = “”;
    db.transaction(function (tx) {
        tx.executeSql(“SELECT id, description, status FROM ToDos”, [], function (tx, result) {
            for (var i = 0; i < result.rows.length; ++i) {
                var row = result.rows.item(i);
                ToDo(row[‘id’], row[‘description’], row[‘status’]);
                if (row[‘id’] > highestId)
                highestId = row[‘id’];
            }
        }, function (tx, error) {
            alert(‘DB에서아이템을읽어오는데실패했습니다 - ’ + error.message);
            return;
        });
    });
}
// TODO ————————————————————————————-
function ToDo(id, description, status) {
    // 각 아이템 등록
    var ToDoItem = “ < div id = ’” + id + “’class = ’part“;
    if (status == 1) {
        ToDoItem += ”done”;
    }
    ToDoItem += “’ > < input type = ’checkbox’”;
    if (status == 1) {
        ToDoItem += “checked = ’checked’”;
    }
    ToDoItem += ”onclick = ’updateToDo(” + id + “, ” + status + “)’id = ’” + id + “box’class = ’checked’ / > ”;
    ToDoItem += ” < span > ” + description + “ < /span>”;
ToDoItem +=”
 < /div>
 “;
document.getElementById(‘items’).innerHTML = document.getElementById(‘items’).innerHTML + ToDoItem;
}

그리고, 오프라인 실행을 위해 webapp.manifest 를 만든다.

CACHE MANIFEST app.js # 캐시를 선언한 index.html 은 자동으로 추가되므로 적지 않아도 된다. # Version 20100905-054

실행한 화면은 다음과 같다. 한번 온라인상태에서 로딩만 되면 오프라인시에도 동작된다.

201011171533.jpg 201011171533.jpg

우측의 화면은 아이폰을 Airplane 모드로 바꾸어서 오프라인에서도 동작하는지를 테스트해본 것이다.

현재 실행에 필요한 파일은 index.html 파일과 app.js 파일 2가지 밖에 없다. webapp.manifest 파일에 의해 index.html , app.js 두 개의 파일이 캐쉬되므로 webapp.manifest 파일이 수정되지 않는한 index.html 이나 app.js 의 수정내용이 변경되지 않는다는 것을 명심하자. 코드를 업데이트 하였다면 webapp.manifest 파일의 마지막 #주석문에 버전번호를 바꾸어주도록 하자.

다음 회에서는 아이폰용 Web App 을 만드는 방법에 대해 알아보자.

.

HTML5 API 강좌 #2 – Web SQL Database 와 GeoLocation”에 대한 7개의 생각

  1. 옴그루

    정말 잘 읽었습니다. HTML5 가 궁금하던 차에 많이 해소가 되었습니다. 다음글도 기다려지네요 ^^

    응답
  2. 핑백: HTML5 API 강좌 #3 – 아이폰 Web App 만들기 | Guru's Blog

  3. topspin

    오랜만입니다. html 관련 자료를 찾아 헤매다가 여기까지 왔네요. 역시 내공이..ㄷㄷ.. 다음 글도 기다리겠습니다. 눈이 빠지게…^^ . -천*훈-

    응답
  4. 이상

    네이티브 앱으로 할지 하이브리드 앱으로 만들지 아니면 웹앱으로 할지 자료 조사중 입니다.
    지금 시장에서는 네이티브만 활성화 되어 있는것 같은데 서로의 장단점이 있어서 고민이네요.
    정말 많은 도움이 됐습니다. ^^

    응답
  5. 크리드

    자바스크립트를 활용한다는게 두렵긴(?)하지만 획기적인것 같습니다.
    플렉스의 액션스크립트가 자바스크립트와 같이 스크립트이긴 하지만 ECMA규격에 따라 나름 강력하게 OOP를 지원하는데요…HTML5에서도 뭔가 획기적인 대안이 아쉽습니다.

    응답

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다