열네번째 날: 초심자를 위한 Mojolicious

저자

@rumidier - 서산 야생마, 가슴 속에 화난새 키우는 어른이

시작하며

펄에는 수 많은 웹프레임워크가 있습니다. 그 중 근래에 가장 많이 사용하는 프레임워크는 CatalystDancer, Mojolicious 세 가지 입니다. 복잡한 엔터프라이즈 환경은 Catalyst를 많이 사용하고 비교적 규모가 작은 웹 사이트는 DancerMojolicious를 많이 사용한다고 합니다. Mojolicious공식 홈페이지에 문서화가 잘 되있고 다루기 쉽다고 하지만 정말 아무 것도 모른채로 시작한다면 막히는 곳이 한 두 군데가 아닙니다. 문서를 보고도 어떻게 시작해야 될지 헤매실 분들에게 이 글을 바칩니다.

준비물

필요한 모듈은 다음과 같습니다.

직접 CPAN을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.

$ sudo cpan Mojolicious DBI DBD::mysql

사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.

$ cpan Mojolicious DBI DBD::mysql

사용 방법

Mojolicious 모듈을 설치하면 같이 제공되는 mojo 명령은 다양한 기능을 제공합니다. help를 인자로 주면 자세한 기능을 살펴볼 수 있습니다.

$ mojo help
usage: mojo COMMAND [OPTIONS]

Tip: CGI and PSGI environments can be automatically detected very often and
    work without commands.

These commands are currently available:
daemon     Start application with HTTP and WebSocket server.
cpanify    Upload distribution to CPAN.
get        Perform HTTP request.
inflate    Inflate embedded files to real files.
eval       Run code against application.
version    Show versions of installed modules.
psgi       Start application with PSGI.
prefork    Start application with preforking HTTP and WebSocket server.
cgi        Start application with CGI.
test       Run unit tests.
routes     Show available routes.
generate   Generate files and directories from templates.

These options are available for all commands:
    -h, --help          Get more information on a specific command.
        --home <path>   Path to your applications home directory, defaults to
                        the value of MOJO_HOME or auto detection.
    -m, --mode <name>   Operating mode for your application, defaults to the
                        value of MOJO_MODE/PLACK_ENV or "development".

See 'mojo help COMMAND' for more information on a specific command.

Mojolicious를 가장 간단히 사용할 수 있는 Mojoliciois::Lite 모듈을 이용한 웹 앱을 작성하기 위해 mojo 명령의 generate, lite_app 옵션을 이용해서 myapp.pl을 생성합니다.

$ mojo generate lite_app myapp.pl
[exist] /Users/mojo-test
[write] /Users/mojo-test/myapp.pl
[chmod] myapp.pl 744

앞의 명령으로 생성되는 myapp.pl 웹 앱을 실행하려면 Mojolicious 모듈과 함께 제공되는 morbo 명령을 사용해서 생성한 myapp.pl 파일을 인자로 줍니다.

$ morbo myapp.pl
Listening at "http://*:3000".
Server available at http://127.0.0.1:3000.

특별한 옵션을 지정하지 않을 경우 3000 포트를 사용합니다. 특정 포트를 지정하려면 -l 옵션을 사용합니다.

$ morbo myapp.pl -l http://*:5001
Listening at "http://*:5001".
Server available at http://127.0.0.1:5001.

실행 후 웹브라우저를 이용해 명령줄에 출력되는 주소로 접속하면 myapp.pl 웹 앱을 브라우저에서 확인할 수 있습니다.

Mojolicious::Lite 실행화면 그림 1. Mojolicious::Lite 실행화면 (원본)

이제 Mojolicious를 사용해 개발할 수 있는 환경이 마련되었습니다. GET/POST를 이용한 읽기, 쓰기, 수정, 삭제 기능을 지원하는 아주 간단한 게시판을 만들어보며 감을 잡아봅시다.

뼈대 코드 작성

우선 간단한 게시판을 만들기 위해 myapp.pl 파일을 열어 뼈대를 잡아보죠.

#!/usr/bin/env perl

use v5.16;
use utf8;

use Mojolicious::Lite;

get  '/'           => sub { };
get  '/list'       => sub { };
get  '/write'      => sub { };
post '/write'      => sub { };
get  '/read/:id'   => sub { };
get  '/edit/:id'   => sub { };
post '/edit'       => sub { };
get  '/delete/:id' => sub { };

app->start;

__DATA__

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
  </head>
  <body>
    <%= content %>
  </body>
</html>

@@ write.html.ep
% layout 'default';
% title 'WRITE';


@@ list.html.ep
% layout 'default';
% title 'LIST';


@@ read.html.ep
% layout 'default';
% title 'READ';


@@ edit.html.ep
% layout 'default';
% title 'EDIT';

뼈대 코드는 크게 __DATA__ 섹션 위와 아래로 구성됩니다. __DATA__ 섹션 위는 컨트롤러에 해당하며 아래는 템플릿에 해당합니다. 컨트롤러 즉 URL 라우팅에 해당하는 코드는 다음과 같습니다.

get  '/'           => sub { };
get  '/list'       => sub { };
get  '/write'      => sub { };
post '/write'      => sub { };
get  '/read/:id'   => sub { };
get  '/edit/:id'   => sub { };
post '/edit'       => sub { };
get  '/delete/:id' => sub { };

전체 페이지의 템플릿을 구성하는 코드는 @@ layouts/default.html.ep 영역입니다.

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
  </head>
  <body>
    <%= content %>
  </body>
</html>

그 외의 템플릿은 모두 % layout 'default' 구문을 이용해 layouts/default.html.ep를 큰 틀로 사용하며 각각의 템플릿에서 표현하는 부분은 layouts/default.html.ep 템플릿에서 <%= content %> 영역에 해당하는 부분과 대치됩니다.

@@ XXXX.html.ep
% layout 'default'; # layouts/default.html.ep를 사용한다는 의미
% title 'WRITE';    # <%= title %> 영역과 대치됨
... 이 부분은 <%= content %> 영역과 대치됨

데이터베이스 생성 및 연결

간단한 게시판이라 하더라도 입력 받은 자료, 즉 글을 어딘가에 저장해야 합니다. MySQL을 이용해 DB를 생성하고 DBI 모듈, DBD::mysql 모듈을 이용해 생성한 데이터베이스에 접속하겠습니다.

우선 Book 데이터베이스를 생성합니다.

$ mysql -u root -p 'create database Book;'

Book 데이터베이스에 다음 스키마를 참고해서 memo 테이블을 생성합니다.

CREATE TABLE `memo` (
  `id`      INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name`    VARCHAR(20)      NOT NULL DEFAULT '',
  `title`   VARCHAR(70)      NOT NULL,
  `content` TEXT             NOT NULL,
  `wdate`   TIMESTAMP        NOT NULL DEFAULT CURRENT_TIMESTAMP,

  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Mojolicious에서 생성한 데이터베이스에 연결하기 위해 다음 코드를 추가합니다. MySQL 접속 계정은 memouser, 비밀 번호는 memopass라고 가정합니다.

#!/usr/bin/env perl

use v5.16;
use utf8;

use Mojolicious::Lite;

use DBI;

my $DBH = DBI->connect(
    'dbi:mysql:Book',
    'memouser',
    'memopass',
    {
        RaiseError        => 1,
        AutoCommit        => 1,
        mysql_enable_utf8 => 1,
    },
);

get '/' => sub { };
...

쓰기

웹페이지에서 쓰기 기능을 사용하기 위해 필요한 것은 두 가지 입니다.

글을 쓰기 위한 페이지는 GET /write 요청을 통해 접근합니다.

get '/write' => sub {
    my $self = shift;
    $self->render('write');
};

메모 쓰기화면 그림 2. 메모 쓰기화면 (원본)

메모 쓰기 HTML 양식 입니다. 이름, 제목, 내용 세 가지를 입력합니다.

@@ write.html.ep
% layout 'default';
% title 'WRITE';

    <form action="/write" method="post">
      <table width=580 border=0 cellpadding=2 cellspacing=1 bgcolor=#777777>
        <tr>
          <td height=20 colspan=4 align=center bgcolor=#999999>
            <font color=white><b>글쓰기</b></font>
          </td>
        </tr>
        <tr>
          <td bgcolor=white>
            <table bgcolor=white>
              <tr>
                <td>이름</td>
                <td><input type="text" name="name"></td>
              </tr>
              <tr>
                <td>제목</td>
                <td><input type="text" name="title"></td>
              </tr>
              <tr>
                <td>내용</td>
                <td colspan=4>
                  <textarea name="content" cols=80 rows=5></textarea>
                </td>
              </tr>
              <tr>
                <td colspan=10 align=center>
                  <input type="submit" value="저장">
                </td>
              </tr>
            </table>
          </td>
        </tr>
      <tr>
        <td bgcolor=#999999>
          <table width=100%>
            <tr>
              <td>
                <a href='/list' style="text-decoration:none;"><font color=white>[목록보기]</font></a>
              </td>
            </tr>
          </table>
        </td>
      </tr>
      </table>
    </form>

동일한 /write 주소라도 GET 요청이냐 또는 POST 요청이냐에 따라 get '/write'post '/write'로 구분이 되므로 주의하세요. POST로 전달된 값은 $self->param('name')으로 값을 얻을 수 있습니다.

post '/write' => sub {
    my $self = shift;

    my $name    = $self->param('name');
    my $title   = $self->param('title');
    my $content = $self->param('content');

    my $sth = $DBH->prepare(qq{
        INSERT INTO `memo` (`name`,`title`,`content`) VALUES (?,?,?)
    });
    $sth->execute( $name, $title, $content );
    $self->redirect_to( $self->url_for('/list') );
};

$self->param(...)으로 얻은 값은 DBI를 통해 저장합니다. 사용법은 다음과 같습니다.

my $sth = $DBH->prepare(qq{
    INSERT INTO `memo` (`name`,`title`,`content`) VALUES (?,?,?)
});
$sth->execute( $name, $title, $content );

문제가 없다면 데이터베이스에는 다음처럼 저장됩니다.

mysql> select * from memo;
+----+----------+--------------+-------------------------------------------------------+---------------------+
| id | name     | title        | content                                               | wdate               |
+----+----------+--------------+-------------------------------------------------------+---------------------+
|  1 | rumidier | book-test-01 | 웹페이지 제작을 위해 Mojolicious를 사용하고 있습니다. | 2013-11-01 20:00:00 |
+----+----------+--------------+-------------------------------------------------------+---------------------+

읽기

저장한 글 목록을 확인할 수 있는 페이지가 필요하겠죠. /로 접속했을 때는 /list로 바로 이동하도록 변경합니다.

get '/' => sub {
    my $self = shift;

    $self->redirect_to( $self->url_for('/list') );
};

GET /list 요청을 받을 수 있는 /list를 작성합니다.

get '/list' => sub {
    my $self = shift;

    my $sth = $DBH->prepare(qq{ SELECT id, name, title, content, wdate FROM memo });
    $sth->execute();

    my %articles;
    while ( my @row = $sth->fetchrow_array ) {
        my ( $id, $name, $title, $content, $date ) = @row;
        my ($wdate) = split / /, $date;

        $articles{$id} = {
        name    => $name,
        title   => $title,
        content => $content,
        wdate   => $wdate,
        };
    }

    $self->stash( articles => \%articles );
};

$self->stash() 함수를 이용해 템플릿 쪽으로 \%articles 해시 레퍼런스를 전달합니다. \%articles 해시 레퍼런스를 이용하는 템플릿 쪽 코드는 다음과 같습니다.

@@ list.html.ep
% layout 'default';
% title 'LIST';

    <table width=580 border=0 cellpadding=2 cellspacing=1 bgcolor=#999999>
      <tr height=20 colspan=4 align=center bgcolor=#CCCCCC >
        <td color=white>No. </td>
        <td>제목</td>
        <td>글쓴이</td>
        <td>date</td>
      </tr>
      % foreach my $id ( keys %$articles ) {
      <tr bgcolor="white">
        <td><%= $id %></td>
        <td><a href="/read/<%= $id %>"><%= $articles->{$id}{title} %></a></td>
        <td><%= $articles->{$id}{name} %></td>
        <td><%= $articles->{$id}{wdate} %></td>
      </tr>
      % }
      <tr>
        <td colspan=4 bgcolor=#999999>
          <table width=100%>
            <tr>
              <td width=2000 align=center height=20>
                <a href='/write' style="text-decoration:none;"><font color=white>[글쓰기]</font></a>
              </td>
            </tr>
          </table>
        </td>
      </tr>
    </table>

컨트롤러로 부터 넘겨 받은 데이터를 출력해보면 데이터베이스의 id를 기준으로 출력이 되는데 이때 id는 정렬되지 않은 상태, 즉 무작위로 출력됩니다.

해쉬 무작위 정렬 그림 3. 해쉬 무작위 정렬 (원본)

정렬 연산자인 sort를 추가합니다. sort만으로는 가장 처음에 입력된 id=1인 값이 맨위로 올라오므로 역순으로 출력하기위해 reverse도 같이 사용합니다.

% foreach my $id ( reverse sort keys %$articles ) {

역순 정렬 그림 4. 역순 정렬 (원본)

1번이 맨아래로 갔지만 10번 이상이 중간에 섞여 있습니다. 이것은 sort 함수가 문자열 기준 정렬을 하기 때문인데 이를 보완하기 위해 숫자 정렬을 해야겠죠. 우주선 연산자(<=>)를 사용해서 다시 정렬해보죠.

% foreach my $id ( reverse sort { $a <=> $b } keys %$articles ) {

숫자 정렬 그림 5. 숫자 정렬 (원본)

이제 저장한 글 목록이 화면에 출력됩니다.

각각의 글 내용을 확인하기 위한 페이지는 /read/1, /read/2, /read/N 등의 주소로 연결했습니다. 다음은 /read/N으로 접속했을 때 처리할 수 있는 컨트롤러 코드입니다.

get '/read/:id' => sub {
    my $self = shift;

    my $input_id = $self->param('id');

    my $sth = $DBH->prepare(qq{ SELECT * FROM memo WHERE id=$input_id });
    $sth->execute();

    my %articles;
    my ( $id, $name, $title, $content, $date ) = $sth->fetchrow_array;
    my ($wdate) = split / /, $date;

    $articles{$id} = {
        name    => $name,
        title   => $title,
        content => $content,
        wdate   => $wdate,
    };

    $self->stash(
        articles => \%articles,
        id       => $id,
    );
    $self->render('read');
};

다음은 /read/N으로 접속했을 때 처리할 수 있는 템플릿 코드입니다.

@@ read.html.ep
% layout 'default';
% title 'READ';

    <table width=580 border=0 cellpadding=2 cellspacing=1 bgcolor=#777777>
      <tr>
        <td height=20 colspan=4 align=center bgcolor=#999999>
          <font color=white><b><%= $articles->{$id}{title} %></b></font>
        </td>
      </tr>
      <tr>
        <td width=50 height=20 align=center bgcolor=#EEEEEE> 글쓴이 </td>
        <td width=240 bgcolor=white> <%= $articles->{$id}{name} %> </td>
        <td width=50 height=20 align=center bgcolor=#EEEEEE> 날짜 </td>
        <td width=240 bgcolor=white> <%= $articles->{$id}{wdate} %> </td>
      </tr>
      <tr>
        <td bgcolor=white colspan=4>
          <font color=black>
            <pre><%= $articles->{$id}{content} %></pre>
          </font>
        </td>
      </tr>
      <tr>
        <td colspan=4 bgcolor=#999999>
          <table width=100%>
            <tr>
              <td width=2000 align=left height=20>
                <a href='/list' style="text-decoration:none;"><font color=white>[목록보기]</font></a>
                <a href='/write' style="text-decoration:none;"><font color=white>[글쓰기]</font></a>
                <a href='/edit/<%= $id %>' style="text-decoration:none;"><font color=white>[수정]</font></a>
                <a href='/delete/<%= $id %>' style="text-decoration:none;"><font color=white>[삭제]</font></a>
              </td>
            </tr>
          </table>
        </td>
      </tr>
    </table>

이제 글 목록에서 각각의 글 제목을 클릭하면 글의 내용을 확인할 수 있습니다.

글 내용 보기 그림 6. 글 내용 보기 (원본)

수정

쓰고 읽은 후 내용을 수정하고 싶을 경우를 위해서 수정 기능이 필요하겠죠. 다음은 /edit/N으로 접속했을 때 처리할 수 있는 컨트롤러 코드입니다.

get '/edit/:id' => sub {
    my $self = shift;

    my $input_id = $self->param('id');

    my $sth = $DBH->prepare(qq{ SELECT * FROM memo WHERE id=$input_id });
    $sth->execute();

    my %articles;
    my ( $id, $name, $title, $content, $date ) = $sth->fetchrow_array;
    my ($wdate) = split / /, $date;

    $articles{$id} = {
        name    => $name,
        title   => $title,
        content => $content,
        wdate   => $wdate,
    };

    $self->stash(
        articles => \%articles,
        id       => $id,
    );

    $self->render('edit');
};

수정하는 페이지는 쓰기 페이지와 유사하지만 기존의 정보가 표시되야 한다는 차이점이 있습니다. 다음은 /edit/N으로 접속했을 때 처리할 수 있는 템플릿 코드입니다.

@@ edit.html.ep
% layout 'default';
% title 'EDIT';

    <form action="/edit" method="post">
      <input type="hidden" name="id" value="<%= $id %>">
      <table width=580 border=0 cellpadding=2 cellspacing=1 bgcolor=#777777>
        <tr>
          <td height=20 colspan=4 align=center bgcolor=#999999>
            <font color=white><b>수정</b></font>
          </td>
        </tr>
        <tr>
          <td bgcolor=white>
            <table bgcolor=white>
              <tr>
                <td>이름</td>
                <td><input type="text" name="name" value="<%= $articles->{$id}{name} %>"></td>
              </tr>
              <tr>
                <td>제목</td>
                <td><input type="text" name="title" value="<%= $articles->{$id}{title} %>"></td>
              </tr>
              <tr>
                <td>내용</td>
                <td colspan=4>
                  <textarea name="content" cols=80 rows=5><%= $articles->{$id}{content} %></textarea>
                </td>
              </tr>
              <tr>
                <td colspan=10 align=center>
                  <input type="submit" value="수정확인">
                </td>
              </tr>
            </table>
          </td>
        </tr>
      <tr>
        <td bgcolor=#999999>
          <table width=100%>
            <tr>
              <td>
                <a href='/list' style="text-decoration:none;"><font color=white>[목록보기]</font></a>
                <a href='/write' style="text-decoration:none;"><font color=white>[글쓰기]</font></a>
                <a href='/read/<%= $id %>' style="text-decoration:none;"><font color=white>[취소]</font></a>
                <a href='/delete/<%= $id %>' style="text-decoration:none;"><font color=white>[삭제]</font></a>
              </td>
            </tr>
          </table>
        </td>
      </tr>
      </table>
    </form>

내용을 수정한 후 POST로 전달하므로 그에 맞는 컨트롤러가 필요합니다.

post '/edit' => sub {
    my $self = shift;

    my $id      = $self->param('id');
    my $name    = $self->param('name');
    my $title   = $self->param('title');
    my $content = $self->param('content');

    my $sth = $DBH->prepare(qq{
        UPDATE `memo` SET `name`=?,`title`=?,`content`=? WHERE `id`=$id
    });
    $sth->execute( $name, $title, $content );

    $self->redirect_to( $self->url_for('/list') );
};

수정이 끝난 후 /list로 페이지를 이동시키는 부분을 유의해주세요. 페이지를 리다이렉트 시킬 때는 $self->redirect_to() 함수를 이용합니다.

수정된 1번 내용 그림 7. 수정된 1번 내용 (원본)

수정 기능도 이제 잘 동작하는군요.

삭제

삭제는 비교적 간단합니다. 다음은 /delete/N으로 접속했을 때 처리할 수 있는 컨트롤러 코드입니다.

get '/delete/:id' => sub {
    my $self = shift;

    my $id = $self->param('id');

    my $sth = $DBH->prepare(qq{ DELETE FROM `memo` WHERE `id`=$id });
    $sth->execute();

    $self->redirect_to( $self->url_for('/list') );
};

삭제 후 /list로 이동하기 때문에 따로 템플릿 코드가 필요하지 않죠.

Helper 사용하기

자세히 살펴보면 /read/$id/edit/$id에서 동일한 코드를 사용하고 있습니다. 코드가 중복되는 것은 좋지 않죠. 중복된 코드를 줄이기 위해 helper를 사용합니다. 다음과 같이 동일 코드를 helper로 작성합니다.

helper db_select => sub {
    my ( $self, $input_id ) = @_;

    my $sth = $DBH->prepare(qq{ SELECT * FROM memo WHERE id=$input_id});
    $sth->execute();

    my %articles;
    my ( $db_id, $name, $title, $content, $date ) = $sth->fetchrow_array;
    my ($wdate) = split / /, $date;

    $articles{$db_id} = {
        name    => $name,
        title   => $title,
        content => $content,
        wdate   => $wdate,
    };

    return \%articles;
};

방금 작성한 헬퍼 코드를 호출하려면 $self->db_select() 형식으로 사용합니다.

my $articles = $self->db_select($input_id);
my ($id)     = keys %$articles;

$self->stash(
    articles => $articles,
    id       => $id,
);

전체 코드

다음은 전체 코드와 SQL 스키마 코드입니다.

잘못되거나 개선할 부분이 있다면 갱신이 될 예정이니 이 점 참고해주세요. :-)

정리하며

웹에 대한 많은 이해와 실전 경험이 있다면 잘 정리된 Mojolicious 문서만 보더라도 금방 이해하고 응용할 수 있습니다. 다만 웹이 익숙치 않다면 문서만 보았을때 힘든 부분이 꽤 많습니다(제가 힘듭니다). 비록 CSS나 자바스크립트도 사용하지 않고 구식의 HTML 코드를 사용하고는 있지만 간단하고 짧기 때문에 이해하기에는 더 용이하지 않을까 바래봅니다. 이 예제와 글이 저처럼 문서를 보다가 헤매시는 다른 분들께도 작은 도움이 되었으면 합니다.

blog comments powered by Disqus