어떠한 데이터라도 받기만 하면 비주얼라이제이션하는 데에는 자신있다고 자부해 왔습니다. 그러나 며칠간 고민에 빠져들게 하는 과제가 하나 생겼습니다. 그것은 바로 '/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

Got something to add? You can just leave a comment.

  • JeongHoon Baek JeongHoon Baek

    RT @firejune: Node.JS와 Express를 이용한 디렉터리 파싱 http://t.co/u8gdVf7B

    from twitter | reply

  • 임대리 임대리

    형 저건 contentType에 이미 디렉토리와 파일의 타입을 가지고 있기 때문에
    저 상태에서 계층을 표현하는건 수월하다규...
    오로지 경로만 가지고 있는 문자열에서 폴더를 표현하는것이 골때리는 거였음.
    헌데 그거도 지금은 간단하게 처리할 수 있는 방법이 있긴 함 시간나면 코드
    보여 주겠삼...

    reply edit

  • 파이어준 파이어준

    음.. 상황이 좀 다르긴 하지만, 문자열 만으로 계층 구조를 만들어야하는 것에는 변화가 없는 거라 생각하는데, 암튼 코드 까봐~ 아 참! 이제 임과장이지? 미안해 임대리...

    reply edit

  • 남처리 남처리

    아... 수월하다고 말하는 임과장님 밉습니다. ㅠ_ㅠ
    지난주 트리 데이터 뿌릴때 힘들었는데 ㅠ_ㅠ

    reply edit

  • HBS HBS

    재미있어 보여서 한번 트라이 해봤습니다.
    http://jsfiddle.net/hbsto/HtupT/
    제 경우는 구조 관련 데이터(json)을 받은 클라이언트라 가정하고 script 선에서 트리 구조를 만들었습니다.

    reply edit

  • 파이어준 파이어준

    우와~ 완전 간결한데요? HBS님의 코드로 교체해야 겠어요 ^^;

    reply edit

Your Reaction Time!

avatar

captcha