Author: eeyees
Category: Seoul.pm
Package: Seoul.pm
Title: Mojolicious, HTML5, WebSocket을 이용한 비동기 채팅

저자

@eeyees - 인쇄기기 업계의 기린아, TAFKA_HoliK라는 닉을 사용하기도 한다. 일본에서 일하다가 올해 한국 후지 제록스로 이직하였다.

시작하며

우리는 지난 기사에서 카탈리스트, 댄서와 같은 웹 프레임워크를 볼 수 있었습니다. 제가 소개시켜 드릴 것은 Mojolicious라는 웹 프레임워크입니다.

Mojolicious는 실시간 웹 프레임워크를 표방하고 있습니다. 그 외에도 멋진 특징들이 많이 있습니다. 공식 사이트에서는 아래와 같이 소개하고 있습니다.

오늘은 가볍게 Mojolicious를 이용해 Websocket으로 비동기 채팅하는 소스 코드를 살펴봅시다.

뼈대 만들기

리눅스라면 다음과 같은 명령으로 간단하게 설치할 수 있습니다.

#!bash
$ sudo sh -c "curl -L cpanmin.us | perl - Mojolicious"

cpan이 설정되어 있다면 아래와 같이 설치합니다.

#!bash
$ cpan Mojolicious

Mojolicious가 설치되면 mojo라는 명령행 도구가 생성됩니다. Mojolicious::Lite를 기반으로 한 간단한 웹 어플리케이션 뼈대를 만들기 위해서는 아래와 같이 입력할 것입니다.

#!bash
$ mojo generate lite_app

오늘은 간소화 버전이 아닌 정식 웹 어플리케이션을 만들어 봅시다. 따라서 lite_app 대신 app 타입으로 지정합니다. 어플리케이션의 이름은 Chat로 지어주었습니다.

#!bash
$ mojo generate app Chat
  [mkdir] /home/holik/tmp/chat/script
  [write] /home/holik/tmp/chat/script/chat
  [chmod] chat/script/chat 744
  [mkdir] /home/holik/tmp/chat/lib
  [write] /home/holik/tmp/chat/lib/Chat.pm
  [mkdir] /home/holik/tmp/chat/lib/Chat
  [write] /home/holik/tmp/chat/lib/Chat/Example.pm
  [mkdir] /home/holik/tmp/chat/t
  [write] /home/holik/tmp/chat/t/basic.t
  [mkdir] /home/holik/tmp/chat/log
  [mkdir] /home/holik/tmp/chat/public
  [write] /home/holik/tmp/chat/public/index.html
  [mkdir] /home/holik/tmp/chat/templates/layouts
  [write] /home/holik/tmp/chat/templates/layouts/default.html.ep
  [mkdir] /home/holik/tmp/chat/templates/example
  [write] /home/holik/tmp/chat/templates/example/welcome.html.ep

생성된 chat 디렉터리에 들어가 내부 구성을 살펴봅시다.

#!bash
~/tmp$ cd chat
~/tmp/chat$ tree
.
├── lib
│   ├── Chat
│   │   └── Example.pm
│   └── Chat.pm
├── log
├── public
│   └── index.html
├── script
│   └── chat
├── t
│   └── basic.t
└── templates
    ├── example
    │   └── welcome.html.ep
    └── layouts
        └── default.html.ep

9 directories, 7 files

lib 아래에 있는 부분이 서버의 컨트롤러 부분이겠군요. log 디렉터리에는 로그를 남길 것입니다. public에는 공통적으로 쓰이는 정적 파일이 올라갈 것입니다. 여기서는 html 페이지를 보관하고 있네요. script에는 구동 스크립트가 들어 있습니다. t 디렉터리에는 테스트 묶음이 들어갈 것입니다. templates 부분은 서버의 뷰 부분을 담당하고 있는 친구들인가 보군요.

간단하게 웹 서비스를 한번 실행해 봅시다. 아래와 같이 script 디렉터리에 들어있는 chat 스크립트를 morbo 명령을 통해 실행합니다.

#!bash
~/tmp/chat$ morbo script/chat 
Server available at http://127.0.0.1:3000.

이제 웹 브라우저에서 http://127.0.0.1:3000/welcome에 접근해 봅시다.

생성된 뼈대를 구동해 열어본 웹 페이지
그림 1. 생성된 뼈대를 구동해 열어본 웹 페이지

잘 나오네요. 그럼 이제부터 채팅 프로그램을 만들어 봅시다. :)

비동기 채팅 만들기

lib/Chat.pm 파일에 chat라는 라우트를 추가해 봅시다. 파일을 열면 아까 열어본 /welcome도 여기에 정의되어 있습니다.

#!perl
package Chat;
use Mojo::Base 'Mojolicious';

# This method will run once at server start
sub startup {
  my $self = shift;

  # Routes
  my $r = $self->routes;

  # Normal route to controller
  $r->route('/welcome')->to('example#welcome');
}

1;

/welcome 라우트를 등록하는 줄 하단에 /chat도 같은 형태로 추가합니다.

#!perl
$r->route('/chat')->to('ChatControl#chatAction');

to에 전달하는 ChatControl#chatAction은 "ChatControl이라는 컨트롤러에 chatAction이라는 액션을 수행해 주세요."라고 말하는 것과 같습니다. 같은 방식으로 웹소켓을 위한 라우트도 추가합니다.

#!perl
$r->websocket('/chatWS')->to('ChatControl#wsAction');

컨트롤러 작성하기

Chat.pm에 등록해 준 컨트롤러 부분을 구현해봅시다. lib/Chat/ChatControl.pm 파일을 만들고 chatAction 함수와 wsAction 함수를 만듭니다.

#!perl
package Chat::ChatControl;
use Mojo::Base 'Mojolicious::Controller';

our $clients = {};

sub chatAction {
    my $self = shift;
    $self->render(
        message => 'Chatting Example'
    );
}

sub wsAction {
    my $self   = shift;
    my $clntID = sprintf "%s" , $self->tx;

    $clients->{$clntID} = $self->tx;

    for my $id (keys %$clients) {
        $clients->{$id}->send_message("Client Connected!");
    }

    say "Client Connected!";

    $self->on(
        finish => sub {
            my $self = shift;
            my $id = sprintf "%s" , $self->tx;

            delete $clients->{$id};
            say "Client is Disconnected!";
        }
    );

    $self->on( 
        message => sub {
            my ($self, $message) = @_;
            for my $id (keys %$clients) {
                $clients->{$id}->send_message($message);
            }
        }
    );
}

1;

chatAction은 채팅 웹페이지 위에 표기할 메시지를 전달해 단순히 템플렛을 랜더링합니다.

wsAction은 웹 소켓으로 통신하는 로직을 작성했습니다. 먼저, $self->tx를 통해 현재 요청에 대한 트랙잭션 객체(Mojo::Transaction)를 얻습니다. 이 경우 웹 소켓 트랙잭션 객체(Mojo::Transaction::WebSocket)가 될 것입니다. 요청받은 모든 트랜잭션은 단순히 패키지 해시에 보관합니다.

$self->on을 통해 현재 요청에 대한 트랜잭션에 이벤트 콜백을 등록합니다. 특히 웹 소켓을 통해 메시지가 도착하면 message 이벤트가 수행됩니다. 그러면 보관했던 모든 트랜잭션, 즉 요청했던 모든 클라이언트에게 메시지를 전달합니다. wsAction의 행동을 위의 두 줄로 요약하면 아래와 같습니다.

뷰 작성하기

이제 뷰 부분을 만들어 보겠습니다. templeates 디렉터리로 이동한 후 ChatControl 디렉터리를 만들고 그 안에 chatAction.html.ep 파일을 만듭니다.

#!bash
$ mkdir ChatControl
$ cd ChatControl
$ vim chatAction.html.ep

그리고 다음과 같이 작성해 봅시다.

#!xml
%layout 'chatLayout';
%title 'Chatting';
<h1>
    <%= $message %>
</h1>
<div id="chatWindow">
</div>
<form>
    <input type="text" id="sendMes">
    <input type="submit" id="sendBtn" value="Send">
</form>

채팅을 보일 창 한 개와 채팅을 입력할 부분과 보낼 버튼을 추가 했습니다. 이제 레이아웃을 작성해 볼까요? 이번에는 templates/layouts 디렉터리에 chatAction.html.ep에서 사용한 레이아웃 파일을 만듭니다.

#!bash
$ cd ..
$ cd ..
$ cd templates/layouts
$ vim chatLayout.html.ep

이번에는 아래와 같이 작성합니다.

#!xml
<!DOCTYPE html>
<html>
<head>
  <title><%= title %></title>
  <style type"text/css">
    #chatWindow {
    width : 500px;
    height : 400px;
    overFlow : auto;
    border : 1px solid #000;
    }
  </style>
  <script type="text/javascript"
          src="http://code.jquery.com/jquery-1.7.min.js">
  </script>
  <script type="text/javascript">
  var ws;
  $(document).ready(function() {

      $('#sendBtn').click(sendMessage); 
      $('#sendBtn').keyup(function(ev) {
          if( ev.keycode == 13) {
              sendMessage();
          }
      });
      $('#disconnBtn').click(disconn);
      $('form').submit(function(ev) {
          ev.preventDefault();
      });

      if (!("WebSocket" in window)) {
          alert("do not Supprot WebSocket!!");
      } else {
          ws = new WebSocket('ws://localhost:3000/chatWS');
          ws.onopen = function() {
          }
          ws.onmessage = function(ev) {
              appendChat(ev.data);  
          }
          ws.onclose = function() {
              appendChat("Connection Closed!");
          }
      }

      function sendMessage() {
          ws.send($('#sendMes').val());
          $('#sendMes').val("");
      }

      function appendChat(mes) {
          $('#chatWindow').append(mes+"<br>");
      }

      function disconn() {
          ws.close();
      }
  });
  </script>
</head>
<body>
  <%= content %>
</body>
</html>

간단한 자바스크립트 코드를 이용해 웹 소켓의 메시지를 송수신합니다. 브라우저가 웹 소켓을 지원하지 않는지 검사한 뒤, 서버와 접속을 수행합니다. 웹 소켓을 통해 메시지를 받으면 onmessage에 등록한 함수가 호출되어 메시지를 출력합니다. 메시지를 보낼 때에는 ws.send(...)와 같이 웹 소켓을 통해 전달하고 있습니다. 클라이언트에서 메시지를 보낸 이후 페이지를 다시 불러들이는 것을 막기 위해 preventDefault()를 걸었습니다.

그럼 작성한 소스 코드를 실행해봅시다.

#!bash
$ morbo script/chat

완성된 서비스
그림 2. 완성된 서비스

잘 되네요 :)

정리하며

간단하게 Mojolicious를 웹 프레임워크를 사용해서 HTML5에 포함되는 WebSocket을 이용한 비동기 채팅 웹 서비스를 만들어 보았습니다. 한국에는 아직도 펄하면 단순히 문자열 처리만을 위한 언어라거나 시스템 엔지니어만 다루는 언어로만 알려져 있는 것 같습니다. 이번 펄 크리스마스 달력을 통해 펄의 다양한 활용에 대해서 조금이나마 공유할 수 있는 시간이 되어 다행인 것 같습니다.

2011년 마무리 잘 하시고, 새해 복 많이 받으세요!