우수사례 - Stream Congress에서 실시간 업데이트

Luigi Montanez
Luigi Montanez

소개

HTML5를 사용하면 WebSocketsEventSource를 통해 개발자가 서버와 실시간으로 통신하는 웹 앱을 빌드할 수 있습니다. Stream Congress(Chrome 웹 스토어에서 사용 가능)는 미국 의회의 활동에 대한 실시간 업데이트를 제공합니다. 하원과 상원의 의안 업데이트, 관련 뉴스 업데이트, 하원의원들의 트윗, 기타 소셜 미디어 업데이트를 스트리밍합니다. 이 앱은 의회의 업무를 담기 때문에 하루 종일 열어 두어야 합니다.

WebSockets 시작

WebSockets 사양은 브라우저와 서버 간에 안정적인 양방향 TCP 소켓을 지원한다는 점에서 많은 주목을 받았습니다. TCP 소켓에는 데이터 형식이 적용되지 않습니다. 개발자는 메시지 프로토콜을 자유롭게 정의할 수 있습니다. 실제로는 JSON 객체를 문자열로 전달하는 것이 가장 편리합니다. 실시간 업데이트를 수신 대기하는 클라이언트 측 JavaScript 코드는 깔끔하고 간단합니다.

var liveSocket = new WebSocket("ws://streamcongress.com:8080/live");

liveSocket.onmessage = function (payload) {
  addToStream(JSON.parse(payload.data).reverse());
};

WebSocket의 브라우저 지원은 간단하지만 서버 측 지원은 아직 초기 단계에 있습니다. Node.js의 Socket.IO는 가장 성숙하고 강력한 서버 측 솔루션 중 하나를 제공합니다. Node.js와 같은 이벤트 기반 서버는 WebSocket에 적합합니다. 대체 구현의 경우 Python 개발자는 TwistedTornado를 사용할 수 있고 Ruby 개발자는 EventMachine을 사용할 수 있습니다.

Cramp 소개

Cramp는 EventMachine을 기반으로 실행되는 비동기 Ruby 웹 프레임워크입니다. 이 가이드는 Ruby on Rails 핵심팀의 일원인 Pratik Naik이 작성했습니다. 실시간 웹 앱을 위한 도메인별 언어 (DSL)를 제공하는 Cramp는 Ruby 웹 개발자에게 이상적인 선택입니다. Ruby on Rails에서 컨트롤러를 작성하는 데 익숙한 사용자라면 Cramp의 스타일을 알아볼 수 있습니다.

require "rubygems"
require "bundler"
Bundler.require
require 'cramp'
require 'http_router'
require 'active_support/json'
require 'thin'

Cramp::Websocket.backend = :thin

class LiveSocket < Cramp::Websocket
periodic_timer :check_activities, :every => 15

def check_activities
    @latest_activity ||= nil
    new_activities = find_activities_since(@latest_activity)
    @latest_activity = new_activities.first unless new_activities.empty?
    render new_activities.to_json
end
end

routes = HttpRouter.new do
add('/live').to(LiveSocket)
end
run routes

Cramp는 비블로킹 EventMachine의 맨 위에 있기 때문에 유의해야 할 몇 가지 고려사항이 있습니다.

  • MySQLPlusem-mongo와 같은 비차단 데이터베이스 드라이버를 사용해야 합니다.
  • 이벤트 기반 웹 서버를 사용해야 합니다. ThinRainbows에 대한 지원이 기본 제공됩니다.
  • Cramp 앱은 Stream Congress를 지원하는 기본 Rails 앱과 별도로 실행되어야 하며, 독립적으로 다시 시작되고 모니터링되어야 합니다.

현재 제한사항

WebSockets는 보안 취약점이 공개되었던 2010년 12월 8일에 차질을 겪었습니다. Firefox와 Opera 모두 WebSocket에 대한 브라우저 지원을 삭제했습니다. 순수 JavaScript 폴리필은 없지만 널리 채택된 플래시 대체가 있습니다. 하지만 플래시를 사용하는 것은 바람직하지 않습니다. Chrome과 Safari는 계속해서 WebSockets를 지원하지만, Flash를 사용하지 않고 모든 최신 브라우저를 지원하려면 WebSockets를 대체해야 한다는 것이 분명해졌습니다.

AJAX 폴링으로 롤백

WebSockets에서 '구식' AJAX 폴링으로 다시 전환하기로 결정했습니다. 디스크 및 네트워크 I/O 관점에서 훨씬 덜 효율적이지만 AJAX 폴링은 Stream Congress의 기술적 구현을 간소화했습니다. 가장 중요한 점은 별도의 Cramp 앱이 필요하지 않게 되었습니다. 대신 AJAX 엔드포인트는 Rails 앱에서 제공했습니다. jQuery AJAX 폴링을 지원하도록 클라이언트 측 코드가 수정되었습니다.

var fillStream = function(mostRecentActivity) {
  $.getJSON(requestURL, function(data) {
    addToStream(data.reverse());

    setTimeout(function() {
      fillStream(recentActivities.last());
    }, 15000);
  });
};

AJAX polling, though, is not without its downsides. Relying on the HTTP request/response cycle means that the server sees constant load even when there aren't any new updates. And of course, AJAX polling doesn't take advantage of what HTML5 has to offer.

## EventSource: The right tool for the job

Up to this point, a key factor was ignored about the nature of Stream Congress: the app only needs to stream updates one way, from server to client - downstream. It didn't need to be real-time, upstream client-to-server communication. 

In this sense, WebSockets is overkill for Stream Congress. Server-to-client communication is so common that it's been given a general term: push. In fact, many existing solutions for WebSockets, from the hosted [PusherApp](http://pusherapp.com) to the Rails library [Socky](https://github.com/socky), optimize for push and don't support client-to-server communication at all.

Enter EventSource, also called Server-Sent Events. The specification compares favorably to WebSockets in the context to server to client push:

- A similar, simple JavaScript API on the browser side.
- The open connection is HTTP-based, not dropping to the low level of TCP.
- Automatic reconnection when the connection is closed.

### Going Back to Cramp

In recent months, Cramp has added support for EventSource. The code is very similar to the WebSockets implementation:

```ruby
class LiveEvents < Cramp::Action
self.transport = :sse

periodic_timer :latest, :every => 15

def latest
@latest_activity ||= nil
new_activities = find_activities_since(@latest_activity)
@latest_activity = new_activities.first unless new_activities.empty?
render new_activities.to_json
end
end

routes = HttpRouter.new do
add('/').to(LiveEvents)
end
run routes

EventSource에서 유의해야 할 중요한 문제는 교차 도메인 연결이 허용되지 않는다는 점입니다. 즉, Cramp 앱은 기본 Rails 앱과 동일한 streamcongress.com 도메인에서 제공되어야 합니다. 이는 웹 서버에서 프록시를 사용하여 실행할 수 있습니다. Cramp 앱이 Thin으로 구동되고 포트 8000에서 실행된다고 가정하면 Apache 구성은 다음과 같습니다.

LoadModule  proxy_module             /usr/lib/apache2/modules/mod_proxy.so
LoadModule  proxy_http_module        /usr/lib/apache2/modules/mod_proxy_http.so
LoadModule  proxy_balancer_module    /usr/lib/apache2/modules/mod_proxy_balancer.so

<VirtualHost *:80>
  ServerName streamcongress.com
  DocumentRoot /projects/streamcongress/www/current/public
  RailsEnv production
  RackEnv production

  <Directory /projects/streamcongress/www/current/public>
    Order allow,deny
    Allow from all
    Options -MultiViews
  </Directory>

  <Proxy balancer://thin>
    BalancerMember http://localhost:8000
  </Proxy>

  ProxyPass /live balancer://thin/
  ProxyPassReverse /live balancer://thin/
  ProxyPreserveHost on
</VirtualHost>

이 구성은 streamcongress.com/live에 EventSource 엔드포인트를 설정합니다.

안정적인 폴리필

WebSocket보다 EventSource의 가장 큰 이점 중 하나는 대체가 Flash에 종속되지 않고 완전히 JavaScript 기반이라는 점입니다. Remy Sharp의 폴리필은 EventSource를 기본적으로 지원하지 않는 브라우저에서 롱 폴링을 구현하여 이를 실행합니다. 따라서 EventSource는 현재 JavaScript가 사용 설정된 모든 최신 브라우저에서 작동합니다.

결론

HTML5는 흥미롭고 새로운 웹 개발 가능성의 문을 열어줍니다. 이제 WebSockets와 EventSource를 통해 웹 개발자는 실시간 웹 앱을 지원하는 깔끔하고 잘 정의된 표준을 사용할 수 있습니다. 하지만 모든 사용자가 최신 브라우저를 실행하는 것은 아닙니다. 이러한 기술을 구현할 때는 원활한 감쇠를 고려해야 합니다. 또한 WebSockets 및 EventSource를 위한 서버 측 도구는 아직 초기 단계에 있습니다. 실시간 HTML5 앱을 개발할 때는 이러한 요소를 염두에 두어야 합니다.