스물세번째 날: 흔한 개발 이야기

저자

@keedi - Seoul.pm 리더, Perl덕후, 거침없이 배우는 펄의 공동 역자, keedi.k at gmail.com

시작하며

펄로 개발하는 일은 무척 신선한 경험입니다. 전형적인 컴파일 언어로 개발하다가 펄을 만나게 되었을 때의 그 짜릿함이란! (잘 모르는) 많은 사람들의 우려와 달리 펄은 모듈화하기가 매우 쉬우며, 큰 규모의 프로그램을 작성하기에 적합한 구조를 가지고 있습니다. 하지만 많은 분들은 여전히 펄을 원라이너나 짧은 스크립트 정도로만 쓰고 있거나, 또는 그것이 펄의 전부라고 믿곤 합니다. 오늘은 짧지만 전형적인 개발 절차를 밟는 과정을 살짝 보여드릴까합니다. 그야말로 흔한 개발 이야기죠. :)

이것 저것 너무 깊진 않아도 여러 분야를 아우르지만, 구색도 갖추려면 무엇이 적당할까요? 역시 모듈화도 해야할테고, 객체지향으로 작성하는 편이 낫겠죠? 모듈로 만들었으니 패키징도 해야할테고, 모듈을 사용하는 명령줄 스크립트는 보너스로 넣고, 자료를 다룬다면 데이터베이스를 사용해야겠죠. 또 이것을 웹과 연동하면서 모바일 환경에서도 사용하는 정도면 어떨까요? 기왕이면 모듈에 문제가 있어서 그대로 사용하기에는 애로사항이 있으면 더할나위 없겠군요. 자, 지금부터 간단한 일정 관리 도구를 만들어 봅시다.

주의

일련의 과정을 보여드리는 것이 목적인만큼 각 단계에 대한 자세한 설명은 소개하는 모듈과 라이브러리의 공식 문서를 참조해주세요. :)

준비물

데비안 계열의 운영체제를 사용하고 있다면 SQLite 개발 관련 패키지를 설치합니다. MySQL이나 PostgreSQL처럼 다른 데이터베이스를 사용한다면 그에 적절한 패키지를 설치합니다.

# SQLite
$ sudo apt-get install libsqlite3-dev
# MySQL
$ sudo apt-get install libmysqlclient-dev
# PostgreSQL
$ sudo apt-get install postgresql-server-dev-all

모듈화 및 객체지향 프로그래밍에 필요한 모듈은 다음과 같습니다.

데이터베이스 접근을 위해 사용한 모듈은 다음과 같습니다. SQLite대신 MySQL이나 PostgreSQL을 사용한다면 CPAN의 DBD::mysql 모듈이나 CPAN의 DBD::Pg 모듈을 설치합니다.

패키징에 필요한 모듈은 다음과 같습니다. 무언가 엄청나게 많은 것을 설치하는 것 같지만 걱정하지 마세요. 거의 대부분의 일은 Dist::Zilla가 자동으로 처리해줍니다. 여러분이 해야할 일은 모듈을 설치하고 설정파일을 만드는 일이죠.

웹앱 작성을 위해 사용한 모듈은 다음과 같습니다.

추가로 더 설치해야 하는 모듈은 다음과 같습니다.

사용하고는 있지만 코어 모듈인 관계로 설치하지 않아도 되는 모듈은 다음과 같습니다.

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

$ sudo cpan \
    DBIx::Lite \
    Dist::Zilla \
    Dist::Zilla::Plugin::AutoPrereqs \
    Dist::Zilla::Plugin::FakeRelease \
    Dist::Zilla::Plugin::InstallGuide \
    Dist::Zilla::Plugin::MetaResources \
    Dist::Zilla::Plugin::PkgVersion \
    Dist::Zilla::Plugin::PodCoverageTests \
    Dist::Zilla::Plugin::PodSyntaxTests \
    Dist::Zilla::Plugin::PodWeaver \
    Dist::Zilla::Plugin::Prereqs \
    Dist::Zilla::Plugin::ReadmeMarkdownFromPod \
    Dist::Zilla::PluginBundle::Basic \
    Dist::Zilla::PluginBundle::Filter \
    Encode \
    ExtUtils::MakeMaker \
    File::HomeDir \
    File::Spec \
    Mojolicious \
    Mojolicious::Plugin::HamlRenderer \
    Moo \
    MooX::Options \
    MooX::Types::MooseLike::Base \
    Pod::Weaver::PluginBundle::KEEDI \
    Time::Piece \
    namespace::clean

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

$ cpan \
    DBIx::Lite \
    Dist::Zilla \
    Dist::Zilla::Plugin::AutoPrereqs \
    Dist::Zilla::Plugin::FakeRelease \
    Dist::Zilla::Plugin::InstallGuide \
    Dist::Zilla::Plugin::MetaResources \
    Dist::Zilla::Plugin::PkgVersion \
    Dist::Zilla::Plugin::PodCoverageTests \
    Dist::Zilla::Plugin::PodSyntaxTests \
    Dist::Zilla::Plugin::PodWeaver \
    Dist::Zilla::Plugin::Prereqs \
    Dist::Zilla::Plugin::ReadmeMarkdownFromPod \
    Dist::Zilla::PluginBundle::Basic \
    Dist::Zilla::PluginBundle::Filter \
    Encode \
    ExtUtils::MakeMaker \
    File::HomeDir \
    File::Spec \
    Mojolicious \
    Mojolicious::Plugin::HamlRenderer \
    Moo \
    MooX::Options \
    MooX::Types::MooseLike::Base \
    Pod::Weaver::PluginBundle::KEEDI \
    Time::Piece \
    namespace::clean

GLGLGLGL...

몇 가지 결정하고 진행해볼까요? 우리가 만들 모듈은 MyTodo, 최종 생성 패키지는 MyTodo-0.00X.tar.gz 타르볼 파일입니다. 패키지 안에는 mytodo.pl이라는 명령줄 유틸리티가 있으며, 웹앱용 구동 파일은 mytodo-web.pl이며, 웹앱 설정 파일은 mytodo-web.conf라고 하죠.

우선 작업을 진행할 디렉터리를 먼저 만들겠습니다.

$ mkdir mytodo

아무래도 모듈화와 패키징에 익숙하지 않다면 그때 그때 파일을 만들기 보다 일단 디렉터리 구조를 보고 시작하는 편이 이해하기에 더 낫겠죠?

$ tree mytodo/
mytodo/
├── Changes
├── bin
│   └── mytodo.pl
├── dist.ini
├── lib
│   ├── MyTodo
│   │   ├── Script.pm
│   │   └── Util.pm
│   └── MyTodo.pm
├── mytodo-web.conf
└── mytodo-web.pl

빈 파일이라도 좋으니 우선 디렉터리를 구성하고 시작하는 것이 편합니다. 앞에서 정한 부분과 다른 부분이 몇가지 있군요. Changes 파일과 dist.ini 파일은 패키징을 위해 사용하며, lib/MyTodo/Script.pm 파일은 명령줄 유틸리티를 만들때 필요한 함수를 위한 모듈이며, lib/MyTodo/Util.pm 파일은 MyTodo에 넣기에 직접적인 연관이 없는 함수를 저장하기 위한 모듈입니다.

빈 파일 채워넣기

dist.ini

dist.ini 파일은 Dist::Zilla를 사용하기 위한 설정 파일입니다. 이름이나 이메일 주소등 필요한 부분을 자신에게 맞게 변경하면 됩니다.

name             = MyTodo
author           = Keedi Kim - 김도형 <[email protected]>
license          = Perl_5
copyright_holder = Keedi Kim
copyright_year   = 2012
version          = 0.000

;[@Basic]
[@Filter]
-bundle = @Basic
-remove = UploadToCPAN
[FakeRelease]

[AutoPrereqs]
[PkgVersion]
[ReadmeMarkdownFromPod]
[InstallGuide]
[Prereqs / RuntimeRequires]
[PodCoverageTests]
[PodSyntaxTests]
[PodWeaver]
config_plugin = @KEEDI

Changes

Changes 파일은 패키지의 릴리즈별 변경사항을 기록하는 파일입니다. 펄 모듈이라면 당연히 포함해야 하는 파일이며, CPAN은 강제하고 있습니다. 여러분을 믿지 못한다면 항상 작성하는 것을 추천합니다.

Release history for MyTodo

0.xxx
    First version, released on unsuspecting world.

펄 모듈

펄 모듈은 우선 기본 형태를 먼저 갖추도록 하죠.

lib/MyTodo.pm 파일입니다.

package MyTodo;
# ABSTRACT: Personal To-Do management

1;
__END__

=head1 SYNOPSIS

    use MyTodo;

    my $todo = MyTodo->new;


=head1 DESCRIPTION

...

lib/MyTodo/Script.pm 파일입니다.

package MyTodo::Script;
# ABSTRACT: MyTodo command line utility options processing

1;
__END__

=head1 SYNOPSIS

    ...


=head1 DESCRIPTION

...

lib/MyTodo/Util.pm 파일입니다.

package MyTodo::Util;
# ABSTRACT: MyTodo code snippets

1;
__END__

=head1 SYNOPSIS

    ...


=head1 DESCRIPTION

...

SYNOPSISDESCRIPTION은 추후 작성하기 편리하게 ...으로 위치를 잡아놓은 것을 제외하면 특별한 부분은 없습니다. :)

일정 관리 모듈

일정 관리 메인 모듈은 MyTodo.pm 파일입니다. 객체지향을 지원하도록 할테니 기본적으로 new() 메소드를 사용할 수 있겠죠. 데이터베이스에 접속해서 자료를 저장, 열람, 갱신, 삭제를 해야하기 때문에 데이터베이스에 접속하기 위한 파라미터가 필요합니다. 객체 생성시 지정할 수 있도록 dsn, dbusername, dbpassword, dbattr 속성으로 관리하는 것이 좋을 것 같습니다. 우리가 만든 모듈을 사용할 사용자(물론 지금은 개발자 자신이겠지만...)가 직접 데이터베이스에 접근해서 제어하는 것을 막기 위해 모듈화를 했으므로 기본적인 add(), delete(), edit(), list() 메소드도 필요합니다.

객체지향 모듈을 제작하기 위해 다음 모듈을 추가합니다.

use Moo;
use MooX::Types::MooseLike::Base qw( Str HashRef Maybe );
use namespace::clean -except => 'meta';

Moo 모듈을 사용하면 속성값을 추가하는 일은 정말 간단합니다.

has dsn => (
    is       => 'ro',
    isa      => Str,
    required => 1,
);

has dbusername => (
    is  => 'ro',
    isa => Maybe[Str],
);

has dbpassword => (
    is  => 'ro',
    isa => Maybe[Str],
);

has dbattr => (
    is  => 'ro',
    isa => Maybe[HashRef],
);

Moo 모듈이 new() 생성자는 기본으로 만들어주기 때문에 CRUD와 관련된 메소드만 추가하면 됩니다. 객체지향 펄에서 메소드는 함수를 추가하는 것으로 간단히 만들어집니다.

sub add {
    my ( $self, %params ) = @_;
    # ...
}

sub delete {
    my ( $self, %params ) = @_;
    # ...
}

sub edit {
    my ( $self, %params ) = @_;
    # ...
}

sub list {
    my ( $self, %params ) = @_;
    # ...
}

DB에 접근을 하려면 데이터베이스에 접속해야겠죠. 전통적인 DBI 모듈을 사용할 수도 있지만 펄에는 현대적인 ORM 모듈이 무척 많습니다. 그중에서도 상대적으로 가볍고 손쉬운 사용법이 특징인 DBIx::Lite를 사용해서 데이터베이스의 자료를 조작해보죠. 우선 DBIx::Lite 모듈을 추가합니다.

use DBIx::Lite;

내부적으로 사용하기 위해 _dbix 속성을 추가하고 여기에 DBIx::Lite 객체를 저장합니다. _ 기호는 외부로 공개하지 않음을 의미하는 펄 프로그래머들 사이의 관용적인 약례입니다. 데이터베이스 접속에 필요한 모든 속성값이 갖춰진 다음 객체를 생성할 수 있도록 lazy 형식으로 지정하고 builder 메소드를 이용해서 객체를 생성합니다.

has _dbix => (
    is      => 'lazy',
    builder => '_builder_handle',
);

sub _builder_handle {
    my $self = shift;

    my $dbix = DBIx::Lite->connect(
        $self->dsn,
        $self->dbusername,
        $self->dbpassword,
        $self->dbattr,
    );
    $dbix->schema->table('mytodo')->autopk('id');

    return $dbix;
}

하지만 DBIx::Lite 객체 생성 시점을 미루더라도 가능하면 일찍 생성되도록 객체 생성 직후에 바로 생성할 수 있도록 BUILDER 메소드에서 언급을 합니다.

sub BUILD {
    my $self = shift;

    $self->_dbix;
}

lazy 방식으로 생성되는 속성의 경우 해당 속성이 참조되는 순간까지 최대한 생성 시점을 늦춰 객체 생성의 오버헤드를 줄여서 성능상의 이점을 얻을 수 있습니다. BUILD 메소드는 객체 생성 이후 동작을 지정할 수 있는데 이 지점에서 _dbix 속성에 접근하면 해당 속성값의 빌더가 자동으로 호출되면서 DBIx::Lite 객체가 생성됩니다.

자료 구조

데이터베이스에 접속할 준비가 끝났는데, 막상 어떻게 저장을 해야할지에 대한 규칙이 없군요. 일정 관리에 필요한 자료구조, 지금은 데이터베이스 스키마를 구성해보죠.

간단한 To-Do 수준의 관리니 해야할 일하고있는 일, 해야할 일 정도로 나누죠. 무엇을 할지도 기록해야할테고, 얼마나 중요한지도 표시해야 할 것입니다. 마감날이 있을 수도 있습니다. 그리고 실제로 기록한 날과 값을 변경한 날도 기록하면 정렬을 할때도 도움이 될 것입니다.

CREATE TABLE mytodo (
    id INTEGER NOT NULL,
    status     CHARACTER(32) NOT NULL,
    content    INTEGER       NOT NULL,
    priority   INTEGER       DEFAULT 0,
    deadline   DATETIME,
    updated_on DATETIME      NOT NULL,
    created_on DATETIME      NOT NULL,
    PRIMARY KEY (id)
);

짜잔~ 하나의 테이블로 구성된 간단한 스키마가 완성되었습니다. SQLite를 기준으로 작성한 스키마이므로 다른 데이터베이스를 사용한다면 약간 문법을 수정해야 합니다.

이렇게 작성한 스키마는 어떻게 보관하면 좋을까요? 별도의 파일로 보관하는 것도 나쁘진 않지만 아무래도 모듈과 따로 보관하다보면 잠시 신경을 쓰지 않으면 금방 모듈의 버전보다 뒤쳐지게 되곤 합니다. 최선은 아니겠지만 저는 주로 모듈안에 이런 데이터를 저장합니다. MyTodo::Util 모듈에 저장하고 이를 손쉽게 꺼내 쓸 수 있도록 간단한 유틸리티를 제작합니다.

MyTodo/Util.pm 파일에 다음 내용을 추가합니다.

sub sql_sqlite {
    return (
        <<'END_SQL',
DROP TABLE IF EXISTS mytodo
END_SQL
        <<'END_SQL',
CREATE TABLE mytodo (
    id INTEGER NOT NULL,
    status     CHARACTER(32) NOT NULL,
    content    INTEGER       NOT NULL,
    priority   INTEGER       DEFAULT 0,
    deadline   DATETIME,
    updated_on DATETIME      NOT NULL,
    created_on DATETIME      NOT NULL,
    PRIMARY KEY (id)
)
END_SQL
    );
}

HERE DOCUMENT를 적절히 활용하면 많은 양의 문자열을 쉽게 저장할 수 있습니다. 보관만 해서는 아무 소용이 없겠죠. 명령줄에서 언제든지 꺼내서 쓸 수 있도록 bin/mytodo.pl 파일에 스키마를 열람할 수 있는 기능을 추가합니다.

먼저 MyTodo/Script.pm 파일에 다음 내용을 추가합니다.

use Moo;
use MooX::Options ( protect_argv => 0 );
use namespace::clean -except => [qw/_options_data _options_config/];

option schema_sqlite => (
    is    => 'ro',
    doc   => 'schema sql for sqlite',
    order => 99,
);

bin/mytodo.pl 파일은 다음처럼 작성합니다.

#!perl
# ABSTRACT: MyTodo command line utility
# PODNAME: mytodo.pl

use 5.010;
use utf8;
use strict;
use warnings;

use MyTodo::Script;
use MyTodo::Util;

my $opt = MyTodo::Script->new_with_options;

if ( $opt->schema_sqlite ) {
    say for map { chomp; "$_;" } MyTodo::Util->sql_sqlite;
    exit;
}

놀랍지만 명령줄에서 실행할 준비가 끝났습니다. --help 옵션을 이용하면 명령줄 옵션을 확인할 수 있습니다.

$ perl -Ilib bin/mytodo.pl --help
USAGE: mytodo.pl [-h] [long options...]
        --schema_sqlite    schema sql for sqlite
        -h --help          show this help message

--schema_sqlite 옵션을 이용해서 스키마를 출력할 수 있습니다.

$ bin/mytodo.pl --schema_sqlite
DROP TABLE IF EXISTS mytodo;
CREATE TABLE mytodo (
    id INTEGER NOT NULL,
    status     CHARACTER(32) NOT NULL,
    content    INTEGER       NOT NULL,
    priority   INTEGER       DEFAULT 0,
    deadline   DATETIME,
    updated_on DATETIME      NOT NULL,
    created_on DATETIME      NOT NULL,
    PRIMARY KEY (id)
);

파이프를 이용하면 간단히 SQLite 데이터베이스 파일을 생성할 수 있습니다.

$ mkdir ~/.mytodo
$ bin/mytodo.pl --schema_sqlite | sqlite3 ~/.mytodo/mytodo.db

CRUD 메소드 구현

MyTodo

MyTodo 메인 모듈에 add(), delete(), edit(), list() 메소드를 추가해봅시다.

sub add {
    my $self = shift;

    my $epoch  = time;
    my %params = (
        status     => 'todo',
        created_on => $epoch,
        updated_on => $epoch,
        @_,
    );

    my $todo = $self->_dbix->table('mytodo')->insert({ %params });

    return $todo;
}

sub delete {
    my ( $self, %params ) = @_;

    my $id = delete $params{id};
    return unless $id;

    $self->_dbix->table('mytodo')
        ->search({ id => $id })
        ->delete;
}

sub edit {
    my ( $self, %params ) = @_;

    my $id = delete $params{id};
    return unless $id;

    $self->_dbix->table('mytodo')
        ->search({ id => $id })
        ->update({ %params, updated_on => time });
}

sub list {
    my $self   = shift;
    my %params = @_;

    my $rs
        = $self->_dbix->table('mytodo')
        ->select(qw/
            id
            status
            content
            priority
            deadline
            created_on
            updated_on
        /);
    $rs = $rs->search($_)   for @{ $params{search} };
    $rs = $rs->order_by($_) for @{ $params{order_by} };

    return $rs;
}

DBIx::Lite의 기본적인 기능을 이용해서 간단히 CRUD를 처리했습니다. 공식 문서에서 다음 메소드의 사용법을 참고해보세요.

MyTodo::Script

MyTodo::Script 모듈의 사용 방법을 보고 눈치채셨겠지만, MooX::Options 모듈을 이용해서 스크립트에서 사용할 명령줄 옵션을 OOP 모듈의 속성으로 지정할 수 있습니다. CRUD 기능을 완전하게 지원하기 위해서 몇가지 옵션을 더 추가해보죠.

use File::HomeDir;
use File::Spec::Functions;

option add => (
    is    => 'ro',
    short => 'a',
    doc   => 'add todo',
    order => 1,
);

option delete => (
    is        => 'ro',
    short     => 'd',
    format    => 'i@',
    doc       => 'delete todo',
    autosplit => ',',
    order     => 1,
);

option edit => (
    is        => 'ro',
    short     => 'e',
    format    => 'i@',
    doc       => 'edit todo',
    autosplit => ',',
    order     => 1,
);

option list => (
    is    => 'ro',
    short => 'l',
    doc   => 'list todo',
    order => 1,
);

option priority => (
    is     => 'ro',
    short  => 'p',
    format => 'i',
    doc    => 'todo priority',
    order  => 12,
);

option deadline => (
    is     => 'ro',
    format => 's',
    doc    => 'todo deadline (local time)',
    order  => 13,
);

option status => (
    is     => 'ro',
    short  => 's',
    format => 's',
    doc    => 'todo status',
    order  => 14,
);

option dsn => (
    is      => 'ro',
    doc     => 'database dsn',
    format  => 's',
    default => sub {
        my $home = File::HomeDir->my_home;
        my $db   = catfile( $home, '.mytodo', 'mytodo.db' );
        return "dbi:SQLite:$db";
    },
    order   => 21,
);

option dbusername => (
    is     => 'ro',
    doc    => 'database username',
    format => 's',
    order  => 22,
);

option dbpassword => (
    is     => 'ro',
    doc    => 'database password',
    format => 's',
    order  => 23,
);

option dbattr => (
    is      => 'ro',
    doc     => 'database attribute',
    default => sub { [] },
    format  => 's@',
    order   => 24,
);

mytodo.pl

추가한 옵션에 대한 액션을 정의하고 실제 동작을 구현해야겠지요. 완전한 코드를 구경해볼까요?

#!perl
# ABSTRACT: MyTodo command line utility
# PODNAME: mytodo.pl

use 5.010;
use utf8;
use strict;
use warnings;

use Encode qw( decode_utf8 encode_utf8 );
use Time::Piece;

use MyTodo;
use MyTodo::Script;
use MyTodo::Util;

binmode STDIN,  ':utf8';
binmode STDOUT, ':utf8';

my $opt = MyTodo::Script->new_with_options;

if ( $opt->schema_sqlite ) {
    say for map { chomp; "$_;" } MyTodo::Util->sql_sqlite;
    exit;
}

my %dbattrs = map { split /=/ } @{ $opt->dbattr };
my $todo = MyTodo->new(
    dsn        => $opt->dsn,
    dbusername => $opt->dbusername,
    dbpassword => $opt->dbpassword,
    dbattr     => \%dbattrs,
);

if ( $opt->list ) {
    my $display_func = sub {
        my $item = shift;

        my $str = sprintf(
            "[%-5s] %5s : #%-2d %s",
            uc $item->status,
            "\x{2605}" x $item->priority . "\x{2606}" x (5 - $item->priority),
            # 2605(★), 2606(☆)
            $item->id,
            decode_utf8($item->content),
        );
        if ($item->deadline) {
            my $deadline = Time::Seconds->new( $item->deadline - time )->pretty;
            $deadline =~ s/minus /-/;
            $deadline =~ s/(\d+) days, /sprintf('%2dD', $1)/e;
            $deadline =~ s/(\d+) hours, /sprintf('%2dH', $1)/e;
            $deadline =~ s/(\d+) minutes, /sprintf('%2dM', $1)/e;
            $deadline =~ s/\d+ seconds$//;
            $str .= " ($deadline)";
        }
        say $str;
    };

    for my $search (
        { status => 'doing' },
        { status => 'todo'  },
        { status => 'done'  },
    )
    {
        my $rs = $todo->list(
            search   => [ $search ],
            order_by => [ '-me.priority' ],
        );
        $display_func->($_) while $_ = $rs->next;
    }
    exit;
}

if ( $opt->add ) {
    my $content = shift;
    my $epoch   = time;

    my %params;
    $params{content}  = $content       if $content;
    $params{priority} = $opt->priority if $opt->priority;
    $params{status}   = $opt->status   if $opt->status && $opt->status =~ /^(todo|doing|done)$/;
    if ($opt->deadline) {
        my $t = Time::Piece->strptime($opt->deadline, "%Y-%m-%dT%H:%M:%S");
        $params{deadline} = $t->epoch;
    }

    $todo->add(%params);

    exit;
}

if ( $opt->delete ) {
    return unless $opt->delete;
    $todo->delete( id => $opt->delete );
    exit;
}

if ( $opt->edit ) {
    my $content = shift;
    my $epoch   = time;

    return unless $opt->edit;

    my %params;
    $params{id}       = $opt->edit;
    $params{content}  = $content       if $content;
    $params{priority} = $opt->priority if $opt->priority;
    $params{status}   = $opt->status   if $opt->status && $opt->status =~ /^(todo|doing|done)$/;
    if ($opt->deadline) {
        my $t = Time::Piece->strptime($opt->deadline, "%Y-%m-%dT%H:%M:%S");
        $params{deadline} = $t->epoch;
    }

    $todo->edit(%params);
    exit;
}

명령줄에서 --help 옵션을 이용하면 구현한 모든 옵션을 확인할 수 있습니다.

$ perl bin/mytodo.pl --help
USAGE: mytodo.pl [-adehlps] [long options...]
        -a --add           add todo
        -d --delete        delete todo
        -e --edit          edit todo
        -l --list          list todo
        -p --priority      todo priority
        --deadline         todo deadline (local time)
        -s --status        todo status
        --dsn              database dsn
        --dbusername       database username
        --dbpassword       database password
        --dbattr           database attribute
        --schema_sqlite    schema sql for sqlite
        -h --help          show this help message

To-Do 목록을 추가하려면 -a 옵션을 이용합니다. 이때 -p 옵션으로 중요도를 조정하고 -s 옵션을 이용해서 todo, doing, done 중 하나의 값을 지정할 수 있습니다. -e 옵션은 수정을 위한 옵션으로 To-Do 목록 아이디를 지정하고 값을 변경할 수 있습니다. -d 옵션은 삭제를 위한 옵션으로 To-Do 목록 아이디를 지정하면 해당 목록을 지웁니다. 마지막으로 -l 옵션을 이용해서 To-Do 목록을 확인할 수 있습니다.

$ todo -a 'writing document for MyTodo'
$ todo -a 'writing perl example using LibreOffice SDK' -p3 -sdoing
$ todo -l
$ todo -e1 -p5
$ todo -l
$ todo -d1 -d2
$ todo -l

Let's Patch!

사실 현재 버전(3.73)의 MooX::Options를 사용하면 도움말 출력시 옵션이 무작위 순서로 출력됩니다. 이 문제는 내부적으로 옵션을 객체의 속성으로 저장하고 있다가 도움말 출력 시점에 각 속성을 해시 형태로 변한한 후 해시의 키를 추출해서 출력하기 때문에 발생하는 현상입니다. 펄에서 해시의 순서는 무작위이기 때문에 나타나는 부작용(side-effect)인 셈이죠.

사실 큰 문제는 없지만 아무래도 사용자 입장에서는 일정한 규칙에 따라 순서대로 출력되는 편이 가독성이나 사용성 면에서 유리합니다. 이 문제는 비교적 간단하게 해결할 수 있는데 패치는 다음과 같습니다.

From cba8e63ceb2a5223a90e7be51b2ff61080db68bd Mon Sep 17 00:00:00 2001
From: Keedi Kim <[email protected]>
Date: Mon, 17 Dec 2012 14:19:03 +0900
Subject: Order attribute when displaying help message

Sorting option is helpful for users, so added order attribute.
First sort keys by order attr value, and default order value set as 0.
If order attr is same, trying to sort by it's key name. :-)
---
 lib/MooX/Options.pm      |    7 ++-
 lib/MooX/Options/Role.pm |    6 ++-
 t/order.t                |  115 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 126 insertions(+), 2 deletions(-)
 create mode 100644 t/order.t

diff --git a/lib/MooX/Options.pm b/lib/MooX/Options.pm
index 0bbdfc9..43b0d86 100755
--- a/lib/MooX/Options.pm
+++ b/lib/MooX/Options.pm
@@ -17,7 +17,7 @@ use Carp;

 # VERSION
 my @OPTIONS_ATTRIBUTES
-    = qw/format short repeatable negativable autosplit doc/;
+    = qw/format short repeatable negativable autosplit doc order/;

 sub import {
     my ( undef, @import ) = @_;
@@ -121,6 +121,7 @@ sub _filter_attributes {
 sub _validate_and_filter_options {
     my (%options) = @_;
     $options{doc} = $options{documentation} if !defined $options{doc};
+    $options{order} = 0 if !defined $options{order};

     my %cmdline_options = map { ( $_ => $options{$_} ) }
         grep { exists $options{$_} } @OPTIONS_ATTRIBUTES, 'required';
@@ -420,6 +421,10 @@ Ex :
     my $t = t->new_with_options;
     t->verbose # 3

+=item order
+
+Specified the order of the attribute.
+
 =back

 =head1 namespace::clean
diff --git a/lib/MooX/Options/Role.pm b/lib/MooX/Options/Role.pm
index 279c025..9cc201a 100644
--- a/lib/MooX/Options/Role.pm
+++ b/lib/MooX/Options/Role.pm
@@ -67,7 +67,11 @@ sub parse_options {
     };

     my %has_to_split;
-    for my $name ( keys %options_data ) {
+    my @sorted_keys = sort {
+        $options_data{$a}{order} <=> $options_data{$b}{order} # sort by order
+            or $a cmp $b                                      # sort by attr name
+    } keys %options_data;
+    for my $name (@sorted_keys) {
         my %data = %{ $options_data{$name} };
         my $doc  = $data{doc};
         $doc = "no doc for $name" if !defined $doc;
diff --git a/t/order.t b/t/order.t
new file mode 100644
index 0000000..8df6196
--- /dev/null
+++ b/t/order.t
@@ -0,0 +1,115 @@
+#!perl
+use strict;
+use warnings;
+use Test::More tests => 3;
+use Test::Trap;
+
+{
+    package t1;
+    use Moo;
+    use MooX::Options;
+
+    option 'first' => (
+        is            => 'ro',
+        documentation => 'first option',
+        order         => 1,
+    );
+
+    option 'second' => (
+        is            => 'ro',
+        documentation => 'second option',
+        order         => 2,
+    );
+
+    option 'third' => (
+        is            => 'ro',
+        documentation => 'third option',
+        order         => 3,
+    );
+
+    option 'fourth' => (
+        is            => 'ro',
+        documentation => 'fourth option',
+        order         => 4,
+    );
+
+    1;
+}
+
+{
+    package t2;
+    use Moo;
+    use MooX::Options;
+
+    option 'first' => (
+        is            => 'ro',
+        documentation => 'first option',
+    );
+
+    option 'second' => (
+        is            => 'ro',
+        documentation => 'second option',
+    );
+
+    option 'third' => (
+        is            => 'ro',
+        documentation => 'third option',
+    );
+
+    option 'fourth' => (
+        is            => 'ro',
+        documentation => 'fourth option',
+    );
+
+    1;
+}
+
+{
+    package t3;
+    use Moo;
+    use MooX::Options;
+
+    option 'first' => (
+        is            => 'ro',
+        documentation => 'first option',
+        order         => 1,
+    );
+
+    option 'second' => (
+        is            => 'ro',
+        documentation => 'second option',
+        order         => 2,
+    );
+
+    option 'third' => (
+        is            => 'ro',
+        documentation => 'third option',
+    );
+
+    option 'fourth' => (
+        is            => 'ro',
+        documentation => 'fourth option',
+    );
+
+    1;
+}
+
+{
+    my $opt = t1->new_with_options;
+    trap { $opt->options_usage };
+    ok $trap->stdout =~ /first.+second.+third.+fourth/gms, 'order work w/ order attribute';
+}
+
+{
+    my $opt = t2->new_with_options;
+    trap { $opt->options_usage };
+    ok $trap->stdout =~ /first.+fourth.+second.+third/gms, 'order work w/o order attribute';
+}
+
+{
+    my $opt = t3->new_with_options;
+    trap { $opt->options_usage };
+    ok $trap->stdout =~ /fourth.+third.+first.+second/gms, 'order work w/ mixed mode';
+}
+
+done_testing;
-- 
1.7.10.4

모듈의 저자에게 패치를 보내기는 했지만 사실 언제 적용이 될지는 알 수가 없습니다. 해당 패치는 설치한 모듈에 적용해야 하는데, 아무리 perlbrew를 이용해 사용자 계정에 설치했다손 치더라도 자동으로 설치한 모듈을 직접 수정하는 것은 여러모로 찜찜합니다. 사실 패치를 하고 그 사실을 잊어버리는 것이 찜찜한 것이겠죠. 최선은 아니겠지만 저는 이런 경우 항상 로컬 저장소에 패치를 별도로 보관하면서 제가 실행할 시스템 또는 모듈에만 지역적으로 적용시키곤 합니다. 이번에도 우리의 MyTodo 모듈에만 적용을 시키도록 하겠습니다.

MooX::Options 모듈 중 해당 패치를 적용시키려면 MooX::OptionsMooX::Options::Role 양쪽 모두에 적용해야 합니다. 따라서 MyTodo::Patch:: 하부에 패치를 적용시킨 상기 두 모듈을 위치시키도록 합니다. 적용시킨 디렉터리 구조는 다음과 같습니다.

$ tree mytodo/
mytodo/
├── Changes
├── bin
├── dist.ini
└── lib
    ├── MyTodo
    │   ├── Patch
    │   │   └── MooX
    │   │       ├── Options
    │   │       │   └── Role.pm
    │   │       └── Options.pm
    │   ├── Script.pm
    │   └── Util.pm
    └── MyTodo.pm

적용 후 패키지명을 MyTodo::Patch::로 시작하도록 변경해야하므로 다음처럼 추가로 수정 하도록 합니다.

diff -urN a/lib/MyTodo/Patch/MooX/Options/Role.pm b/lib/MyTodo/Patch/MooX/Options/Role.pm
--- a/lib/MyTodo/Patch/MooX/Options/Role.pm     2012-12-24 16:27:23.923471855 +0900
+++ b/lib/MyTodo/Patch/MooX/Options/Role.pm     2012-12-24 16:26:34.707469991 +0900
@@ -1,4 +1,4 @@
-package MooX::Options::Role;
+package MyTodo::Patch::MooX::Options::Role;

 # ABSTRACT: role that is apply to your object
 use strict;
 diff -urN a/lib/MyTodo/Patch/MooX/Options.pm b/lib/MyTodo/Patch/MooX/Options.pm
--- a/lib/MyTodo/Patch/MooX/Options.pm  2012-12-24 16:27:09.483471308 +0900
+++ b/lib/MyTodo/Patch/MooX/Options.pm  2012-12-24 16:26:34.707469991 +0900
@@ -1,4 +1,4 @@
-package MooX::Options;
+package MyTodo::Patch::MooX::Options;

 # ABSTRACT: add option keywords to your object (Mo/Moo/Moose)

@@ -70,7 +70,7 @@
     my $options_data = {};
     my $apply_modifiers = sub {
         return if $target->can('new_with_options');
-        $with->('MooX::Options::Role');
+        $with->('MyTodo::Patch::MooX::Options::Role');

         $around->(
             _options_data => sub {

이제 MooX::Options 모듈을 사용하는 MyTodo::Script 모듈 쪽을 수정합니다.

diff -urN a/lib/MyTodo/Script.pm b/lib/MyTodo/Script.pm
--- a/lib/MyTodo/Script.pm      2012-12-24 16:28:33.971474508 +0900
+++ b/lib/MyTodo/Script.pm      2012-12-24 16:26:34.707469991 +0900
@@ -2,7 +2,7 @@
 # ABSTRACT: MyTodo command line utility options processing

 use Moo;
-use MooX::Options ( protect_argv => 0 );
+use MyTodo::Patch::MooX::Options ( protect_argv => 0 );
 use namespace::clean -except => [qw/_options_data _options_config/];

 use File::HomeDir;

네, 모든 패치가 끝났습니다. 사실 앞의 코드에서 속성값을 정의할때 order라는 값을 지정했는데, 이 기능이 원래의 MooX::Options에 존재하는 기능이 아니라 방금의 패치를 하면서 추가된 기능입니다. 무언가 부족한 부분이 있다면 언제든지 수정해서 원하는 기능을 추가하거나 성능을 개선시킬 수 있다는 사실이 오픈 소스의 매력이 아닐까요?

덧글: 사실 해당 패치는 12월 24일 3.74 버전에 적용되었습니다. 따라서 그 이후에 모듈을 설치했다면 이 패치 과정은 필요없습니다. 하지만 앞의 과정에서 정말 중요한 것은 모듈의 원저자와 상관없이 나만의 패치를 적용시키고 소스코드와 함께 관리하는 것이 펄에서는 무척 쉽다는 점입니다.

웹앱과 모바일

이제 고지가 눈 앞입니다. 조금만 더 힘내보죠. :)

지금까지의 작업으로 데이터베이스를 이용해서 일정 관리를 할 수 있는 모듈을 만들었고, 또 그 모듈을 사용해서 명령줄에서 일정 관리를 하는 간단한 유틸리티를 만들었습니다. 이미 모듈화를 했기 때문에 웹앱으로 만들고 모바일까지 지원하는 것은 그리 어렵지 않습니다. Mojolicious 웹프레임워크와 jQuery 모바일을 조합해서 웹과 스마트폰에서도 사용이 가능한 웹앱을 만들어 보겠습니다.

설정

웹앱용 설정 파일인 mytodo-web.conf 파일에는 MyTodo 모듈을 만들때 생성자에게 넘겨줄 인자를 저장하도록 하겠습니다. 파일의 내용은 다음과 같습니다.

#!/usr/bin/env perl

use utf8;
use strict;
use warnings;

+{
    #
    # mytodo
    #
    dsn        => "dbi:SQLite:$ENV{HOME}/.mytodo/mytodo.db",
    dbusername => q{},
    dbpassword => q{},
    dbattr     => +{ sqlite_unicode => 1},
};

MySQL을 사용하거나 PostgreSQL등 다른 데이터베이스를 사용한다면 그에 적절하게 값을 변경하도록 합니다. 물론 SQLite 파일의 위치가 다르더라도 dsn 값을 수정해야겠지요.

컨트롤러

mytodo-web.plMojolicious::Lite 모듈을 적재해서 웹앱으로써 동작하도록 합니다. 추가로 설정 파일을 사용하기 위해 Config 플러그인을 적재하고, Haml을 사용하기위해 haml-renderer 플러그인도 적재합니다. 그리고 지금까지 작성한 MyTodo 모듈을 적재하고 객체를 생성해서 웹앱이 일정관리를 할 만반의 준비를 갖추도록 합니다.

#!/usr/bin/env perl

use 5.010;
use utf8;
use Mojolicious::Lite;

use MyTodo;

plugin 'Config';
plugin 'haml_renderer';

my $mytodo = MyTodo->new(
    dsn        => app->config->{dsn},
    dbusername => app->config->{dbusername},
    dbpassword => app->config->{dbpassword},
    dbattr     => app->config->{dbattr},
);

app->start;

__DATA__

웹앱을 만들기 위해 작성할 컨트롤러는 //detail/:_id 단 두 개입니다.

/ 컨트롤러는 다음과 같습니다. list 렌더러(뷰)로 연결되는 점을 유의하세요.

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

    my $todo = $mytodo->list(
        order_by => [ '-me.priority' ],
        search   => [{ status => 'todo' }],
    );

    my $doing = $mytodo->list(
        order_by => [ '-me.priority' ],
        search   => [{ status => 'doing' }],
    );

    my $done = $mytodo->list(
        order_by => [ '-me.priority' ],
        search   => [{ status => 'done' }],
    );

    my $all = $mytodo->list(
        order_by => [ '-me.priority' ],
    );

    $self->render(
        'list',
        todo  => $todo,
        doing => $doing,
        done  => $done,
        all   => $all,
    );
};

/detail/:_id 컨트롤러는 다음과 같습니다. 따로 렌더러(뷰)가 없이 직접 json을 반환하는 점을 유의하세요. Ajax를 이용한 데이터 송수신을 위해서 추가하는 컨트롤러입니다.

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

    my $id         = $self->param('_id');
    my $item       = $mytodo->_dbix->table('mytodo')->find($id);
    my $created_on = localtime($item->created_on);
    my $updated_on = localtime($item->updated_on);

    $self->render_json({
        content    => $item->content,
        priority   => $item->priority,
        star       => "\x{2605}" x $item->priority . "\x{2606}" x (5 - $item->priority),
        _status    => uc($item->status),
        created_on => $created_on->ymd . ' ' . $created_on->hms,
        updated_on => $updated_on->ymd . ' ' . $updated_on->hms,
    });
};

렌더러(뷰)

뷰에서는 jQuery 모바일용 CSS와 자바스크립트를 사용합니다. 최대한 간단한 디렉터리 구조를 유지하고, 캐시 효과를 높이기 위해 직접 jQuery 모바일 관련 파일을 유지하지 않고 CDN의 자원을 사용함을 유의하세요. 로컬에서만 돌리겠다면 public 디렉터리를 구성한 다음 적절한 위치에 다운로드 받고 렌더러의 자원 URI를 수정해야합니다.

@@ list.html.ep
% layout 'list', navbar => 1, back => 0;
% title 'MyTodo';
<!-- CONTENT -->


@@ layouts/list.html.haml
!!! 5
%html
  %head
    %title= title
    = include 'layouts/default/meta'
    = include 'layouts/default/css'
    = include 'layouts/default/js'

  %body
    = include 'layouts/default/items', id => 'todo',  items => $todo
    = include 'layouts/default/items', id => 'doing', items => $doing
    = include 'layouts/default/items', id => 'done',  items => $done
    = include 'layouts/default/detail'


@@ layouts/default/items.html.ep
<!-- <%= uc $id %> -->
    <div id="<%= $id %>" data-role="page">
      %= include 'layouts/default/header', navbar => 1, new => 1
      <div data-role="content">
        <!-- CONTENT -->
        <div data-role="fieldcontain">
          <ul data-role="listview" data-split-icon="arrow-r">
            % while ( my $item = $items->next ) {
              <li>
                <a href="#" style="padding-top: 0px;padding-bottom: 0px;padding-right: 42px;padding-left: 0px;">
                  <label style="border-top-width: 0px;margin-top: 0px;border-bottom-width: 0px;margin-bottom: 0px;border-left-width: 0px;border-right-width: 0px;" data-corners="false">
                    <fieldset data-role="controlgroup" >
                      <input type="checkbox" name="checkbox-2b" id="checkbox-2b" />
                      <label for="checkbox-2b" style="border-top-width: 0px;margin-top: 0px;border-bottom-width: 0px;margin-bottom: 0px;border-left-width: 0px;border-right-width: 0px;">
                        <label  style="padding:0;">
                          <h3><%= $item->content %></h3>
                        </label>
                      </label>
                    </fieldset>
                  </label>
                </a>
                <a class="slide-reload" href="#detail" id="todo-item-<%= $item->id %>" data-transition="slide">Show details</a>
              </li>
            % }
          </ul>
        </div>
      </div>
      %= include 'layouts/default/footer'
    </div>


@@ layouts/default/detail.html.ep
<!-- DETAIL -->
    <div id="detail" data-role="page" data-add-back-btn="true">
      %= include 'layouts/default/header', navbar => 0, new => 0
      <div data-role="content">
        <h1 class="todo-content"></h1>
        <h2 class="todo-status"></h2>
        <h2 class="todo-priority"></h2>
        <div class="todo-etc"></div>
      </div>
      %= include 'layouts/default/footer'
    </div>


@@ layouts/default/meta.html.haml
/ META
    %meta{:charset => "utf-8"}
    %meta{:name => "author",      content => "Keedi Kim"}
    %meta{:name => "description", content => "MyTodo"}
    %meta{:name => "viewport",    content => "width=device-width, initial-scale=1"}


@@ layouts/default/css.html.ep
<!-- CSS -->
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.css" />


@@ layouts/default/js.html.ep
<!-- Javascript -->
    <script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
    <script src="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.js"></script>
    <script>
      $(document).ready(function() {
        $('a.force-reload').live('click', function(e) {
          var url = $(this).attr('href');
          $.mobile.changePage( url, { reloadPage: true, transition: "none"} );
        });
        $('a.slide-reload').live('click', function(e) {
          var url = $(this).attr('href');
          var id  = this.id.replace( /.*todo-item-/, "" );
          $.get(
            '/detail/' + id,
            function(_data) {
              $("#detail .todo-content").text(_data.content);
              $("#detail .todo-status").text(_data._status);
              $("#detail .todo-priority").text(_data.star);
              $("#detail .todo-etc").text('');
              $("#detail .todo-etc").append("<p>created: " + _data.created_on + "</p>");
              $("#detail .todo-etc").append("<p>updated: " + _data.updated_on + "</p>");
            },
            'json'
          );
        });
      });
    </script>


@@ layouts/default/navbar.html.ep
<!-- NAVBAR -->
        <div data-role="navbar">
          <ul>
            <li><a data-transition="none" class="<%= $id eq 'todo'  ? 'ui-btn-active ui-state-persist' : q{} %>" href="#todo">  Todo  </a></li>
            <li><a data-transition="none" class="<%= $id eq 'doing' ? 'ui-btn-active ui-state-persist' : q{} %>" href="#doing"> Doing </a></li>
            <li><a data-transition="none" class="<%= $id eq 'done'  ? 'ui-btn-active ui-state-persist' : q{} %>" href="#done">  Done  </a></li>
          </ul>
        </div>


@@ layouts/default/header.html.ep
<!-- HEADER -->
      <div data-role="header" data-position="fixed">
        % if ($new) {
          <a class="force-reload" href="/" data-icon="refresh">Refresh</a>
        % }
        <h1><%= title %></h1>
        % if ($new) {
          <a href="#" data-icon="plus" class="ui-btn-right">New</a>
        % }
        % if ($navbar) {
          %= include 'layouts/default/navbar'
        % }
      </div>


@@ layouts/default/footer.html.haml
/ FOOTER

Rock 'n Roll!!

모두 완료되었습니다. 다음 명령으로 웹앱을 실행할 수 있습니다.

$ PERL5LIB=lib morbo mytodo-web.pl 
[Mon Dec 24 16:59:00 2012] [debug] Reading config file "/home/askdna/workspace/github/mytodo/mytodo-web.conf".
[Mon Dec 24 16:59:00 2012] [info] Listening at "http://*:3000".
Server available at http://127.0.0.1:3000.
...

이제 휴대폰을 이용해서 접속하면 다음과 같은 화면을 볼 수 있습니다.

iPhone에서 접속한 첫 화면

그림 1. iPhone에서 접속한 첫 화면

상단 네비게이션 바로 이동하는 화면

그림 2. 상단 네비게이션 바로 이동하는 화면

각각의 To-Do 항목의 세부 사항

그림 3. 각각의 To-Do 항목의 세부 사항

목록 화면에서 아이템 선택

그림 4. 목록 화면에서 아이템 선택

제법 그럴듯하죠? :)

사실 현재 다음 기능은 빠져 있습니다.

남은 부분은 여러분의 숙제로 남겨두도록 하죠. MojoliciousjQuery 모바일 공식 문서를 참고해서 한 번 도전해보세요! :-)

패키징

여기까지 잘 따라왔다면 이제 패키징은 덤입니다. :-)

dist.ini 파일의 version 항목을 0.001로 수정합니다. 더불어 Changes 파일에 지금까지 작업한 내역을 적절하게 적어 넣고, dist.ini에 기입한 버전 정보를 적어줍니다. 다음은 변경 내역입니다.

diff -urN a/Changes b/Changes
--- a/Changes   2012-12-24 17:15:04.863580223 +0900
+++ b/Changes   2012-12-24 17:14:48.943579620 +0900
@@ -1,4 +1,7 @@
 Release history for MyTodo

-0.XXX
-    First version, released on unsuspecting world.
+0.001
+    - Add MyTodo main module
+    - Patch MooX::Options
+    - Add mytodo.pl utility
+    - Add mytodo-web.{pl|conf} web app
diff -urN a/dist.ini b/dist.ini
--- a/dist.ini  2012-12-24 17:13:22.471576343 +0900
+++ b/dist.ini  2012-12-24 17:13:48.999577347 +0900
@@ -3,7 +3,7 @@
 license          = Perl_5
 copyright_holder = Keedi Kim
 copyright_year   = 2012
-version          = 0.000
+version          = 0.001

 ;[@Basic]
 [@Filter]

자, 타르볼을 만들어봅시다!

$ ls
Changes  bin  dist.ini  lib  mytodo-web.conf  mytodo-web.pl
$ dzil build
[DZ] beginning to build MyTodo
[DZ] guessing dist's main_module is lib/MyTodo.pm
[DZ] extracting distribution abstract from lib/MyTodo.pm
[@Filter/ExtraTests] rewriting release test xt/release/pod-coverage.t
[@Filter/ExtraTests] rewriting release test xt/release/pod-syntax.t
[DZ] writing MyTodo in MyTodo-0.001
[DZ] building archive with Archive::Tar; install Archive::Tar::Wrapper for improved speed
[DZ] writing archive to MyTodo-0.001.tar.gz
$ ls
Changes  MyTodo-0.001  MyTodo-0.001.tar.gz  bin  dist.ini  lib  mytodo-web.conf  mytodo-web.pl

유후~! :-D

정리하며

크리스마스 달력 기사라고 하기엔 무척 긴 호흡의 글이 되었네요. 어떻게 보면 짧은 기사에서 단순한 스크립트가 아닌 완전한 객체지향 모듈을 제작했습니다. 더불어 ORM을 이용해서 데이터베이스에 접속했으며, 유연한 펄 모듈 덕에 다양한 데이터베이스를 지원할 수 있었습니다. 이 모듈을 활용해서 명령줄 유틸리티를 만들었으며, 명령줄 유틸리티는 아주 다양한 옵션을 지원합니다. 또한 이를 위해 사용한 모듈은 명령줄 유틸리티가 도움말을 출력할때 순서를 지정할 수 없는 한계가 있는데, 이를 시스템에 설치한 모듈을 손대지 않고 로컬 환경에서만 적용할 수 있도록 패치도 했습니다. 마지막으로 웹과 모바일을 지원하기 위한 미려한 웹앱을 만들었고 이 때 작성했던 객체지향 모듈을 재사용해서 활용성을 높였습니다. 그리고 지금까지 작업한 모든 내용은 단 한 줄의 명령을 이용해서 릴리스용 타르볼을 만들 수도 있게 되었습니다!! 현재 POD를 이용한 문서화만이 빠져 있는데 실제 내용을 넣기 위한 모든 플레이스홀더를 이미 만들어 두었으니 문서화를 마무리하는 것도 여러분에게 맡기도록 하죠.

정말 놀랍지 않나요? 적어도 펄 프로그래머에게 있어 여러분이 만들 수 있는 프로그램의 한계는 여러분의 상상력에 닿아 있을 것입니다.

Enjoy Your Perl! ;-)

Don't forget fork me on GitHub!! ;-)

blog comments powered by Disqus