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

루이지 몬타네즈
Luigi Montanez

소개

HTML5에서는 WebSocketsEventSource를 통해 개발자가 서버와 실시간으로 통신하는 웹 앱을 빌드할 수 있습니다. Stream Congress (Chrome 웹 스토어에서 다운로드)에서는 미국 의회 운영에 관한 실시간 업데이트를 제공합니다. 하원과 상원의원 관련 소식, 관련 뉴스 업데이트, 국회의원 트윗, 기타 소셜 미디어 업데이트를 스트리밍합니다. 이 앱은 의회의 업무 상황을 보여주므로 하루 종일 열려 있습니다.

WebSocket으로 시작하기

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

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

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

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

생리통 소개

Cramp는 EventMachine을 기반으로 실행되는 비동기 Ruby 웹 프레임워크입니다. 이 가이드는 Ruby on 레일스 코어 팀의 구성원인 프라틱 나이크가 작성했습니다. 실시간 웹 앱에 도메인별 언어 (DSL)를 제공하는 Cramp는 Ruby 웹 개발자에게 이상적인 선택입니다. Ruby on 레일에서 컨트롤러를 작성하는 데 익숙한 사용자는 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와 같은 비차단 데이터베이스 드라이버를 사용해야 합니다.
  • 이벤트 기반 웹 서버를 사용해야 합니다. 얇은무지개가 기본적으로 지원됩니다.
  • Cramp 앱은 Stream Congress를 구동하는 기본 Rust 앱과 별도로 실행되어야 하며, 독립적으로 다시 시작 및 모니터링되어야 합니다.

현재 제한사항

2010년 12월 8일에 보안 취약점이 발표되면서 WebSocket이 차질을 겪었습니다. Firefox와 Opera에서 모두 WebSockets에 대한 브라우저 지원을 삭제했습니다. 순수 자바스크립트 폴리필은 없지만 널리 채택된 플래시 대체가 있습니다. 그러나 플래시에 의존하는 것은 이상적이지 않습니다. Chrome과 Safari는 계속해서 WebSocket을 지원하지만, Flash에 의존하지 않고 모든 최신 브라우저를 지원하려면 WebSocket을 교체해야 한다는 것이 분명해졌습니다.

AJAX 폴링으로 롤백

이 결정은 WebSockets에서 벗어나 "구식" AJAX 폴링으로 전환하기로 했습니다. AJAX 폴링은 디스크 및 네트워크 I/O 관점에서는 훨씬 덜 효율적이지만 Stream Congress의 기술적 구현을 간소화했습니다. 무엇보다도 별도의 Cramp 앱이 필요 없게 되었습니다. AJAX 엔드포인트는 대신 Rail 앱에서 제공했습니다. 클라이언트 측 코드가 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 앱은 기본 레일 앱과 동일한 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 엔드포인트를 설정합니다.

안정적인 Polyfill

WebSocket에 비해 EventSource가 갖는 가장 큰 장점 중 하나는 대체가 완전히 JavaScript에 기반하며 플래시에 의존하지 않는다는 것입니다. Remy Sharp의 polyfill은 EventSource를 기본적으로 지원하지 않는 브라우저에서 긴 폴링을 구현하여 이를 달성합니다. 따라서 EventSource는 JavaScript가 사용 설정된 모든 최신 브라우저에서 작동합니다.

결론

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