'js'에 해당되는 글 2건

  1. 2014.02.14 localForage : 더 나아진 오프라인 저장소
  2. 2010.02.16 사우더스의 성능 문제 1타5피 (3)

웹앱에는 대용량 데이터나 바이너리 파일을 오프라인으로 저장할 수 있는 기능이 있습니다. 심지어 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

댓글을 달아 주세요

http://www.flickr.com/photos/jasonprini/4104210048/

스티브 사우더스씨가 브라우저의 이상한 동작에 대한 글을 작성했습니다. 성능에 관련된 내용인데, 간략한 내용은 다음과 같습니다.

스키마가 없으면 두 번 다운로드
인터넷 익스플로러 7과 8 버전은 http 프로토콜이 빠져있으면 스타일 시트를 두 번 다운로드 합니다. 가끔 "//stevesouders.com/images/book-84x110.jpg" 과 같이 사용하는 경우는 봤는데, 이 경우는 계속 주의해야 할 것 같습니다.

document.write 스크립트가 다운로드 정지의 원인(파이어폭스)
파이어폭스에서는 docuemnt.write를 사용한 스크립트를 읽어들이면 다른 다운로드를 정지시킵니다. 안타까운 사실이지만, document.write는 이미 만들어졌습니다. 이 문제는 페이지 컨텐트에 document.write를 이용해서 광고를 삽입하려고 할 때 수천억배쯤 더 나빠집니다. 대충 이런 식으로 작성하죠.
[code:js]
document.write('<script src="http://www.adnetwork.com/main.js"><\/script>');
다행히 대부분의 최신 브라우저들은 document.write로 추가된 스크립트까지 병렬로 읽어들입니다. 그러나, 몇 주전 파이어폭스 3.6에서 광고를 삽입할 때 이상한 중단 현상을 발견하고, 추적해보았더니 document.write로 추가된 스크립트가 문제였습니다.
document.write 스크립트 테스트 페이지에서 이 문제점을 보여주고 있습니다. 이 페이지에는 4개의 스크립트가 있는데, 첫번째와 두번째 스크립트는 document.write를 사용해 추가되었습니다. 세번째와 네번째 스크립트는 일반적인 방법(HTML과 SCRIPT SRC)을 사용해서 추가했습니다. 그리고 모든 스크립트는 4초가 걸려야 다운로드 할 수 있도록 했습니다. IE8, 크롬4, 사파리4, 오페라 10.10은 페이지를 전부 다운로드 하는데 ~4초가 걸렸습니다. 모든 스크립트는 document.write까지 포함해서 병렬로 처리되었습니다. 반면 파이어폭스에서는 페이지를 전부 다운로드 하는데 12초가 걸렸습니다(2.0, 3.0, 3.6에서 테스트). 첫번째 document.write 스크립트는 1초부터 4초까지 걸렸고, 두번째 document.write 스크립트에 5초부터 8초까지 걸렸습니다. 그리고 나머지 두 개의 일반적인 스크립트를 9초부터 12초까지 다운로드 했습니다.

media=print 스타일시트
인터넷 익스플로러에서는 media="print"를 사용한 스타일시트가 렌더링을 정지시킵니다.
저는 웹브라우저가 현재 사용중이지 않은 미디어 타입의 스타일시트를 건너뛰지 않고 다운로드한다는 사실에 놀랐습니다. 몇몇 웹 개발자들에게 물어보았지만 아무도 이런 동작에 대한 적당한 이유를 대지 못하더군요. 또한 여러분은 media="print" 타입의 스타일시트라 하더라도 Page Speed나 YSlow의 추천에 따라 문서 HEAD에 스타일시트를 두려고 할 것입니다.

동적인 스타일시트
IE에서는 DHTML과 setTimeout을 사용해서 스타일시트를 읽어들이면 렌더링이 멈추는 것을 방지할 수 있습니다. 몇 주전에 유명한 위젯을 만든 회사와 회의를 했습니다. 위젯이 메인 페이지에 주는 영향을 줄이기 위해 그들이 사용한 기술이 바로 다음과 같이 스타일시트를 동적으로 불러들이는 것이었습니다.
[code:js] var link = document.createElement('link'); link.rel = 'stylesheet'; link.type = 'text/css'; link.href = '/main.css'; document.getElementsByTagName('head')[0].appendChild(link);
과거에는 다운로드 중단을 피하기 위해 스크립트를 동적으로 로딩하는 것에만 주의를 기울였습니다. 스타일시트를 동적으로 읽어들일 생각은 미처 하지 못했었죠. 스타일시트로 오면 다운로드 중단은 문제가 되지 않습니다. 스타일시트는 다운로드를 멈추게 하지 않습니다(파이어폭스 2.0는 예외). 스타일시트 다운로드에서 걱정스러운 부분은 스타일시트를 모두 다운로드 하기 전까지 렌더링을 멈추는 IE의 특성입니다. 다른 브라우저에서는 스타일이 적용되지 않은 컨텐트가 잠깐 나왔다가 사라지는 현상(Flash Of Unstyled Content, FOUC)이 생길 것입니다.

배경이미지 예측
크롬과 사파리는 스타일시트가 전부 준비되기 전이라도 배경 이미지의 다운로드를 시작합니다. 배경 이미지 스타일이 재정의 되는 경우라면 쓸데없는 다운로드가 늘어나는 셈입니다.
직장 동료인 스티브 램이 이런 동작을 저에게 알려주었는데, 이 얘기를 들었을 때 제가 처음 했던 말은 "완전 낭비잖아!" 였습니다. 개인적으로 프리페칭(prefetching)을 좋아하기는 하지만, 대부분의 프리페칭 기능은 너무 공격적이어서 크게 좋아하지 않습니다. 사용하지 않아도 될 다운로드 리소스를 소모하는 일이 잦기 때문입니다. 이 얘기를 처음 들은 이후에 조금 더 생각해봤습니다. 예측된 배경 이미지의 낭비는 얼마나 자주 일어날까? 검색해봤더니 유명 웹 사이트에서는 재정의된 배경 이미지 스타일이 없더군요. 하나도요. 그런 페이지가 하나도 없다고 말할 수는 없겠지만, 매우 이례적인 경우인 것만은 확실합니다.
다르게 생각하면 이러한 배경 이미지 예측 다운로드는 사용자가 인지하는 페이지 속도와 성능을 개선시킬 것입니다.

from Souders blast off 5 in a row
Posted by 행복한고니 트랙백 1 : 댓글 3

댓글을 달아 주세요

  1. addr | edit/del | reply BlogIcon mooo 2010.02.17 07:58

    재미있게 잘 봤습니다!
    근데 고민할 것들이 더 늘어난 기분이네요. :-)

  2. addr | edit/del | reply BlogIcon 아즈키 2010.02.17 14:50

    document.write 가 별로인 것은 알고 있었지만, FF 에서의 성능이 이렇게까지 취약일줄이야. ㄷㄷ