'2014/02'에 해당되는 글 2건

  1. 2014.02.14 localForage : 더 나아진 오프라인 저장소
  2. 2014.02.03 W3C 모바일 웹 모범 사례

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

이 글은 W3C의 Mobile Web Best Practices 1.0을 번역 및 요약 정리한 글입니다. 최대한 원문의 의미를 그대로 전달하려고 노력했지만 번역 및 요약 과정에서 잘못 전달되거나 누락된 부분이 있을 수도 있으니, 이 점을 감안하고 봐주시기 바랍니다. 편의상 글 내용에서는 높임말을 사용하지 않았으니 양해부탁드립니다.


모바일 핸드폰CC licensed image by Irita Kirsbluma


모바일 웹 모범 사례는 모바일 기기에서 사용하는 웹의 사용자 경험을 증진시킬 목적으로 만들어졌다. 따라서 콘텐츠 자체에 대해서는 다루고 있지만, 콘텐츠를 만드는 방법, 사용하는 모바일 기기나 브라우저에 대해서는 다루지 않는다.

1. [의미적 일관성] 하나의 URI에서 제공하는 콘텐츠는 서로 다른 기기에서 접근하더라도 하나의 의미적으로 일관성있는 경험을 제공하라.

어떤 기기에서 접근해도 동일한 사용자 경험을 제공해야 한다.

2. [기능] 디바이스의 기능을 최대한 잘 활용하여 개선된 사용자 경험을 제공하라.

3. [결함] 합리적인 단계를 통해 지원되지 않는 기능을 우회하라.

데스크톱과 마찬가지로 모바일 브라우저에도 차이가 있고 브라우저 별로 지원하는 기능에 차이가 있을 수 있다. 관련 표준의 필수적인 기능이 지원되지 않거나 구현 자체에 버그나 에러가 있는 결함이 있을 수도 있으므로 합리적인 방법을 통해 이러한 문제를 우회하라. 특정 기기에서만 우회하거나  특정 기기에만 제한을 두는 방법은 권장되지 않는다.

4. [테스트] 에뮬레이터는 물론 실제 디바이스에서도 테스트하라.

5. [URI] 사이트에 진입하는 URI는 짧게 만들어라.

6. [탐색바] 페이지 상단에는 최소한의 탐색 메뉴만 제공하라.

상단에는 기본적인 메뉴만 제공하고 정말 필요하다면 페이지 하단에 추가 메뉴를 제공하라. 사용자는 페이지를 읽어들였을 때 스크롤하지 않고도 콘텐츠를 볼 수 있어야 한다.

7. [균형] 한 페이지에 너무 많은 링크를 포함시키는 것과 어떤 콘텐츠에 접근하기위해 링크를 너무 많이 이동해야 하는 것은 서로 트레이드 오프 관계이므로 적절히 조절하라.

8. [탐색] 일관성있는 탐색 메커니즘을 제공하라.

9. [키보드 접근] 탐색 메뉴의 링크와 자주 사용하는 기능에 접근할 수 있는 단축키를 제공하라.

10. [링크 대상 ID] 각 링크의 대상을 명확히 설정하라.

11. [링크 대상 형식] 해당 디바이스가 대상 파일을 지원하는지 정확히 알 수 없다면 대상 파일의 형식을 고지하라.

테스트 방법

- 휴먼 테스트 : 링크에 적절한 설명이 제공되는가 (예. "여기를 클릭하세요"는 안됨)
- 머신 테스트 : 링크가 HTML이 아닌 형식을 가리키는가
- 휴먼 테스트 : 만약 HTML이 아닌 형식을 가리킨다면 이 링크 대상의 형식에 대한 정보가 적절히 제공되는가

12. [이미지맵] 디바이스가 이미지맵을 효과적으로 지원하는지 정확히 알 수 없다면 이미지맵을 사용하지 마라.

13. [팝업] 사용자에게 알리지 않고 팝업이나 새 창을 띄우지 말고, 현재 창을 변경하지 마라.

14. [자동 새로고침] 사용자에게 알리지 않고, 멈출 수단도 만들지 않은 상태에서 주기적으로 페이지를 자동으로 새로 고침하도록 만들지 마라.

15. [리디렉션] 다른 페이지로 자동으로 이동하는 마크업을 사용하지 마라. 필요하다면 서버에서 HTTP 3xx 코드를 반환하여 리디렉션하라.

16. [외부 리소스] 링크한 외부 리소스의 개수를 최소한으로 유지하라.

17. [적합성] 모바일 환경에서 사용하기 적합한 콘텐츠가 되도록 하라.

18. [명료성] 분명하고 간결한 말을 사용하라.

19. [제한] 콘텐츠는 사용자가 요청한 것으로만 제한하라.

모바일 사용자는 트래픽에 돈을 지불하므로 관련 없는 콘텐츠를 너무 많이 제공하면(특히 광고) 사용자의 시간과 돈을 소비하게 되어 결과적으로는 불만족스러운 사용자 경험을 낳는다. 일반적으로 이러한 콘텐츠는 다운로드 하기 전에 사용자의 동의를 구하는 것이 좋다.

20. [유용한 페이지 크기] 페이지를 유용하지만 제한된 크기로 나누어라.

21. [페이지 크기 제한] 페이지의 전체 크기가 디바이스의 제한된 메모리에서도 문제없는지 확인하라.

22. [스크롤링] 불가피한 경우를 제외하고, 스크롤은 한 방향으로 제한하라.

가로 혹은 세로 중 하나만 사용하라. 어쩔 수 없는 경우는 예외로 한다.

23. [중심 의미] 페이지의 중심 의미에 해당하는 글감을 그렇지 않은 글감보다 앞에 나오도록 작성하라.

가장 중요한 정보 혹은 관련있는 정보가 먼저 나타나도록 하라.

24. [공백 그래픽] 그래픽을 사용해 공백을 표현하지 마라.

25. [큰 그래픽] 모바일 디바이스에서 표현할 수 없는 이미지를 사용하지 마라. 필수적인 이미지인 경우를 제외하고 대형 혹은 고해상도 이미지의 사용을 피하라.

26. [색상 사용] 색상을 사용한 정보는 색상을 사용하지 않는 경우에도 알 수 있는지 확인하라.

27. [색상 대비] 전경색과 배경색은 서로 적절한 수준의 대비를 이루어야 한다.

28. [배경 이미지 가독성] 배경 이미지를 적용할 때는 이 상태에서도 디바이스에서 콘텐츠를 읽을 수 있는지 확인하라.

29. [페이지 제목] 짧지만 페이지의 내용을 설명할 수 있는 제목을 사용하라.

30. [프레임 금지] 프레임을 사용하지 마라.

31. [구조] 마크업 언어의 특성을 사용하여 논리적 문서 구조를 표현하라.

32. [테이블 지원] 디바이스에서 테이블을 지원하는지 확실하지 않아면 테이블을 사용하지 마라.

33. [중첩 테이블] 중첩 테이블을 사용하지 마라.

34. [테이블 레이아웃] 테이블을 사용해 레이아웃을 구성하지 마라.

35. [테이블 대안] 가능하다면 다른 수단을 사용해 표 형식을 표현하라.

36. [대체 텍스트] 텍스트가 아닌 콘텐츠에 대해서는 항상 그에 상응하는 텍스트를 제공하라.

37. [오브젝트 혹은 스크립트] 내장 오브젝트나 스크립트에 의존하지 마라.

38. [이미지 크기] 크기가 정해진 이미지는 마크업에서 이미지 크기를 설정하라.

39. [이미지 리사이즈] 이미지 크기가 정해져 있다면, 이미지 리사이즈는 서버에서 하라.

40. [유효한 마크업] 문서는 유효한 문법을 사용해 작성하라.

41. [단위] 마크업 언어의 속성이나 스타일 속성의 값에서 픽셀 단위 혹은 절대 단위를 사용하지 마라.

42. [스타일시트 사용] 디바이스가 CSS를 제공하는지 명확하지 않은 경우가 아니라면, 레이아웃과 프리젠테이션은 스타일 시트를 사용해 제어하라.

43. [스타일시트 지원] 필요에 따라 스타일 시트없이도 문서가 잘 보이도록 문서를 구조화하라.

44. [스타일시트 크기] 스타일시트의 크기는 작게 유지하라.

45. [최소화] 간결하고 효율적인 마크업을 사용하라.

46. [콘텐츠 형식 지원] 디바이스에서 지원하는 것이 확실한 형식으로 콘텐츠를 전송하라.

47. [콘텐츠 형식 선호] 가능하다면 디바이스에서 선호하는 형식으로 콘텐츠를 전송하라.

48. [문자 인코딩 지원] 디바이스에서 지원하는 것이 확실한 문자 인코딩을 사용해 콘텐츠를 인코딩하라.

49. [문자 인코딩 사용] 응답시 사용한 문자 인코딩을 명시하라.

HTTP 헤더에서는 Content-Type: text/html; charset=utf-8 으로 표현할 수 있고, XML에서는 <?xml version="1.0" encoding="UTF-8" ?> 과 같이 표현할 수 있다.

50. [에러 메시지] 정보가 있는 에러 메시지를 제공하는 한편, 에러 메시지에서 관련 정보로 이동할 수 있는 수단도 제공하라.

51. [쿠키] 쿠키를 항상 사용할 수 있다고 가정하지 마라.

52. [캐싱] HTTP 응답에 캐시 정보를 제공하라.

53. [글꼴] 특정 글꼴과 관련한 스타일에 의존하지 마라.

54. [키 입력 최소화] 키 입력 횟수를 최소화하라.

55. [자유 입력 회피] 가능하다면 자유롭게 입력할 수 있는 텍스트는 피하라.

56. [기본값 제공] 가능하다면 미리 선택된 기본값을 제공하라.

57. [기본 입력 모드] 디바이스에서 지원한다면 기본 텍스트 입력 모드, 언어, 입력 형식 등을 제공하라.

58. [탭 순서] 논리적인 순서로 링크, 폼 컨트롤, 객체를 탐색할 수 있어야 한다.

59. [컨트롤 레이블] 모든 폼 컨트롤은 적절하게 레이블을 추가해야하며, 명확히 관련있는 레이블이 있어야 한다.

60. [컨트롤 위치] 레이블의 위치를 조정하여 연관성있는 폼 컨트롤과 관련있어 보이도록 배치하라.

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