메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

HTML 캔버스와 AJAX를 이용한 Supertrain 만들기

한빛미디어

|

2006-01-23

|

by HANBIT

15,374

원문: http://www.xml.com/pub/a/2006/01/18/ajax-html-canvas-ruby.html
저자: Dave Hoover, 한동훈 역

애플의 브라우저, 사파리는 개발자들이 간단한 자바스크립트 API로 2D 그림을 그릴 수 있는 HTML 요소 canvas를 추가했다. 최근에 발표된 FireFox 1.5도 canvas를 제공하면서 주류로 편입되기 위한 큰걸음을 내디뎠다. (현재는 canvas를 HTML 5에 포함시킬 것인지를 논의중이다) 불행하게도 마이크로소프트는 이 게임에서 가장 많은 카드를 손에 쥐고 있다. 즉, canvas를 지원하는 IE를 내놓기 까지 꽤 시간이 걸릴 것이라는 점이다. 그때까지도 웹 2.0 혁명이 계속될 것이며, IE 사용자를 지원하지 않는 응용프로그램들도 나타날 것이라고 믿는다. IE 사용자들에게 canvas 요소는 사용될 수 없는 것이며, 특히 XMLHttpRequest와 간단한 인터페이스를 제공하는 자바스크립트의 등장은 더더욱 그럴 것이다. 캔버스와 XHR(XMLHttpRequest)를 잠시 사용해본 소감은 대단하다.(역설적인 점은 애플과 마이크로소프트가 서로를 간과하지 않은데서 캔버스와 XHR이 나올 수 있었다는 것이다)

캔버스와 AJAX(에이잭스)를 결합한 첫번째 실험은 HTML, 자바스크립트, 워드넷(WordNet) 데이터베이스(서버측)을 사용해서 씽크맵의 비주얼 동의어사전(Thinkmap"s Visual Thesaurus, http://www.visualthesaurus.com/)의 복사본을 만드는 것이었다. canvas가 갖고 있는 몇가지 한계점 특히, 텍스트를 처리 능력의 부재에 직면했었지만 canvas가 갖는 나름의 장점들도 발견할 수 있었다. 특히, canvas의 단순함과 AJAX와의 궁합이 좋았다. 실험의 결과는 awordlike.com에서 볼 수 있다.

The Supertrain

여기서는 가상의 철도 시스템의 실시간 상태 정보를 그래픽으로 보여주기 위해 canvas를 사용하는 작은 실험을 진행할 것이다.( 예제 다운로드) 자바스크립트나 루비에 대한 세세한 내용들을 설명하지 않지만, 이들 언어에 대해 이해할 필요가 있다면 참고할만한 자료들을 정리해두었다.(참고자료 절을 참고)

워싱턴 주는 시에틀의 끔찍한 교통문제를 해결하기 위해 가벼운 공공 철도 시스템, Supertrain을 완성했다. Supertrain 전체 구조의 각 부분은 열차들이 어디에 있는지 Supertrain 사령센터에 보고하는 소프트웨어 시스템이다. 이 소프트웨어를 구축한 개발자들은 네트워크에 있는 누구나 철도 상태에 대한 정보를 얻을 수 있는 메시지 기반 시스템을 제공하는 하부 시스템(back end)를 구축하는 작업을 훌륭하게 완수했다.

불행히도, 개발팀은 시스템을 보여주는 부분에는 많은 역량을 집중하지 못했다. 사용자들은 텍스트 기반의 웹 페이지를 볼 수 있고, 시스템의 최근 상태를 보기 위해서는 브라우저를 직접 새로고쳐야 한다. 나는 각 Supertrain 선로의 상태를 동적이면서 그래픽하게 표시해주는 새로운 프론트 엔드를 작성할 것을 주문받았다. 워싱턴 주의 요구사항은 이것이 가능한지만 알 수 있게 초기버전으로 Supertrain 라인 중에 하나만 갖고 이 작업을 하라는 것이다.

server.rb

require "webrick"
include WEBrick

server = HTTPServer.new( :Port => 8053 )
server.mount("/", HTTPServlet::FileHandler, "./docroot")

server.mount_proc("/train/line") do |request, response|
  response["Content-Type"] = "text/plain"
  response.body = "toot, toot"
end

trap("INT") { server.shutdown }

server.start

[ruby server.rb]로 이 스크립트를 실행하고, 브라우저에서 http://localhost:8053/train/line을 방문하면 다음 화면을 볼 수 있을 것이다.

그림1
그림1.

로컬 디렉터리 docroot를 만들었고, 여기에 HTML을 추가할 것이다. 지금은 간단하게 위치만 알 수 있게 "toot, toot"로 표시해두었다.

docroot/redwood.html


mmlt;bodymmgt;
hello woodinville!
mmlt;/bodymmgt;


이제, 보다 유용한 것들을 출력하기 위해 /train/line closure를 개발할 것이다. 자바스크립트에서 JSON은 정말 쉽기 때문에, 서버와 클라이언트 사이의 프로토콜로는 JSON을 사용할 것이다.

server.rb

...
require "trainspotter"
...
train_spotter = TrainSpotter.new

server.mount_proc("/train/line") do |request, response|
  response["Content-Type"] = "text/plain"

  json = train_spotter.status_report.
           map { |train| "{"track": "" + train.track.to_s + "", "location": " + train.location.to_s + "}" }.
             join ","

  response.body = "[ #{json} ]"
end
...

trainspotter.rb

class TrainSpotter
  def status_report
    [ Status.new("south", 20) ]
  end
end

class Status
  attr_reader :track, :location

  def initialize(track, location)
    @track = track
    @location = location
  end
end

브라우저에서 http://localhost:8053/train/line을 방문하면 뭔가 전보다 약간 더 유용한 것을 출력한다.

그림2
그림2

이제는 TrainSpotter 객체가 끊임없이 업데이트되는 상태 보고서를 갖고 있다고 가정한 상태에서 이 객체에 동작을 부여할 것이다. 다음은 몇가지 실제 데이터를 제공하는 간단한 버전을 구현한 것이다.

trainspotter.rb

TRACKS = [:north, :south]
TRAINS_PROGRESS = {:north => 5, :south => 420}
MAX_SPEED = 5

class TrainSpotter
  def status_report
    report = []

    TRAINS_PROGRESS[:north] += rand(MAX_SPEED)
    report << Status.new("north", TRAINS_PROGRESS[:north])

    TRAINS_PROGRESS[:south] -= rand(MAX_SPEED)
    report << Status.new("south", TRAINS_PROGRESS[:south])
  end
end
...

http://localhost:8053/train/line을 방문한 후 반복적으로 새로고침을 하면 데이터가 변하는 것을 볼 수 있다! 이는 Woodinville에서 Redmond로, 남쪽 방향으로 향하는 열차의 진행상태와 Redmond에서 Woodinville로, 북쪽 방향으로 향하는 열차의 진행상태를 함께 보여주고 있다.

그림3
그림3

다음은 반복적으로 새로고침을 하지 않아도 되게 AJAX 스타일로 redwood.html을 바꿀차례다. AJAX 스타일로 고치기 위해 가장 단순한 Prototype 라이브러리를 사용할 것이다.

docroot/redwood.html





mmlt;bodymmgt;

mmlt;div id="status"mmgt;mmlt;/divmmgt;


mmlt;/bodymmgt;


브라우저에서 http://localhost:8053/redwood.html을 방문하면 매 2초마다 열차의 상태가 업데이트 되는 것을 볼 수 있다.(Prototype의 Ajax.PeriodicalUpdater의 기본 주기가 2초다) 멋지다! 불행히도 나의 고객은 쉽게 감명받지 않는다. 따라서, 이번에는 서버측 상태를 클라이언트측 그래픽으로 동적으로 업데이트하게 할 차례다. 이번에도 계속해서 한 단계씩 시작할 것이다. 좋은 철도 프로젝트가 그런 것처럼 필자도 일부 트랙에 대해서만 작업을 시작할 것이다.

docroot/redwood.html

...
mmlt;bodymmgt;

  id="redwood"
  width="500"
  height="120"
  style="border: 1px solid black">




mmlt;div id="status"mmgt;mmlt;/divmmgt;
...

그림4
그림4

루프를 위해서 표준 자바스크립트를 사용하지 않고 있다. AJAX를 위해 자바스크립트 라이브러리 Prototype 1.4.0을 사용하고 있기 때문에 루비식의 컬렉션 이터레이터와 문법 설탕 즉, $(), $H().values(), each()를 사용할 수 있다. 이제, 선로가 놓였으니 열차만 놓으면 된다. 먼저, Ajax.PeriodicalUpdater 예제를 제거하고, window.setInternal을 호출하는 것으로 대체한다.(setInternal이 동적 캔버스를 만드는데 필수요소다) 또한, drawTracks를 보다 높은 수준의 updateCanvas 함수로 리팩토링을 수행할 것이다.

docroot/redwood.html

  ...
  if ($("redwood").getContext) {
    canvas = $("redwood").getContext("2d")
    window.setInterval(updateCanvas, 1000 * 2)
    updateCanvas()
  }

  function updateCanvas() {
    clearScreen()
    drawTracks()
  }

  function clearScreen() {
    canvas.clearRect(0, 0, $("redwood").width, $("redwood").height)
  }

  function drawTracks() {
  ...

열차는 아직 없지만, 캔버스는 이제 2,000 밀리초(2초)마다 다시 그려진다. 열차는 이미지를 사용할 것이고, canvas의 drawImage 메서드를 사용한다. 주기적인 실행을 처리하는 window.setInternal 함수를 갖고 있으며, 열차의 상태를 알기 위해 Prototype의 Ajax.Request를 사용할 수 있다. 서버에서 데이터를 가져오면 열차 이미지의 위치를 업데이트 하면 된다. 다음 코드는 열차를 움직이면서 실시간 진행을 보여준다.

docroot/redwood.html

  var trains = {
    north: new Train("train-lr.png", 5),
    south: new Train("train-rl.png", 60)
  }
  ...
  function updateCanvas() {
    clearScreen()
    drawTracks()

    new Ajax.Request("/train/line",
                     { onComplete: function(request) {
                         var jsonData = eval(request.responseText)
                         if (jsonData == undefined) { return }
                         jsonData.each(function(data) {
                           trains[data.track].update(data.location)
                         })
                       }
                     })
  }
  ...
  function Train(image, y) {
    this.image = new Image()
    this.image.src = image
    this.y = y
    this.update = updateTrain
  }

  function updateTrain(location) {
    canvas.drawImage(this.image, location, this.y)
  }
  ...

그림5
그림5

이 작업으로 몇가지 개념들을 함께 이용했다. Prototype을 사용해서 비동기 호출을 수행했으며, 응답에서는 JSON 문자열을 받았으며, JSON 문자열을 자바스크립트 객체의 배열로 마샬링하기 위해 eval()을 호출했다. 초기 열차 상태를 구하고, 동작을 업데이트하기 위해 Train 객체를 사용하였다. 불행히도, 지금 작성한 코드는 열차 이미지를 움직일 때 깜빡거림을 만들어낸다. 이는 스크린을 지우고, 이미지를 다시 그리는 AJAX의 onComplete 콜백 사이에 지연되는 시간 때문에 발생한다. 열차를 그리기 바로 직전에 clearScreen을 호출하면 이런 깜빡거림을 제거할 수 있다.

docroot/redwood.html

  ...
  function updateCanvas() {
    new Ajax.Request("/train/line",
                     { onComplete: function(request) {
                         var jsonData = eval(request.responseText)
                         if (jsonData == undefined) { return }
                         clearScreen()
                         jsonData.each(function(data) {
                           trains[data.track].update(data.location)
                         })
                         drawTracks()
                         drawHotspots()
                       }
                     })
  }
  ...

이제, 마지막 함수가 하나 필요하다. 열차 위치는 우리가 표시할 수 있는 항목의 가능한 상태들 중에 하나에 불과하다. Supertrain 시스템은 또한 열차가 고장나거나 사고가 발생한 열차를 추적할 수 있는 "사고지역"을 추적할 수 있다. TrainSpotter에 사고지역 몇 개를 직접 코드로 작성해서 이런 사고지역을 구현할 것이며, 웹 클라이언트에 사고지역을 보여주기 위해 WEBrick에 새로운 closure를 마운트할 것이다.

server.rb

...
server.mount_proc("/train/line") do |request, response|
  response["Content-Type"] = "text/plain"
  json = train_spotter.status_report.
           map { |train| "{"track": "" + train.track.to_s + "", "location": " + train.location.to_s + "}" }.
             join ","
  response.body = "[ #{json} ]"
end

server.mount_proc("/train/hotspots") do |request, response|
  response["Content-Type"] = "text/plain"
  json = train_spotter.hot_spots
           map { |train| "{"track": "" + train.track.to_s + "", "location": " + train.location.to_s + "}" }.
             join ","
  response.body = "[ #{json} ]"
end
...
trainspotter.rb
class TrainSpotter
...
  def hot_spots
    [ Status.new(:north, 125), Status.new(:south, 250), Status.new(:south, 150) ]
  end
end

/train/hotspots clousre를 볼 때, 불편함을 느낄 수 있다. Status 객체를 JSON 문자열로 변환하는 함수로 추출해서 이런 불편함을 해결해보자.

server.rb

...
def status_list_to_json(list)
  json = list.
           map { |train| "{"track": "" + train.track.to_s + "", "location": " + train.location.to_s + "}" }.
             join ","
  "[ #{json} ]"
end

server.mount_proc("/train/line") do |request, response|
  response["Content-Type"] = "text/plain"
  response.body = status_list_to_json(train_spotter.status_report)
end

server.mount_proc("/train/hotspots") do |request, response|
  response["Content-Type"] = "text/plain"
  response.body = status_list_to_json(train_spotter.hot_spots)
end
...

http://localhost:8053/train/hotspots 에서 다음과 같은 화면을 볼 수 있다
그림6
그림6

사고지역을 표시할 수 있게 클라이언트 부분을 업데이트하자. 사고지역 상태는 열차 상태만큼 자주 바뀌지 않기 때문에 사고지역 데이터는 한 시간에 한 번만 가져오도록 별도의 window.setInternal을 사용할 것이다.

docroot/redwood.html

...


...

그림7
그림7

이로써 시험버전을 완성했다! 워싱턴 주의 내 고객은 만족해하면서 실제 Woodinville-Redmond Supertrain 구간에 대해 시험삼아 실제 운영중인 메시징 서비스에 TrainSpotter 클래스를 추가할 것을 요구했다. 모든 게 잘 진행된다면, 일이 더 많아지겠지....

참고자료
TAG :

이전 글 : Ajax와 자바

다음 글 : WASP을 이용한 PHP 개발

댓글 입력
자료실

최근 본 책0