Ghost 라우팅 로직 분석

Php에서는 워드프레스, NodeJS에서는 Ghost를 많이 사용한다. 노드로 API 서버를 만드는 일이 많아 지다보니 다른 사람은 어떻게 구현하나 궁금한 점이 많다. 노드로 만든 CMS 오픈소스 중 가장 인기있는 프로젝트인 Ghost의 백엔드 API 부분을 살펴보자.

일단은 express 사용

가장 많이 사용하는 Express 엔진을 사용한다. 여느 CMS툴과 마찬가지로 블로그 페이지가 있고 어드민 페이지가 있는데 Ghost에서는 blogApp, adminApp에 각 각 Express 인스턴스를 할당하여 관리한다.

// /core/server/index.js

function init(optinos) {
  var blogApp = express(),
    adminApp = express()

  // ...
}

blogApp에 설정된 라우팅을 살펴보자.

// core/server/middleware/index.js

function setupMiddleware(blogApp, adminApp) {
  // ...

  blogApp.use(routes.apiBaseUri, routes.api(middleware))

  //...
}

Api 주소는 routes.appBaseUri에 할당되어 있고 "/ghost/api/v0.1" 문자열이다. 이 문자열을 prefix로 하여 나머지 uri를 구성한다. 나머지 세부 라우팅은 /core/server/routes/api.js 파일에서 설정한다. 라우팅 테이블이라고 볼 수 있겠다.

라우팅 핸들러

기존에 코딩할 때는 핸들러에서 받은 response 객체를 바로 사용하여 응답하도록 했다. Ghost에서는 http()라는 데코레이터를 만들어 사용한다.

// /core/server/routes/api.js

router.get("/posts", authenticatePublic, api.http(api.posts.browse))

http() 데코레이터가 라우팅 핸들러의 공통 로직을 작성한 부분인데, (1) 요청 데이터 정리와 (2) 성공시 응답 로직이다.

요청 데이터 정리

express에서 요청데이터는 req.body, req.params, req,query, req.file인데 이를 두개의 객체 object, options로 정리한다.

// /core/server/api/index.js

function http(apiMethod) {
  var object = req.body,
    options = _.extend({}, req.files, req.query, req.params, {
      context: {
        user: req.user && req.user.id ? req.user.id : null,
      },
    })

  // ...

  return apiMethod(object, options)

  // ...
}

이렇게 정리된 object와 options는 실제 핸들러 함수인 apiMethod의 파라메터로 들어간다. 결과적으로 라우팅 로직에는 모든 파라매터가 object, options로 정리되어 들어가게 되는 것이다.

응답

apiMethod()가 정상 동작되면 (다시말해 promise가 resolve되면) json() 함수로 응답한다. 만약 에러가 발생하면 바로 처리하지 않고 next() 로 넘긴다.

// /core/server/api/index.js

function http(apiMethod) {
  // ...

  return apiMethod(object, options).tap(function onSuccess(response) {
      // Add X-Cache-Invalidate, Location, and Content-Disposition headers
      return addHeaders(apiMethod, req, res, (response || {}));
    }).then(function then(response) {
      // Send a properly formatting HTTP response containing the data with correct headers
      res.json(response || {});
    }).catch(function onAPIError(error) {
      // To be handled by the API middleware
      next(error);
});

에러처리

http()에서 발생한 에러는 라우팅 테이블 마지막에 추가한 errorHanlder 미들웨어에서 처리한다.

// /core/server/routes/api.js
// ...

// API Router middleware
router.use(middleware.api.errorHandler)

errorHander 함수에서는 에러 포맷을 일정하게 맞추고 statusCode 값을 설정하여 json() 으로 응답한다. 평소 하던 것과 비슷하다.

// /core/server/errors/index.js

function errorHandler(err, req, res, next) {
  /*jshint unused:false */
  var httpErrors = this.formatHttpErrors(err)
  this.logError(err)
  // Send a properly formatted HTTP response containing the errors
  res.status(httpErrors.statusCode).json({ errors: httpErrors.errors })
}

기타 새로운 점

mysql orm은 sequelize만 있는게 아니다. bookshelf.js를 사용한다.

특징

  • promise interface, callback interface 제공
  • model & collection 패턴 (backborn.js와 유사한)
  • postgreSQL, MySQL, SQLite3 지원
  • 아직은 v0.9.1

Handlebars 뷰 엔진을 사용한다.

  • jade만 있는게 아니다. ejs도 있고 handlbars도 있다.
  • 미티어에서도 이거 사용하더라.

SQLite로도 cms 디비를 만들수 있다니!