Rev. 2.73

지금까지 많은 사람들이 Open Source Initiative의 MIT 라이센스 페이지를 이용하여 MIT 라이센스를 명시해 왔습니다. 왜냐하면 라이센스 전문을 복사해서 쓰는 번거로움을 피할 수 있었기 때문이죠. 보통 권리자의 이름 등을 기입하고 라이센스 조항은 URL을 참조하는 형식으로 작성했습니다. 그런데, 이 라이센스 페이지가 좀 야리꾸리(?)하다고 판단한 Remy Sharp씨는 간단 명료하게 MIT 라이센스만 명시된 웹사이트를 mit-license.org 도메인에 물려 서비스하기 시작했습니다.

재미있는 것은 사용자 등록을 한 경우 권리자의 이름이 페이지에 표시되며 firejune.mit-license.org과 같은 URL로 접근할 수 있는 개인화 된 페이지를 제공합니다. 사용자 등록방법은 두 가지 입니다. 하나는 curl을 이용하는 방법이고 다른 하나는 GitHub의 mit-license 프로젝트를 포크하여 'users' 디렉토리에 'username.json'파일을 작성하고 Remy Sharp씨에게 pull 요청을 하는 것입니다. 두 가지 방법 중 curl을 이용하는 방법을 추천합니다. Remy Sharp씨가 손수 등록해 줄 때 까지 기다릴 필요가 없으며, 실행 즉시 자동으로 추가 및 생성되기 때문이죠. curl 실행 방법은 다음과 같습니다.

curl -d'{ "copyright": "Joon Kyung, http://firejune.com/" }' http://firejune.mit-license.org

curl에서 JSON을 직접 보내는 방식으로, 사용할 주소 정보를 서브-도메인에 작성해야 한다는 사실을 꼭 기억하세요. JSON에는 copyright(필수), url, email, format, version, theme 항목을 작성할 수 있으며 예를 들면 다음과 같습니다.

{
  "copyright": "Joon Kyoung, http://firejune.com",
  "url": "http://firejune.com",
  "theme": "flesch"
}

여기서 format 항목은 지정하지 않은 경우 기본으로 HTML을 생성하지만 "txt" 값을 입력하면 plain text로 표시됩니다. 보다 자세한 사용방법은 README.md 문서를 참고 하세요.

Comments

어떠한 데이터라도 받기만 하면 비주얼라이제이션하는 데에는 자신있다고 자부해 왔습니다. 그러나 며칠간 고민에 빠져들게 하는 과제가 하나 생겼습니다. 그것은 바로 '/dir1/dir2/file.name'과 같은 형식으로 반환되는 데이터들에 대한 디렉터리 구조를 표현하는 것이었습니다. 언뜻 보기에는 굉장히 쉬워보였습니다만, 막상 작업에 돌입해 보니 그리 만만한 프로그래밍이 아니구나라는 생각이 들어 그 해결과정을 이곳에 기록합니다. 사실, 예전에도 이와 유사한 고민을 앓았던 적이 있었는데 서버-사이드에서 처리해야 할 문제였으므로 다른 분에게 부탁하고 기억에서 지웠던 일이 고스라니 돌아온 상황이네요.(임대리 난 이렇게 해결했어!)

app.get('/files/:container?', function(req, res) {
  var swift = users[req.session.name]
    , container = req.params.container;

  if (!swift)
    return res.redirect('/login');

  swift.listObjects(container, function(err, result) {
    var objects = serialize(JSON.parse(result.body));
    if (req.xhr)
      res.partial('partial/objects', {
          container: container
        , objects: objects
      });
    else
      res.render('files', {
          layout: 'layout/main'
        , view: 'files'
        , container: container
        , objects:objects
      });
  });
});

위 코드는 Node.JSExpress 프레임웤을 이용하여 진행중인 OpenStack Object Storage(이하 Swift)의 웹 클라이언트 프로젝트에서 라우팅 룰의 일부분입니다. 컨테이너 이름을 URL로 받아 Swift에 질의하고 그 결과를 HTML로 응답하는 일을 합니다. 그리고 xhr요청을 구분하여 문서의 일부만 그릴 것인지 문서 전체를 그릴 것인지를 구분하는 조건이 있습니다. 이 때 Swift는 다음과 같은 JSON형식의 데이터를 돌려줍니다.(지면 관계상 이 처리과정과 무관한 hash, bytes, last_modified 속성들은 생략했습니다.)

[
  {"name": "README.chromium", "content_type": "application/octet-stream"},
  {"name": "buildinf.h", "content_type": "text/x-chdr"},
  {"name": "config", "content_type": "application/directory"},
  {"name": "config/k8", "content_type": "application/directory"},
  {"name": "config/k8/openssl", "content_type": "application/directory"},
  {"name": "config/k8/openssl/opensslconf.h", "content_type": "text/x-chdr"},
  {"name": "config/piii", "content_type": "application/directory"},
  {"name": "config/piii/openssl", "content_type": "application/directory"},
  {"name": "config/piii/openssl/opensslconf-posix.h", "content_type": "text/x-chdr"},
  {"name": "config/piii/openssl/opensslconf-win32.h", "content_type": "text/x-chdr"},
  {"name": "config/piii/openssl/opensslconf.h", "content_type": "text/x-chdr"},
  {"name": "openssl.gyp", "content_type": "application/octet-stream"},
  {"name": "patches", "content_type": "application/directory"},
  {"name": "patches/handshake_cutthrough.patch", "content_type": "text/x-diff"},
  {"name": "patches/missing_stddef.patch", "content_type": "text/x-diff"},
  {"name": "patches/next_proto_neg.patch", "content_type": "text/x-diff"},
  {"name": "patches/posix_c_source.patch", "content_type": "text/x-diff"},
  {"name": "patches/snap_start.patch", "content_type": "text/x-diff"}
]

데이터(오브젝트)들을 잘 살펴보면 "name"속성에는 일반적으로 파일명이 있지만 경로만 있거나 경로명을 포함한 파일명도 있습니다. 그리고 컨텐츠-타입으로 구분할 수 있도록 되어있습니다. 이것은 오브젝트들이 계층구조를 가질수 있게하는 Swift의 Pseudo-Hierarchical Folders/Directories API입니다. 이렇게 수신된 데이터들은 serialize 함수에 의해 최초로 가공됩니다.

function serialize(objects) {
  var folders = []
    , files = [];

  for (var i = 0; i < objects.length; i++) {
    var object = objects[i]
      , depth = object.name.split('/');

    if (object.content_type == 'application/directory' || depth.length > 1) {
      object.pseudo_path = object.name;
      object.name = depth[depth.length - 1];

      for (var j = 0; j < depth.length; j++) {
        object.parent = depth[j - 1] || null;
        if (!folders[j]) folders[j] = {};
        if (!folders[j][depth[j]]) folders[j][depth[j]] = object;
      }
    } else
      files.push(object);
  }

  folders = hierarchy(folders);
  if (!folders.length) folders = [];

  return folders.concat(files);
}

오브젝트들의 반복문에서는 크게 파일과 폴더를 구분합니다. 폴더는 슬래시('/')를 구분자로 나누고 폴더의 깊이를 알아내어 배열을 만듭니다. 이 배열의 값으로는 key-value 형식으로 오브젝트를 담아둡니다. 이 때 오브젝트에는 약간의 변화가 발생하는데 부모(상위 디렉터리)는 누구인지, 그리고 name속성은 디스플레이에서 사용될 값으로 교체하고 새로운 pseudo_path 속성에 실제 name값을 백업합니다. 이것으로 1차 가공을 완료했습니다.

[
  // depth 1
  {
    "config": {name: "config", content_type: "application/directory", pseudo_path: "config", parent: null},
    "patches": {name: "patches", content_type: "application/directory", pseudo_path: "patches", parent: null}
  },
  
  // depth 2
  {
    "k8": {name: "k8", content_type: "application/directory", pseudo_path: "config/k8", parent: "config"},
    "piii": {name: "piii", content_type: "application/directory", pseudo_path: "config/piii", parent: "config"},
    "handshake_cutthrough.patch": {name: "handshake_cutthrough.patch", content_type: "text/x-diff", pseudo_path: "patches/handshake_cutthrough.patch", parent: "patches"},
    "missing_stddef.patch": {name: "missing_stddef.patch", content_type: "text/x-diff", pseudo_path: "patches/missing_stddef.patch", parent: "patches"},
    "next_proto_neg.patch": {name: "next_proto_neg.patch", content_type: "text/x-diff", pseudo_path: "patches/next_proto_neg.patch", parent: "patches"},
    "posix_c_source.patch": {name: "posix_c_source.patch", content_type: "text/x-diff", pseudo_path: "patches/posix_c_source.patch", parent: "patches"},
    "snap_start.patch": {name: "snap_start.patch", content_type: "text/x-diff", pseudo_path: "patches/snap_start.patch", parent: "patches"}
  },
  
  // depth 3
  {
    "openssl": {name: "openssl", content_type: "application/directory", pseudo_path: "config/k8/openssl", parent: "k8"}
  },
  
  // depth 4
  {
    "opensslconf.h": {name: "opensslconf.h", content_type: "text/x-chdr", pseudo_path: "config/k8/openssl/opensslconf.h", parent: "openssl"},
    "opensslconf-posix.h": {name: "opensslconf-posix.h", content_type: "text/x-chdr", pseudo_path: "config/piii/openssl/opensslconf-posix.h", parent: "openssl"},
    "opensslconf-win32.h": {name: "opensslconf-win32.h", content_type: "text/x-chdr", pseudo_path: "config/piii/openssl/opensslconf-win32.h", parent: "openssl"}
  }
]

위와 같이 1차적으로 가공을 마친 폴더 데이터를 hierarchy함수에서 2차 가공을 진행합니다. 2차 가공에서는 배열에 속해있는 오브젝트들을 계층구조로 만드는 일을 수행합니다. 배열의 뒤쪽에서부터 순차적으로 처리하며, 부모(parent)가 누구냐라는 근거로 자식들(childs) 배열을 만들어 냅니다. hierarchy함수는 디렉터리의 깊이 단위로 반복해서 자신을 호출하는 재귀함수이며 마지막 처리시점(부모가 더이상 존재하지 않는 시점)에서 완전이 가공된 데이터를 돌려줍니다.

function hierarchy(arr, childs) {
  var item = arr.pop()
    , parents = {};

  for (var name in item) {
    var parent = item[name].parent || '_root_';
    if (!parents[parent]) parents[parent] = [];
    if (!item[name].childs && childs)
      for (var key in childs)
        if (key == name)
          item[name].childs = childs[key];

    parents[parent].push(item[name]);
    delete item[name].parent;
  }

  if (arr.length) parents = hierarchy(arr, parents);
  return parents['_root_'] || parents;
}

hierarchy함수에 의해 2차적으로 가공된 데이터는 다음과 같습니다. 드디어 궁극적으로 원했던 계층형 데이터 컬렉션이 완성되었습니다. serialize함수는 최종 폴더 데이터와 파일 데이터와 병합하여 돌려줍니다.

[{
  name: "config",
  content_type: "application/directory",
  pseudo_path: "config"
  childs: [{
    name: "k8",
    content_type: "application/directory",
    pseudo_path: "config/k8"
    childs: [{
      name: "openssl",
      content_type: "application/directory",
      pseudo_path: "config/k8/openssl"
      childs: [{
        name: "opensslconf-win32.h",
        content_type: "text/x-chdr",
        pseudo_path: "config/piii/openssl/opensslconf-win32.h"
      }, {
        name: "opensslconf-posix.h",
        content_type: "text/x-chdr",
        pseudo_path: "config/piii/openssl/opensslconf-posix.h"
      }, {
        name: "opensslconf.h",
        content_type: "text/x-chdr",
        pseudo_path: "config/k8/openssl/opensslconf.h"
      }]
    }]
  }, {
    name: "piii",
    content_type: "application/directory",
    pseudo_path: "config/piii"
  }]
}, {
  name: "patches",
  content_type: "application/directory",
  pseudo_path: "patches",
  childs: [{
    name: "snap_start.patch",
    content_type: "text/x-diff",
    pseudo_path: "patches/snap_start.patch"
  }, {
    name: "posix_c_source.patch",
    content_type: "text/x-diff",
    pseudo_path: "patches/posix_c_source.patch"
  }, {
    name: "next_proto_neg.patch",
    content_type: "text/x-diff",
    pseudo_path: "patches/next_proto_neg.patch"
  }, {
    name: "missing_stddef.patch",
    content_type: "text/x-diff",
    pseudo_path: "patches/missing_stddef.patch"
  }, {
    name: "handshake_cutthrough.patch",
    content_type: "text/x-diff",
    pseudo_path: "patches/handshake_cutthrough.patch"
  }]
}]

이제 이 데이터를 뷰-파셜에 그냥 컬렉션으로 전달해 주기만 하면 됩니다. 다음은 컨텐츠 영역에 해당하는 내용을 담은 뷰 파일입니다. 참고로 템프릿 엔진으로는 EJS가 사용되고 있습니다.

<ul class="section objects" id="objects">
  <li class="header">
    <span class="name active"><a href="#refresh">File Name <em>asc</em></a></span>
    <span class="when"><a href="#refresh">Modified <em>asc</em></a></span>
    <span class="size"><a href="#refresh">Size <em>asc</em></a></span>
  </li>

  <%- partial('partial/object', objects) %>

  <li class="actions">
    <a class="button upload">Upload</a>
    <em class="message">Drop files here to upload</em>
    <a class="button action" href="#refresh">Refresh</a>
    <div class="progress" style="display: none;">
      <span id="bar" class="bar" style="width: 0%;"></span>
      <span id="state" class="state"></span>
    </div>
  </li>
</ul>

그리고 컬렉션 단위로 그려질 뷰 파셜입니다. 여기에서 하나의 조문이 있는데 만약에 자식이 있다면 뷰 자신에게 자신을 다시 파셜하라는 명령을 수행합니다.

<li class="object">
  <p>
    <span class="name">
      <a href="<%= object.content_type == 'application/directory' ? '#open' : '/store/' + container + '/' + (object.pseudo_path || object.name) %>" target="_blank"><%- object.name %></a>
    </span>
    <span class="when"><%- object.when %></span>
    <span class="size"><%- object.size %></span>
  </p>
  <% if (object.childs) { %>
  <ul class="tree">
    <%- partial('partial/object', object.childs) %>
  </ul>
  <% } %>
</li>

이리하여 얻어진 결과물은 다음과 같습니다.

tree.png

Comments

애플 홈의 아이폰 4S 제폼 웹페이지에서 메인 비주얼를 장식하는 슬라이드는 총 6개의 애니메이션 시퀀스로 구분된 제품 특징들로 구성되어 있습니다. 이 제품 슬라이드를 구현하는 CSS 애니메이션과 자바스크립트 코드를 사례로 효과적인 애니메이션을 구현하는 방법에 대하여 공부해 봅시다.

아래의 데모는 작동원리를 이해하기 위해 약 20% 수준으로 축소한 것이며, 브라우저의 뷰포트를 확인하기 위한 가상 요소(browser)가 포함되어 있습니다. 슬라이드 요소들이 커다란 하나의 맵(phone-stage)으로 구성되어 있고 아이폰 검정색(bphone)만 별도의 레이어로 취급되고 있습니다. 그리고 슬라이드 6번은 1번 슬라이드와 절묘하게 교차하면서 반복된다는 사실을 알 수 있습니다. 그러나 실제 버전에는 각 슬라이드에 사용된 이미지와 텍스트가 개별 요소들로 구분되어 보다 많은 구성 요소들로 이루어져 있으며, 링크와 텍스트 애니메이션 정보 등의 처리까지 포함하고 있지만 이 데모에서는 그 것을 통합하거나 생략한 것입니다.

데모 출처: http://johnbhall.com/iphone-4s/ via John B. Hall

슬라이드 기능을 만드는 자바스크립트 핵심 함수는 SlideSequencer 클래스입니다. Sequencer는 재생/정지 상태를 관장하고 정해진 시간에 다음 시퀀스를 재생합니다. Slide는 애니메이션 대상 요소와 이벤트 핸들러, CSS transformtransition 속성에 사용될 translate, rotate 등의 값들로 구성된 인스턴스를 생산합니다. 대상이 된 HTML 요소에 스타일(style) 속성이 인라인으로 갱신되면서 애니메이션이 발생하는 것입니다. 파이어버그 또는 돔인스펙터(개발자 도구)를 열고 "phone-stage"의 스타일 속성을 감시해 보세요. 위 데모에 사용된 코드는 다음과 같습니다.

  var phoneStage = $("phone-stage"),
      bphone = $("bphone");
  
  var q = new Slide([{
      el: phoneStage,
      x: -325,
      y: -234,
      r: -30
    }, {
      el: bphone,
      x: 0,
      y: 0,
      r: -30
    }]),
  
  t = new Slide([{
      el: phoneStage,
      x: -264,
      y: -316,
      r: -0
    }, {
      el: bphone,
      x: -300,
      y: -300,
      r: -30
    }]),
  
  ...
  
  b = new Sequencer([q, t, z, u, j, f, v]);
  b.play();

즉, 이 슬라이드에서 발생하는 애니메이션은 전부 CSS로 처리하지만 이것을 제어하는 것은 자바스크립트입니다. 자바스크립트는 시퀀스를 구성하는 transition과 transform 속성을 만들어 해당 요소의 인라인 스타일 속성에 대입해 주는 거죠. 이 간단한 원리로 얻어지는 가치는 무엇일까요? 자, 우리는 일반적으로 애니메이션을 처리할 때 jQuery에서 제공하는 .animate 또는 Prototype & Scriptaculous의 .morph와 같은 자바스크립트 기반 애니메이션 함수를 사용해 왔습니다. 왜냐하면 사용자의 미세한 행동들 까지도 감지하여 화면에 응답하는 것을 만들어야 했기 때문입니다. 그리고 이 애니메이션 함수들은 Frame-by-Frame으로 연산하기 때문에 생각보다 많은 처리 비용이 든다는 사실을 여러분은 잘 알고 있을 것입니다.

CSS로 처리되는 애니메이션을 자바스크립트로 제어한다는 것은 곧 화면상의 상호작용을 직접 제어할 수 있게 되는 것을 의미합니다. 이것은 매우 흥미롭습니다. .animate.morph를 사용하는 대신에 CSS 속성을 활용하여 브라우저의 자원을 효율적으로 분산 처리하는 효과적인 애니메이션 구현 기법입니다. 이렇게 확보된 자원으로 더욱 복잡한 애플리케이션을 구상할 수 있고, 아름다운 그래픽 애니메이션도 지원 받을 수 있습니다. 특히 CSS transitions는 하드웨어 가속을 지원하기 때문에 자바스크립트 연산으로는 도저히 답이 안나오는 여러가지 사치성(호화로운) 애니메이션 효과들을 제품에 적용여 네이티브 애플리케이션에 하이킥을 날리는 레알 웹애플리케이션을 만들어 낼 수 있습니다.

Comments