Rev. 2.73

CodeMirror는 웹 브라우저에서 코드 편집 기능을 제공할 수 있게 하는 자바스크립트 컴포넌트입니다. 자바스크립트, CSS, HTML은 물론 C, Java, PHP, Python, Ruby 등 50여 종의 언어 모드(Mode)를 지원하며, 문법 강조, 단축키, 자동포맷, 코드 폴딩, 테마, 검색 및 바꾸기 등의 유틸리티를 제공합니다. JSBin, Google Apps Script, Zen Coding등의 사이트에서 CodeMirror를 이용하여 코드 편집 기능을 제공하고 있습니다. JSBin을 포크 해서 고쳐 사용 중인 FireJSBin(코드네임)의 라이브러리를 업데이트하려다가 CodeMirror 3의 정식 버전이 최근에 릴리즈되었다는 사실을 알게 되었습니다. 그래서 버전 2.x에 비해 어떤 점들이 달라지고 어떻게 업그레이드하는지 살펴보도록 하겠습니다.

CodeMirror 버전 3은 2.x의 API에서 크게 벗어나지 않았습니다. 단순한 방법으로 CodeMirror를 이용한 사이트는 별다른 문제 없이 업그레이드할 수 있습니다. 애석하게도 버전 3부터는 인터넷 익스플로러 7을 완전히 지원하는 것을 포기합니다. IE7을 지원하는 코드들이 효율성을 떨어트리기 때문일까요? 아무튼, 그렇습니다.

DOM structure

편집기 내부에 사용되는 새로운 스크롤링 모델을 구현하기 위해 꽤 많은 DOM 구조가 변경되었습니다. 그래서 codemirror.css를 변경해서 사용하는 경우에는 문제를 일으킬 가능성이 큽니다. 편집기의 높이를 처리하는 요소가 스크롤러 요소(CSS class CodeMirror-scroll)에서 래퍼(Wrapper) 요소(CSS class CodeMirror)로 변경되었습니다. 그 외 다른 요소들이 이동되거나 제거되었고 추가되었습니다. 만약 편집기 내부의 DOM 요소를 제어하는 코드를 작성했다면 버전 3에서는 그 코드들을 다시 테스트해야 할 것입니다. CodeMirror 메뉴얼의 스타일링 섹션에서 더 많은 정보를 얻을 수 있습니다.

Gutter model

CodeMirror 2.x에서는 싱글 거터(Gutter, 라인 넘버가 출력되는 영역)만 제공되어 라인을 마킹하거나 할 때 좁은 공간에 줄 번호를 비롯한 추가적인 정보가 공존해야 했었지만, 버전 3부터는 거터를 클래스네임의 배열로 지정함으로써 복수로 사용할 수 있게 되었습니다. setGutterMarker, clearGutter 등의 추가적인 메서드를 이용하여 거터단위 마커를 추가 및 제거할 수 있고, HTML 스니펫이 아닌 DOM 노드로 지정됩니다.

수평으로 스크롤할 때 거터가 들쭉날쭉하는 문제가 사라졌으며, fixedGutter 옵션은 삭제되었습니다.

<style>
  /* Define a gutter style */
  .note-gutter { width: 3em; background: cyan; }
</style>
<script>
  // Create an instance with two gutters -- line numbers and notes
  var cm = new CodeMirror(document.body, {
    gutters: ["note-gutter", "CodeMirror-linenumbers"],
    lineNumbers: true
  });
  // Add a note to line 0
  cm.setGutterMarker(0, "note-gutter", document.createTextNode("hi"));
</script>

Event handling

onXYZ 옵션이 대부분 삭제되었습니다. 같은 효과로 이벤트 유형을 식별하는 문자열에서 입수할 수 있습니다. 여러 개의 핸들러를 하나의 이벤트에 할당합니다. 이러한 방식으로 라인 핸들러는 더 쉽게 이벤트를 받습니다. onKeyEventonDragEvent 옵션은 이벤트 핸들러를 후크하는 방식으로 작동하며 이전과 마찬가지로 사용할 수 있습니다.

cm.on("change", function(cm, change) {
  console.log("something changed! (" + change.origin + ")");
});

markText method arguments

markText 메서드는 이제 CSS 클래스명을 인자로 받지 않습니다. 이것은 선택 옵션이며, 옵션 개체를 통하여 전달할 수 있습니다.

// Style first ten lines, and forbid the cursor from entering them
cm.markText({line: 0, ch: 0}, {line: 10, ch: 0}, {
  className: "magic-text",
  inclusiveLeft: true,
  atomic: true
});

Line folding

라인을 숨기는 인터페이스가 제거되었습니다. markText를 이용하여 지금보다 더 유연하고 강력한 방법으로 동일한 작업을 수행하는 데 사용할 수 있습니다. 폴딩 스크립트는 새로운 인터페이스를 사용되도록 견고하게 업데이트되었습니다.

// Fold a range, replacing it with the text "??"
var range = cm.markText({line: 4, ch: 2}, {line: 8, ch: 1}, {
  replacedWith: document.createTextNode("??"),
  // Auto-unfold when cursor moves into the range
  clearOnEnter: true
});
// Get notified when auto-unfolding
CodeMirror.on(range, "clear", function() {
  console.log("boom");
});

Line CSS classes

setLineClass 메서드는 addLineClassremoveLineClass으로 대체되었습니다.

var marked = cm.addLineClass(10, "background", "highlighted-line");
setTimeout(function() {
  cm.removeLineClass(marked, "background", "highlighted-line");
});

Position Properties

모든 포지션 메서드는 화면에 표시하는 위치 개체를 반환합니다. {left, top, bottom, right} 속성을 항상 사용할 수 있으며, 버전 2.x에서 사용하던 {x, y, yBot} 속성을 사용하고 있다면 수정해야 합니다. 포지션 속성을 반환하는 메서드는 cursorCoords, charCoords, coordsChar 그리고 getScrollInfo 입니다.

Bracket matching no longer in core

matchBrackets 옵션은 이제 코어 에디터에 정의되지 않으며 lib/util/matchbrackets.js를 로드합니다.

Mode management

정의된 모드(Mode)의 목록을 반환하던 CodeMirror.listModesCodeMirror.listMIMEs 함수는 이제 없어졌습니다. 대신 CodeMirror.modesCodeMirror.mimeModes로 간단하게 확인할 수 있습니다.

New features

버전 3으로 업그레이드하면 다음과 같은 이점이 있습니다.

  • 양 방향(Bi-directional) 텍스트 지원, 아랍어 또는 히브리어 텍스트를 편집할 수 있습니다.
  • 임의의 행 높이 처리, 에디터 내부에서 높이가 서로다른 글꼴이 사용되는 경우 이제 우아하게 처리합니다.
  • 인라인-위젯 지원, 이 데모문서를 참고하세요.
  • CodeMirror.defineOption으로 사용자 정의 옵션을 지정할 수 있습니다.

이상으로 CodeMirror 버전 3으로의 업그래이드 방법에 대하여 알아보았습니다. 스크롤이 수평으로 이동할때 거터가 들락날락하는 현상이 수정된 거 빼고는 그리 업그레이드의 필요성을 못느끼겠네요. 아랍어를 다룰것도 아니고; 이 기존 2.x 버전은 버그를 수정하는 수준으로 업데이트가 이루어 진다고 하니, 너무 성급하게 3으로 갈아탈 필요는 없어 보입니다.

Comments

Google Analytics에서 제공하는 기본 트래킹 코드는 정적인 페이지에서 사용하기 적합하며 Ajax나 history. pushState를 이용하여 컨텐츠를 갱신하는 동적인 사이트는 필요한 경우 추가적인 작업을 해야 합니다. Analytics는 페이지의 이동 경로와 이벤트 등을 추적할 수 있는 자바스크립트로 작성된 오픈 API를 제공합니다. API를 이용하는 방법은 크게 두 가지로 구분할 수 있는데 _gaq.push로 호출할 메서드와 값을 전달하는 방법과 _gat._getTracker에서 반환하는 인스턴트의 _trackPageview 메서드를 호출하는 방법입니다. 이 API를 Ajax에 응용한 예제는 다음과 같습니다.

// _gaq.push
$.ajax({
  url: "/some.html",
  context: document.body,
  success: function(){
    _gaq.push(['_trackPageview', '/some.html']);
  }
});

// or

// _gat._getTracker
var pageTracker = _gat._getTracker('UA-XXXXX-X');
$.ajax({
  url: "/some.html",
  context: document.body,
  success: function(){
    pageTracker._trackPageview('/some.html');
  }
});

두 방법 모두 같은 결과를 보입니다. 자바스크립트를 이용하여 트래킹하는 경우에 주의해야 할 점 역시 두가지 입니다. _trackPageview는 자동으로 호스트 네임을 걸러주지 않기 때문에 인자로 입력되는 주소는 호스트 정보가 빠진 경로만을 입력해야 합니다. 그리고 페이지 타이틀을 갱신해야 합니다. 그렇지 않으면 Analytics의 페이지 제목 분석기능을 제대로 활용할 수 없게 됩니다. 예를 들어 첫 페이지의 타이틀이 'firejune.com'이면 비동기로 갱신되어 Analytics에 기록된 페이지들이 모두 'firejune.com'이란 타이틀을 가지게 되기 때문이죠. '_trackPageview' 함수는 단순히 document.title의 값을 기록하죠. 다음은 간단한 실용 예를 작성한 것입니다.

var pageTracker = _gat._getTracker('UA-XXXXX-X');
function trackPageview(url, title) {
    url = url.replace(location.protocol + '//' + location.host, '');
    document.title = document.title.split(' : ')[0] + (title && ' : ' + title || '');
    pageTracker._trackPageview(url);
}

function updatePage(el) {
  $.ajax({
    url: el.href,
    context: document.body,
    success: function(){
      trackPageview(el.href, el.innerHTML);
    }
  });
  return false;
}
<a href="/some.html" onclick="return updatePage(this);">Some HTML</a>

최근 작업 중인 벨록스 프로젝트는 애플리케이션 레이아웃이어서 패널과 다이얼로그를 이용하여 컨텐츠를 갱신하기 때문에 다른 방식으로 사용자의 행동을 추적해야 할 필요가 있었습니다. 그래서 속성 편집 패널을 열면, 기존 document.title에 추가로 컨트롤러/액션(' : /files/edit') 이름을 더하여 Analytics에 기록한 후 원래대로 돌리는 방식으로 데이터를 쌓음으로써 사용자의 이동 경로를 조금 더 세밀하게 파악할 수 있었습니다.

Comments

MongoDB의 ObjectID는 12바이트짜리 BSON타입 UUID를 사용합니다. 4-byte timestamp, 3-byte machine identifier, 2-byte process id, 그리고 3-byte counter로 구성되어 합이 12바이트인 것입니다. 데이터를 쓰기도 전에 ID를 생성하며 ID만으로 타임스탬프를를 뽑아낼 수도 있지요. 그런데 이 ID를 문자로 반환하면 무려 24자 길이의 HEX 스트링이 "50812452a46e98744d000003" 요렇게 튀어나옵니다. 이 ID를 각종 파라메터나 URL에 사용하고 있는데 너무 길어서 좀 예쁘게 줄일 방법이 없나 싶어 찾아봤더니 Base64 인코딩을 이용하여 "UIjDaeXm18RLAAAD" 처럼 반 토막으로 축소할 수 있는 방법이 있어 소개합니다.

module.exports = {
  _encodeBase64ID: function(id) {
    return
      id && new Buffer(id.toString(), 'hex')
        .toString('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_') || id;
  },
  _decodeBase64ID: function(id) {
    return
      id && new Buffer(id
        .replace(/-/g, '+')
        .replace(/_/g, '/')
        , 'base64').toString('hex') || id;
  },
  ...
}

ExpressJS에서 위 두 함수를 이용하여 ObjectID를 Base64ID로 인코딩/디코딩 할 수 있습니다. 일단은 Helper 모듈로 만들어 가시적으로 노출되는 라우트와 컨트롤러에서만 호출하는 식으로 사용하고 있는데 포스트를 작성하면서 곰곰이 생각해 보니, 익스프레스용 미들웨어로 작성하기에 아주 적합하다는 생각이 드는군요. 라우팅 정보에서 req.params 키를 추출하여 값을 치환해 주면 되는 간단한 일이잖아요. 말이 나온 김에 지금 작성해 보겠습니다.

/*!
 * Middleware - ObjectID parsor:
 * Joon Kyoung(@firejune)
 * Copyright(c) 2012 Spark & Associates Inc.
 * MIT Licensed
 */

exports.objectIDParser = function() {
  var parse = require('url').parse;

  /**
   * Attempt to match the given params to one of the routes.
   */
  function match(req, routes) {
    var params = {}
      , method = req.method
      , captures
      , i = 0;
  
    if ('HEAD' == method) method = 'GET';
    if (routes = routes[method.toLowerCase()]) {
      var pathname = parse(req.url).pathname;
      for (var len = routes.length; i < len; ++i) {
        var route = routes[i]
          , keys = route.keys
          , path = route.regexp
          , params = [];

        if (captures = path.exec(pathname)) {
          for (var j = 1, len = captures.length; j < len; ++j) {
            var key = keys[j - 1]
              , val = typeof captures[j] === 'string'
                ? decodeURIComponent(captures[j])
                : captures[j];

            if (key) {
              params[key.name] = val;
            } else {
              params.push(val);
            }
          }
          return params;
        }
      }
    }
    return params;
  }

  /**
   *  Parse Base64ID to ObjectID in request params,
   *  providing the parsed object as `params.uuid`.
   */
  return function objectIDParser(req, res, next) {
    if (req._body) return next();

    var routes = req.app.routes.routes
      , params = match(req, routes);

    // uuid in params
    if (!params.uuid) return next();      

    // flag as parsed
    req._body = true;

    // current uuid
    if (24 <= params.uuid.length) {
      console.warn('Deprecated parameter type ObjectID is now using base64ID');
    } else {
      // decode to hex
      params.uuid = new Buffer(params.uuid.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('hex');
      Object.defineProperty(req, "params", {value: params, writable: false});
    }

    next();
  }
};

생각처럼 쉬운 일이 아니었습니다. 몇 시간을 삽질했어요. req.url 파싱해서 라우트 룰을 모두 뒤져 찾아내고 거기서 찾은 정규식을 또다시 req.url에 대입하여 req.params 값을 뽑아내야 했어요. req.app.routes에는 이미 req.params 값들이 할당되어 있었지만 업데이트되는 시점은 app.route가 호출되는 가장 마지막이기 때문에 미들웨어에서 찾을 수 있는 정보는 이전 라우트에서 사용했던 거였죠. 또 다른 문제는 req.params 값도 전달이 안 된다는 거에요. 익스프레스가 app.route를 처리되면서 리플레이스해 버립니다.

app.configure('development', function() {
  app.use(express.static(process.cwd() + '/public', {maxAge: 86400000}));
  app.use(middleware.objectIDParser());
  app.use(middleware.octetStream({tempDir: process.cwd() + '/temp/'}));
  app.use(express.bodyParser());
  app.use(express.cookieParser('fire*une'));
  app.use(express.session({secret: 'fire*une'}));
  app.use(express.methodOverride());
  app.use(app.router);
});

일단, 익스프레스에서 라우트를 비교하는 로직을 가져올 필요가 있어서 소스코드를 몽창 뒤져 match라는 함수를 발견하고 땡겨와 약간 수정했습니다. 그리고 아까 배운 Object.defineProperty로 속성을 변경할 수 없게 만들어버렸더니 잘 전달 되더라고요! 단, 주의할 점은 익스프레스가 req.params 속성을 변경 할 수 없는 상태가 돼 버리므로 이 단계에서 완성해야 합니다. 자~ 이제 라우팅 룰에 ":uuid"로 키만 설정하면 미들웨어에 의해 올바른 ObjectID로 자동 치환되어 컨트롤러에 전달됩니다. 아, 왠지 뿌듯하네요! thevelox.com에 바로 적용했습니다.

Comments