IndexedDB, Redis, Node.js로 노트 앱 만들기

이 글은 Jennifer Fong-Adwent과 Robert Nyman의 Building a Notes App with IndexedDB, Redis and Node.js의 한국어 번역본입니다.

이번 글에서는 기본 노트 앱을 만들어 볼 것입니다. 지금 만들어 볼 노트 앱은 사용자가 오프라인일 때 로컬에 데이터를 저장하고 온라인일 경우 원격으로 로컬과 동기화할 수 있는 앱입니다.

notes app sample

서버사이드에 Redis 사용하기

Redis에 데이터를 기록할 때는 MySQL이나 PostgreSQL같은 관계형 데이터베이스를 다룰 때와는 다릅니다. IndexedDB같은 키-값 구조를 가지고 작업을 해야합니다. 그렇다면 노트 앱을 만들기 위해 키-값만을 가지고 무엇을 해야할까요? 우선 각 노트를 가리키는 고유한 id 값과 노트 메타데이터의 해시 값이 필요합니다. 이번 예제에서 메타데이터는 고유 id, timestamp, 텍스트 내용으로 이루어져 있습니다.

아래의 소스코드는 Node에서 Redis로 id를 생성한 뒤 노트의 메타데이터를 저장하는 방법입니다.

// Let's create a unique id for the new note.
client.incr('notes:counter', function (err, id) {

...

    // All note ids are referenced by the user's email and id.
    var keyName = 'notes:' + req.session.email + ':' + id;
    var timestamp = req.body.timestamp || Math.round(Date.now() / 1000);

    // Add the new id to the user's list of note ids.
    client.lpush('notes:' + req.session.email, keyName);

    // Add the new note to a hash.
    client.hmset(keyName, {
      id: id,
      timestamp: timestamp,
      text: finalText
    });

...

});

우리는 이 소스코드를 가지고 서버사이드 상에서 모든 노트에 대해 다음과 같은 주요 규칙들을 확인할 수 있습니다:

  1. notes:counter 는 1로 시작하며 모든 고유한 id를 포함합니다.
  2. notes:<email> 사용자가 가지고 있는 모든 노트의 id를 포함합니다. 이것은 메타데이터를 가져오기 위해 사용자의 모든 노트를 조회할 때 참조하는 리스트입니다.
  3. notes:<email>:<note id> 는 노드의 메타데이터를 포함합니다. 사용자의 이메일 주소는 해당 노트가 올바른 사용자에게 참조되었는지 확인하는 용도로 쓰입니다. 사용자가 노트를 지우면, 우리는 로그인 한 사용자가 노트를 소유한 사용자가 맞는지 확인할 필요가 있습니다. 그렇기 때문에 사용자로 하여금 자신이 소유하지 않은 노트를 지우지 못하도록 합니다.

클라이언트 사이드에 IndexedDB 추가하기

IndexedDB는 localStorage 보다 더 많은 소스코드가 필요합니다. 하지만 비동기적이기 때문에, 이번 앱에 더 나은 선택일 수 있습니다. 그 이유는 다음과 같습니다:

  1. 페이지가 모든 엘리먼트들을 렌더링하기 전에 모든 노트가 동기화될 때까지 기다릴 필요가 없습니다. 한 번 상상해보세요. 수천개의 노트가 있는데  페이지의 어떤 것도 나타나기 전에 그 모든 노트를 조회할 때까지 기다릴 수 있나요?
  2. 노트 객체를 객체로 저장할 필요가 없습니다 – 여러분은 노트를 문자열로 먼저 바꿔야 하는데 이 것은 가져올 때 다시 객체로 변환해야 하는 것을 의미합니다. 그래서  { id: 1, text: 'my note text', timestamp: 1367847727 } 같은 것들은 localStorage에 문자열로 변환되어 저장이 되고, 뒤에 다시 객체로 파싱됩니다. 수천개의 노트에 대해 이 작업을 하는 것을 다시 한 번 상상해보세요.

둘 다 완벽한 사용자 경험에 맞지 않습니다 – 하지만 IndexedDB의 비동기적 특징과 localStorage API의 쉬운 사용법 모두 쓰길 원하면 어떻게 해야할까요? 우리는 Gaia의 async_storage.js 파일을 가지고 두 가지 모두 충족시킬 수 있습니다.

오프라인일 경우, 서버사이드와 비슷한 두가지 작업을 해야합니다:

  1. 노트의 고유한 id를 저장하고 id들의 배열에 저장합니다. Redis에서 만들어진 서버사이드 id를 참조할 수 없기 때문에, timestamp를 사용할 것입니다.
  2. 노트 메타데이터의 로컬 버젼을 저장합니다.
var data = {
  content: rendered,
  timestamp: id,
  text: content
};

asyncStorage.setItem(LOCAL_IDS, this.localIds, function () {
  asyncStorage.setItem(LOCAL_NOTE + id, data, function () {
    ...
  });
});

IndexedDB 키 구조는 Redis와 매우 유사합니다. 규칙은 다음과 같습니다:

  1. 모든 로컬 id는 localNoteIds 배열에 저장됩니다.
  2. 모든 로컬 노트 객체는 note:local:<id>에 저장됩니다.
  3. 모든 동기화된 id는 noteIds 배열에 저장됩니다.
  4. 모든 동기화된 노트 객체는 note:<id>에 저장됩니다.
  5. 로컬 노트는 고유한 id를 위해 timestamp를 사용하며 Redis가 데이터를 저장한 뒤에는 유효한 서버사이드 id로 변환됩니다.

그리고 온라인 상태가 되면, 로컬 노트들을 업로드하여 클라이언트사이드에 다시 원격 노트들을 저장하고 로컬에 있는 것들을 삭제합니다.

클라이언트사이드에서 note.js 실행하기

페이지를 새로고침할 때는 반드시 server와 동기화를 시도해야 합니다. 오프라인일 경우, 노트에 표시하여 로컬에서 가지고 있는 것들을 유지합니다.

/**
 * Get all local and remote notes.
 * If online, sync local and server notes; otherwise load whatever
 * IndexedDB has.
 */
asyncStorage.getItem('noteIds', function (rNoteIds) {
  note.remoteIds = rNoteIds || [];

  asyncStorage.getItem('localNoteIds', function (noteIds) {
    note.localIds = noteIds || [];

    $.get('/notes', function (data) {
      note.syncLocal();
      note.syncServer(data);

    }).fail(function (data) {
      note.offline = true;
      note.load('localNoteIds', 'note:local:');
      note.load('noteIds', 'note:');
    });
  });
});

거의 다 왔습니다!

위의 소스코드는 노트 앱의 로컬 저장소와 원격 동기화를 지원하는 기본적인 CRD를 제공하고 있지만 아직 끝난건 아닙니다.

사파리에서는 IndexedDB가 지원되지 않으며 여전히 WebSQL을 사용하고 있습니다. 즉, 우리가 짠 IndexedDB 코드가 전혀 동작하지 않겠죠. 이러한 크로스-브라우저 호환성 문제를 해결하기 위해 WebSQL만을 지원하는 브라우저를 위한 polyfill을 추가해야 합니다. 이 것을 소스 코드 전 부분에 추가해서 IndexedDB가 동작하도록 해주세요.

최종 프로젝트

http://notes.generalgoods.net에서 앱을 테스트해볼 수 있습니다.

소스 코드

Github 저장소에서 자유롭게 소스 코드를 확인하실 수 있습니다.

작성자: Hoony Chang

Web Programmer

Hoony Chang가 작성한 문서들…


댓글이 없습니다.

댓글 쓰기