Rev. 2.73

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

신경 안 쓰는 사이에 거의 모든 브라우저가 ECMAScript 5를 지원하게 되는 황금기를 맞는군요. NodeJS에서 다른 사람이 짠 코드를 참고할 때 종종 보이던 ES5의 새 기능을 눈여겨 두었다가 드문드문 써먹곤 했었는데 이제 정말로 깨우치고 사용해야 할 시기라는 생각이 듭니다. 다음은 MDN에서 퍼온 자바스크립트 1.8에서 추가된 기능과 설명입니다.

/* Object */
Object.create // Creates a new object with the specified prototype object and properties.
Object.defineProperty // Adds the named property described by a given descriptor to an object.
Object.defineProperties // Adds the named properties described by the given descriptors to an object.
Object.getPrototypeOf // Returns a property descriptor for a named property on an object. 
Object.keys // Returns an array of all enumerable properties on an object.
Object.preventExtensions // Prevents any extensions of an object.
Object.seal // Prevents other code from deleting properties of an object.
Object.isSealed // Determine if an object is sealed.
Object.freeze // Freezes an object: other code can't delete or change any properties.
Object.isFrozen // Determine if an object was frozen. 
Object.isExtensible // Determine if extending of an object is allowed.
Object.getOwnPropertyDescriptor //Returns a property descriptor for an own property of a given object.
Object.getOwnPropertyNames // Returns an array of all enumerable and non-enumerable properties on an object.

/* Array */
Array.isArray // Checks if a variable is an array. 
Array.prototype.indexOf // Returns the first (least) index of an element within the array equal to the specified value, or -1 if none is found.
Array.prototype.lastIndexOf // Returns the last (greatest) index of an element within the array equal to the specified value, or -1 if none is found.
Array.prototype.every // Returns true if every element in this array satisfies the provided testing function.
Array.prototype.some // Returns true if at least one element in this array satisfies the provided testing function.
Array.prototype.forEach // Calls a function for each element in the array.
Array.prototype.map // Creates a new array with the results of calling a provided function on every element in this array.
Array.prototype.filter // Creates a new array with all of the elements of this array for which the provided filtering function returns true.
Array.prototype.reduce // Apply a function simultaneously against two values of the array (from left-to-right) as to reduce it to a single value.
Array.prototype.reduceRight // Apply a function simultaneously against two values of the array (from right-to-left) as to reduce it to a single value.

/* More */
Function.prototype.bind // Creates a new function that, when called, itself calls this function in the context provided 
String.prototype.trim // Removes whitespace from both ends of the string.
Date.prototype.toJSON // Returns a string encapsulating the Date object in JSON format.
Date.prototype.toISOString // JavaScript provides a direct way to convert a date object into a string in ISO format.
Date.now // Returns the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC.
"use strict" // strict mode support

ObjectArray.prototype에 새로운 기능들이 대거 추가되었습니다. 특히, Object.keys는 정말 유용하게 사용될 거 같습니다. 인자로 받은 options을 default options과 머지할 때 아주 유용하겠네요. 자바스크립트는 배열과 관련된 메서드들이 정말 부실했었는데, indexOf, forEach, map, filter, reduce, some 등 전부 요긴하게 사용할 수 있는 녀석들이 뭉탱이로 들어와 있습니다. 또한 Function.prototype.bind이 추가되어 별다른 트릭없이 선호하는 문법을 구사할 수 있게 되었습니다. 아무리 생각해도 var self = this와 같은 문장은 언어적 결함으로밖에 보이지 않거든요. 다음은 자주 사용될 것 같은 코드 스니펫들입니다.

/* Basic Class */
var book={ title: 'default',  author: 'default' };
var techBook=Object.create(book, {category: {value: 'technology'}});
techBook.category; //=> technology
Object.defineProperty(techBook, "category", {value: 'javascript'});
techBook.category; //=> javascript

/* Mixin Class */ 
function MyClass() {
    SuperClass.call(this);
    OtherSuperClass.call(this);
}

MyClass.prototype = Object.create(SuperClass.prototype); //inherit
mixin(MyClass.prototype, OtherSuperClass.prototype); //mixin

MyClass.prototype.myMethod = function() {
    // do a thing
};

/* Object extend */
Object.keys(options).forEach(function(key){
    form[key] = options[key];
});

/* Getter/Setter */
var myObject = {};
Object.defineProperty( myObject, '_myProp', {
    value:          'myDefault',
    writable:       false,
    enumberable:    false,
    configurable:   true
});

Object.defineProperty( myObject, 'myProp', {
    enumberable: true,
    configurable: true,
    set: function( v ) {
        Object.defineProperty( this, '_myProp', { writable:true } );
        this._myProp = v;
        Object.defineProperty( this, '_myProp', { writable:false } );
    },
    get: function() {
        return this._myProp;
    }
});
 
alert( myObject.myProp );
myObject.myProp = 'myValue';
alert( myObject.myProp );
myObject._myProp = 'teehee, not writable';
alert( myObject._myProp );

/* non-writable */
var o = {}; // Creates a new object
Object.defineProperty(o, "a", { value : 37, writable : false });
o.a = 25; // No error thrown (it would throw in strict mode)
o.a //=> 37

/* Fu*k self */
function LateBloomer() {
    this.petalCount = Math.ceil( Math.random() * 12 ) + 1;
}

// declare bloom after a delay of 1 second
LateBloomer.prototype.bloom = function() {
    window.setTimeout( this.declare.bind( this ), 1000 );
};

LateBloomer.prototype.declare = function() {
    console.log('I am a beautiful flower with ' + this.petalCount + ' petals!');
};

/* ISODateString */
new Date().toISOString() //=> "2012-12-01T20:24:27.681Z"

/* Real trim */
"   _firejune   ".trim() //=> "_firejune"

하위 호환을 위한 폴리필(Polyfill) 라이브러리로는 ES5-Shim이 대세인 듯 합니다.

Comments