웹앱에는 대용량 데이터나 바이너리 파일을 오프라인으로 저장할 수 있는 기능이 있습니다. 심지어 MP3 파일을 캐시하는 것도 가능합니다. 이미 웹 브라우저 기술은 오프라인으로 데이터를 저장할 수 있으며, 대용량도 저장할 수 있습니다. 문제는 사용할 방법이 파편화되어 있다는 것입니다.

localStorage는 매우 단순한 데이터 저장소이지만, 속도도 느리고 대용량 바이너리 데이터를 다룰 수 없습니다. IndexedDB와 WebSQL은 비동기 식이며 빠르고 대용량도 다룰 수 있지만 API가 직관적이지 못합니다. 심지어 아직도 주요 브라우저 중에는 IndexedDBWebSQL를 둘 다 지원하지 않는 것도 있으며 가까운 시일 내에는 이런 상황이 나아질 것 같지도 않습니다.

만약 오프라인을 지원하는 웹앱을 작성해야 하고, 어디서부터 시작해야 할지 갈피를 못 잡겠다면 이 글이 유용할 것입니다. 오프라인 지원 기능은 한 번도 다뤄본 적이 없지만, 이 기능 때문에 골치 아픈 분이 읽어도 좋습니다. 모질라에서 만든 localForage는 어떤 브라우저에서도 손쉽게 데이터를 오프라인으로 저장할 수 있게 해 주는 라이브러리입니다.

저는 around라는 HTML5 포스퀘어 클라이언트를 개발하며 오프라인 저장소의 문제점들을 경험했습니다. localForage 사용법은 이 글에서 설명하겠지만, 코드를 꼼꼼히 보고 배우는 것을 좋아하는 분들에게는 소스도 공개되어 있습니다.

localForage는 매우 단순한 localStorage API를 사용하는 자바스크립트 라이브러리로서, get, set, remove, clear, length와 같은 기본적인 기능은 물론 다음과 같은 기능도 지원합니다.

  • 콜백을 사용한 비동기 API
  • IndexedDB, WebSQL, localStorage 드라이버 (가장 적절한 드라이버를 자동으로 선택)
  • Blob와 임의의 데이터 타입을 지원하여 이미지, 파일 등 저장 가능
  • ES6 Promises 지원

IndexedDBWebSQL을 포함한 덕분에 localStorage만 사용할 때보다 더 많은 데이터를 저장할 수 있으며, 이들의 비동기적 특성 덕분에 get/set 함수를 호출해도 메인 쓰레드가 느려지지 않으므로 앱이 더 빨라질 수 있습니다. promises를 지원하므로 콜백 수프(callback soup, 콜백이 너무 많아서 프로그램의 흐름을 알기 어렵게 된 상태) 없이 자바스크립트를 작성할 수 있습니다. 물론 콜백을 좋아한다면 localForage는 그 방법도 지원합니다.

얘기는 그만. 어떻게 동작하는지 보여줘!

localStorage API는 여러 경우에 있어 사실 꽤 훌륭합니다. 사용하기 편하고 복잡한 데이터 구조를 만들지 않아도 되고, 단순하여 별도의 코드 조각이나 라이브러리가 따로 필요하지 않습니다. 예를 들어, localStorage를 사용해 앱 설정을 저장하고 싶다면 다음과 같이 작성할 수 있습니다.

// 오프라인으로 저장할 설정값
var config = {
    fullName: document.getElementById('name').getAttribute('value'),
    userId: document.getElementById('id').getAttribute('value')
};
 
// 다음에 앱을 실행할 때 사용하기 위해 설정값을 저장한다.
localStorage.setItem('config', JSON.stringify(config));
 
// 다음에 앱을 실행할 때 다음과 같이 사용할 수 있다.
var config = JSON.parse(localStorage.getItem('config'));

단, localStorage에는 문자열 형식으로 값을 저장해야 하므로 값을 JSON 형태로 혹은 JSON 형태에서 변환해야 합니다.

아주 기분 좋을 정도로 직관적이지만 금세 localStorage에 있는 문제점을 깨닫게 될 것입니다.

  1. 동기식이다. 데이터가 얼마나 크던 데이터를 디스크에서 읽어 들여 해석할 때까지 기다려야 합니다. 이 때문에 앱의 반응성이 느려질 것입니다. 이는 특히 모바일 디바이스에 안 좋은데 데이터를 완전히 읽어 들일 때까지 메인 쓰레드가 대기해야 하므로 여러분의 앱은 느리고 반응성까지 떨어지는 것처럼 보일 것입니다.
  2. 문자열만 지원한다. 반드시 JSON.parseJSON.stringify를 사용해야 합니다. 이는 localStorage가 값으로 자바스크립트 문자열 형식만 지원하기 때문입니다. 숫자도, 불리언도, 대용량 바이너리 데이터 등도 저장할 수 없습니다. 이 때문에 숫자나 배열을 저장하는 작업은 귀찮아지고, 대용량 바이너리 데이터를 저장하는 것은 사실상 불가능합니다(최소한 엄.청. 짜증나고 느릴 것입니다).

localForage를 사용한 더 좋은 방법

localForage는 localStorage의 API를 사용하면서도 비동기식 API를 사용해 이 같은 문제를 극복했습니다. 다음은 같은 데이터를 저장할 때 IndexedDB와 localStorage의 사용법을 비교한 것입니다.

IndexedDB 코드

// IndexedDB.
var db;
var dbName = "dataspace";
 
var users = [ {id: 1, fullName: 'Matt'}, {id: 2, fullName: 'Bob'} ];

var request = indexedDB.open(dbName, 2);
 
request.onerror = function(event) {
    // 에러 처리
};
request.onupgradeneeded = function(event) {
    db = event.target.result;
 
    var objectStore = db.createObjectStore("users", { keyPath: "id" });
 
    objectStore.createIndex("fullName", "fullName", { unique: false });
 
    objectStore.transaction.oncomplete = function(event) {
        var userObjectStore = db.transaction("users", "readwrite").objectStore("users");
    }
};
 
// 데이터베이스가  생성되고 나면 사용자를 추가한다
 
var transaction = db.transaction(["users"], "readwrite");
 
// 데이터가 데이터베이스에 모두 저장되었을 때 할 일
transaction.oncomplete = function(event) {
    console.log("All done!");
};
 
transaction.onerror = function(event) {
    // 에러도 빠뜨리지 말고 다루어야 한다
};
 
var objectStore = transaction.objectStore("users");
 
for (var i in users) {
    var request = objectStore.add(users[i]);
    request.onsuccess = function(event) {
        // Contains our user info.
        console.log(event.target.result);
    };
}

WebSQL은 번잡하다 싶을 정도는 아니지만 그래도 기본 코드가 약간 필요하긴 합니다. localForage에서는 다음과 같이 사용하면 됩니다.

localForage Code

// 사용자를 저장한다
var users = [ {id: 1, fullName: 'Matt'}, {id: 2, fullName: 'Bob'} ];
localForage.setItem('users', users, function(result) {
    console.log(result);
});

얼마 차이 안나는군요?

문자열 외의 데이터 저장하기

사용자의 프로필 이미지를 다운받은 뒤 오프라인 상태에서 볼 수 있도록 캐시한다고 생각해 봅시다. localForage에서는 바이너리 데이터를 간단하게 저장할 수 있습니다.

// AJAX를 사용해서 사용자의 사진을 다운로드한다
var request = new XMLHttpRequest();
 
// 첫 번째 사용자의 사진 가져오기
request.open('GET', "/users/1/profile_picture.jpg", true);
request.responseType = 'arraybuffer';
 
// AJAX 상태가 변경되면 사진을 로컬에 저장한다.
request.addEventListener('readystatechange', function() {
    if (request.readyState === 4) { // readyState DONE
        //localStorage로는 바이너리 데이터를 있는 그대로 저장할 수 없었을 것이다.
        localForage.setItem('user_1_photo', request.response, function() {
            // 사진이 저장되었다. 다음 단계를 진행하자.
        });
    }
});
 
request.send();

저장한 사진을 가져올 때는 코드 3줄만 있으면 된다.

localForage.getItem('user_1_photo', function(photo) {
    // data URI 등을  만들어 img 태그 등에 사진을 표현한다
    console.log(photo);
});

Callback과 Promise

코드에서 콜백을 사용하고 싶지 않다면, localForage에서 콜백 함수를 인수로 사용하는 대신 ES6 Promise를 사용할 수 있습니다.

localForage.getItem('user_1_photo').then(function(photo) {
    // data URI 등을  만들어 img 태그 등에 사진을 표현한다
    console.log(photo);
});

사실 이 예제는 조금 억지로 만든 감이 있습니다. 그러나 around를 살펴보면 이 라이브러리가 실제로 사용되는 것을 볼 수 있습니다.

크로스 브라우저 지원

localForage는 최근 브라우저를 모두 지원합니다. IndexedDB사파리 외의 모든 최근 브라우저에서 지원하고 (IE 10+, IE Mobile 10+, Firefox 10+, Firefox for Android 25+, Chrome 23+, Chrome for Android 32+, and Opera 15+), 내장 안드로이드 브라우저(2.1+)와 사파리는 WebSQL을 사용합니다.

가장 안 좋은 경우라 해도 localForage는 localStorage를 대비책으로 사용하므로 최소한 기본적인 데이터는 오프라인으로 저장할 수 있게 됩니다(하지만 이 경우 Blob은 저장할 수 없고 많이 느릴 수도 있다). 그러나 적어도 자동으로 데이터를 JSON 문자열로 변환 혹은 JSON 문자열을 데이터로 변환해주는 정도의 이점은 얻을 수 있습니다.

localForage에 대한 자세한 정보는 Github에서 볼 수 있으며 localForage가 해줬으면 하는 기능이 있다면 이슈에 추가해주세요.

이 글은 localForage: Offline Stroage, Improved를 번역한 글입니다. 원문의 라이센스에 따라 CC BY-SA로 공개합니다.


Posted by 행복한고니 트랙백 0 : 댓글 0