여섯째 날: 초소형 프레임워크와 함께 춤을

저자

@am0c - 단것과 귀여운 것을 좋아하는 올해 크리스마스 달력의 주편집자. 하지만 웹디자인에 더 열중했다는 비하인드 스토리. 결국 @keedi님의 도움을 받고 있다. 최근 안드로이드와 리눅스 커널에 관심을 돌리고 있으나 페이스북의 재미에 빠져 허우적대고 있다.

시작하며

웹사이트 개발 패러다임도 계속 변하고 있습니다.

CGI가 쌘 놈이었던 때도 있었다고 합니다. 물론 CGI는 이미 흘러간 물입니다. 몇 년 전에는 철도 위에 놓인 루비가 붐이었죠. 지금은 웹 클라우딩뇨인지 클라우드인지 하는 플랫폼 서비스 사업이 일어나고 있습니다.

저는 웹 개발자도 아니고 HTML5를 어서 만져보고 싶어하는 덕후도 아니기 때문에 이런 패러다임에 대해 시시각각 논하지는 못합니다만, 취미로 만든 서비스가 완성되면 보기 좋기 운영하거나 외부에 공개허거나 인터페이스를 제공하고 싶게 되고, 그러다보면 어쨌든 웹 서비스 개발에 직면하게 됩니다.

무엇으로 만들까

생각해보니 올해 부산에서 열린 지스타에 가지 못했습니다. 현장에서 찍은 사진들을 인터넷에서 구해 아름답게 정리해보고 싶습니다. 부스별로 정리하거나 색상별로 정리하면 즐거울 것 같군요.

일일이 받아서 폴더에 전부 쑤셔넣는 것은 짜증나는 일입니다. 지금은 정말 간단하게 테스트 페이지만 만들고 나중에 다양한 카테고리의 사진들도 관리하도록 확장하고 싶습니다. 아무래도 지금이 바로 웹 서비스가 필요한 상황이군요. 그런데 무엇으로 만드는 것이 가장 적합할까요.

Perl에는 아주 좋은 웹 프레임워크가 있습니다. 바로 Catalyst라고 불리는 놈입니다. 참고로 카탈리스트는 크리스마스 달력을 진행하고 있습니다. 어쨌든 이것만 있으면 아주 세밀하고도 거대하게 웹을 개발할 수 있고, 대량의 플러그인까지 존재합니다.

그런데 작은 블로그나 웹 페이지 하나 만드는데 이만한 것이 필요한 것은 아닙니다. 그렇다고 정해진 틀에만 구애받아야 하는 PHP의 CodeIgniter는 사용하고 싶지 않습니다. PHP 페이지 몇개로 include를 나열하는 것은 더 소름끼치는 일입니다. 맙소사, 이 상황에 Java를 사용하라고 하진 않겠죠?

그러니까 너무 간단한 나머지 불필요한 타이핑은 존재하지 않으며, 동시에 유연하고 확장 가능한 것을 원하는 겁니다. 물론 의존하고 있는 라이브러리가 너무 많은 것도 원하지 않습니다. 웹 프레임워크의 설정을 편집하느라 허송세월하고 싶지도 않습니다. 그런건 아무래도 없을테니 올해 크리스마스도 TV와 함께 보내야 하겠군요. 그렇게 슬퍼하고 있을 때,

놀랍게도 그런게 있었다는 겁니다. 바로 댄서입니다.

use Dancer;

get '/' => sub {
    "I can code in Christmas!!"
};

dance;

세상에 이런것이!

위의 코드는 완전한 코드입니다. 즉 웹브라우저에 /로 요청을 하면 I can code in Christmas!!가 출력됩니다. 이보다 간단할 수는 없습니다.

각 라우트 핸들러는 기본적으로 사용자 함수(sub)입니다. GET 요청을 위해 get을 POST 요청을 위해 post를 연결합니다.

get '/admin' => sub { }
post '/send' => sub { }

이렇게 간단하게 특정 정적 경로에 대한 핸들러를 기술할 수 있습니다. 물론 패턴 매치를 수행하고 파라미터로 받을 수 있습니다.

get '/post/id/:id' => sub { 
   my $id = params->{id};
}

정말 간단하죠? 요청을 통해 들어온 쿼리나 메시지도 이렇게 받아낼 수 있습니다. 또 아래와 같이 정적인 파일을 전달할 수도 있습니다.

get '/download/:file' => sub {
   my $file = params->{file};
   send_file $file;
}

헤더를 변경합니다.

get '/' => sub {
   content_type 'text/plain';
   ...
}

리다이렉트도 간단합니다.

get '/admin' => sub {
   redirect '/login';
}

탬플렛을 지정합니다.

get '/welcome' => sub {
   template 'welcome', { user => guest' };
}

로그를 기록합니다.

get '/error' => sub {
   debug 'TODO or not TODO';
}

와우! 깔끔해요. :)

만들어 봅시다

지금까지는 신택스를 보았습니다. 이번에는 실제로 웹 사이트를 만들어 봅시다. 웹에서 많이 사진을 끌어오고 쉽게 관리하는 서비스를 만들어 보겠습니다. 웹 어플리케이션 이름은 YouPerl이라고 지어보았습니다. CPAN 도구를 통해 Dancer를 설치한 후 아래와 같이 명령을 내리면 YouPerl이라는 웹 어플리케이션을 위한 파일 뼈대들이 생성됩니다.

$ dancer -a YouPerl
$ cd YouPerl

find를 입력하여 전체 구조를 살펴봅시다. 아래는 파일을 어느정도 생략한 모습입니다.

$ find
./views
./bin
./bin/app.pl
./t
./public
./environments
./lib
./lib/YouPerl.pm

/views 디렉터리에는 탬플릿이 위치합니다. /bin에는 실행도구들이 있습니다. /t디렉터리에는 테스트 스크립트가 모여 있습니다. /public 디렉터리 이하의 파일들은 정적 파일로 외부에 노출됩니다. 기본적인 설정은 /config.yml에 있지만 /environments 디렉터리 밑에 개발버전과 정식버전으로 나눈 설정을 따로 보관합니다. /lib 이하에 웹 어플리케이션의 로직 코드가 위치합니다.

아래와 같이 입력하고 웹부라우저를 통해 http://localhost:3000/으로 접근해봅시다.

$ bin/app.pl

이미 완성?

이미 완성된 것 같은 페이지가 뜹니다. 여기에 나타난 링크를 따라가 강좌와 문서를 읽어봅시다.

재료를 준비합니다

먼저 웹을 통해 지스타 이미지를 긁어 데이터베이스에 기록합니다. 데이터베이스는 Redis를 사용하겠습니다. Dancer::Plugin::Redis를 받습니다.

$ cpanm Dancer::Plugin::Redis

Redis 플러그인은 설정 몇줄만 넣으면 아래와 같이 redis 키워드를 통해 접근할 수 있습니다.

use Dancer;
use Dancer::Plugin::Redis;

get '/widget/view/:id' => sub {
    template 'display_widget', { widget => redis->get('hash_key'); };
};

dance;

설정은 /config.yml을 열고 마지막에 다음 네 줄을 입력하면 됩니다.

plugins:
  Redis:
    server: 127.0.0.1:6379
    debug:  0

아래 구글을 통해 검색해보니 지스타 이미지를 모은 24개의 게시물이 보입니다. Web::ScraperWeb::Query를 사용해도 되지만, 24개의 게시물의 경로를 구하기 위한 정규표현식 한 개와 각 게시물에서 이미지를 긁어올 정규표현식 한 개만 있으면 모든 이미지를 간단히 긁어올 수 있겠습니다.

발견

my $grep_srclist = qr|<a href="([^"]+)".{1,30}지스타 2011 부스걸 사진 보기|si;
my $grep_imglist = qr|http://p.playforum.net/[^"]+|si;

긁은 이미지의 파일명을 서로 겹치지 않게 하기 위해 MD5 알고리즘을 이용하고, 이것을 데이터베이스에서 각 파일의 식별자로 사용하겠습니다. GD::Thumbnail을 사용하면 쉽게 썸네일을 생성할 수도 있습니다. bin/grep_gstar.pl으로 저장합니다..

#!/usr/bin/env perl
use utf8;
use 5.010;
use Dancer qw();
use Dancer::Plugin::Redis;
use Digest::MD5 qw(md5_hex);
use GD::Thumbnail;
use File::Basename;
use LWP::Simple;

## 시작 전 데이터베이스 항목 초기화
redis->setnx("youperl:img:cat.gstar:page", 0);

## 긁을 페이지 목록을 구하는 구하는 정규표현식과
## 각 페이지에서 이미지를 긁을 정규표현식
my $grep_srclist = qr|<a href="([^"]+)".{1,30}지스타 2011 부스걸 사진 보기|si;
my $grep_imglist = qr|http://p.playforum.net/[^"]+|si;

## 일단 첫번째 페이지를 가져와요!
my $first_page = 'http://www.playforum.net/www/newsDirectory/-/id/1047955';
my $src_list = get $first_page;

## 페이지 목록을 긁어요!
my @urls = $src_list =~ m/$grep_srclist/g;
my @imgs;

## 각 페이지를 순회하면서 이미지 목록을 긁어요!
for my $url ($first_page, @urls) {
    say " <- $url";
    my $src_imgs = get $url;
    push @imgs, $src_imgs =~ m/$grep_imglist/g;
}

## 각 이미지를 처리합니다.
for my $img (@imgs) {
    say $img;
    my ($name, $path, $suffix) = fileparse $img, qr/\.[a-z]+/i;
    my $hex = md5_hex "youperl_$path$name";
    my $fn = "public/img/$hex$suffix";
    my $thumb = "public/img/thumb/$hex$suffix";
    my $i;

    ## 유일한 파일명을 구해요.
    while (-e $fn) {
        ++$i;
        $fn = sprintf "public/img/${hex}_%d$suffix", $i;
        $thumb = sprintf "public/img/thumb/${hex}_%d$suffix", $i;
    }
    $hex = "${hex}_$i" if defined $i;

    ## 이미지를 저장하구요.
    say " -> $fn";
    getstore $img, $fn unless -e $fn;

    ## 작은 파일은 필요 없습니다.
    if (-s $fn < 1024 * 8) {
        say " xx $fn";
        unlink $fn;
        next;
    }

    ## 썸네일도 생성합시다.
    say " -> $thumb";
    my $t = GD::Thumbnail->new;
    my $raw = $t->create($fn, 140, 0);
    open my $fh, '>', $thumb or die;
    binmode $fh;
    print $fh $raw;

    say " -o $fn";
    ## 데이터베이스에 400개씩 기록합니다.
    my $page = redis->get("youperl:img:cat.gstar:page");
    my $size = redis->llen("youperl:img:cat.gstar:$page");
    if ($size >= 400) {
        $page = redis->incr("youperl:img:cat.gstar:page");
    }
    redis->lpush("youperl:img:cat.gstar:$page", $hex);
}

긁어온 모습입니다.

잔뜩

심심하지 않은 웹서비스를 만들기 위해 각 이미지를 색상별로 분류해 봅니다. 역시 데이터베이스에 기록합니다. bin/calc_gstar.pl으로 저장합니다.

#!/usr/bin/env perl
use 5.010;
use Dancer qw();
use Dancer::Plugin::Redis;
use File::Basename;
use GD;

## 마음대로 골라본 색상 샘플 목록

my %sample = (
    gray    => [177, 177, 177],
    black   => [0,   0,   0  ],
    red     => [255, 0,   0  ],
    magenta => [255, 0,   255],
    blue    => [0,   0,   255],
    cyan    => [0,   255, 255],
    green   => [0,   255, 0  ],
    yellow  => [255, 255, 0  ],
    white   => [255, 255, 255],
    ocean   => [125, 148, 183],
    grass   => [125, 183, 133],
    sky     => [125, 183, 174],
    flower  => [183, 125, 181],
    stone   => [183, 174, 125],
    wood    => [183, 125, 125],
);

my $page = redis->get("youperl:img:cat.gstar:page");
for my $p (0 .. $page) {
    my @items = redis->lrange("youperl:img:cat.gstar:$p", 0, -1);
    for my $item (@items) {
        my ($img) = glob "public/img/$item.*";
        my $color_name = sampling($img);

        redis->sadd("youperl:img:cat.gstar.color:$p:$color_name", $item);
    }
}


## 가장 가까운 색을 고르자
##
sub sampling {
    my $file = shift;
    return unless -f $file;

    my ($hex, $path, $suffix) = fileparse $file, qr/\.[a-z]+/i;

    my %dist;
    my $image = new GD::Image($file);
    my $color = new GD::Image(1, 1);

    ## 이미지 평균 색상값을 구한다.
    ##
    $color->copyResampled(
        $image,
        (0, 0),   (0, 0),
        (1, 1),   ($image->width, $image->height),
    );

    my $index = $color->getPixel(0, 0);

    ## 샘플 색상과 각각 비교해본다
    ##
    for my $name (keys %sample) {
        $dist{$name} = rgb_dist( $sample{$name}, [$color->rgb($index)] );
    }

    ## 오름차순으로 정렬한다
    ##
    my @sort = sort {
        $dist{$a} <=> $dist{$b}
    } keys %sample;


    print "$hex:\t $sort[0]\t   ";
    print "$_(", int $dist{$_}, ") " for @sort;
    print "\n";

    return $sort[0];
}

sub rgb2xyz {
    my ($r, $g, $b) = @_;
    my ($x, $y, $z);

    $r = $r / 255;
    $g = $g / 255;
    $b = $b / 255;

    for $c ($r, $g, $b) {
        if ($c > 0.04045) {
            $c = ($c + 0.055) / 1.055;
            $c = $c ** 2.4;
        }
        else {
            $c = $c / 12.92;
        }
        $c = $c * 100;
    }

    $x = $r * 0.4124 + $g * 0.3576 + $b * 0.1805;
    $y = $r * 0.2126 + $g * 0.7152 + $b * 0.0722;
    $z = $r * 0.0193 + $g * 0.1192 + $b * 0.9505;

    return $x, $y, $z;
}

sub xyz_dist {
    my ($l, $r) = @_;
    my ($x1, $y1, $z1) = @$l;
    my ($x2, $y2, $z2) = @$r;
    my $t = ($x1 - $x2) ** 2 + ($y1 - $y2) ** 2 + ($z1 - $z2) ** 2;
    return sqrt $t;
}

sub rgb_dist {
    my ($l, $r) = @_;
    return xyz_dist [rgb2xyz @$l], [rgb2xyz @$r];
}

색상을 대표하는 값은 GD의 copyResampled 함수를 사용하면 됩니다. "Perl Hacks" 책의 44번 항목을 참고했습니다. 색상별로 분류하는 방법은 다음의 문서를 참고했습니다.

래퍼 만들기

래퍼 파일(/views/layouts/main.tt)을 적절하게 수정합니다. 컨테이너 부분이 [% content %]로 되어 있습니다. 각 링크를 클릭하면 선택한 분류에 따라 컨테이너 안에 이미지가 다르게 출력되도록 할 것입니다. 색상 목록을 전역 변수로 빼놓는 것이 더 좋겠습니다. 지금은 그대로 두겠습니다.

<div id="menu">
  <ul>
    <li> <a href="/category/gstar">지스타</a>
    <li> <a href="/category/gstar/color/gray">gray</a>
    <li> <a href="/category/gstar/color/black">black</a>
    <li> <a href="/category/gstar/color/red">red</a>
    <li> <a href="/category/gstar/color/magenta">magenta</a>
    <li> <a href="/category/gstar/color/blue">blue</a>
    <li> <a href="/category/gstar/color/cyan">cyan</a>
    <li> <a href="/category/gstar/color/green">green</a>
    <li> <a href="/category/gstar/color/yellow">yellow</a>
    <li> <a href="/category/gstar/color/white">white</a>
    <li> <a href="/category/gstar/color/ocean">ocean</a>
    <li> <a href="/category/gstar/color/grass">grass</a>
    <li> <a href="/category/gstar/color/sky">sky</a>
    <li> <a href="/category/gstar/color/flower">flower</a>
    <li> <a href="/category/gstar/color/stone">stone</a>
    <li> <a href="/category/gstar/color/wood">wood</a>
  </ul>
</div>
<div id="cont">
  [% content %]
</div>

로직 작성하기

먼저 기본 페이지를 만들어야 겠군요. 기본 요청은 이것으로 그대로 둡시다.

get '/' => sub {
    template 'index';
};

index.tt는 간단한 문구로 완성합니다.

I can code in Christmas! :)

/category/gstar와 같이 카테고리를 부여하면 특정 카테고리에 해당하는 이미지를 모두 반환하도록 합니다. 템플릿으로 반환합니다.

get '/category/:category' => sub {
    my $name = param 'category';

    my @items = redis->lrange("youperl:img:cat.$name:0", 0, -1);
    my $files = items2files @items;

    template 'images', { images => $files };
};

래퍼는 이미 만들었으므로 이미지 템플릿은 단순히 이미지를 나열하도록 합시다.

[% FOREACH file IN images %]
  <a href="/img/[% file %]"><img src="/img/thumb/[% file %]" /></a>
[% END %]

지금은 탬플릿 언어로 Template Toolkit을 사용하고 있습니다. Dancer의 기본 탬플릿 언어는 Dancer::Template::Simple입니다. 탬플릿 언어를 바꾸려면 config.yml을 수정하면 됩니다. 개인적으로 Xslate이 짱입니다.

여기까지 완성하면 아래와 같이 사진이 잔뜩 올라옵니다.

이렇게요

/category/gstar/color/red와 같이 특정 색상을 필터하도록 요청해오면 특정 색상에 대한 항목만 반환하도록 합니다. 간단하죠?

get '/category/*/**' => sub {
    my ($category) = splat;
    var category => $category;
    pass;
};

prefix '/category/:category';
get '/color/:color' => sub {
    my ($color)  = param 'color';
    my $category = vars->{category};

    my @items = redis->smembers("youperl:img:cat.$category.color:0:$color");
    my $files = items2files @items;
    template 'images', { images => $files, color => $color };
};

이것으로 완성입니다. 색상에 따라 필터링도 할 수 있게 되었습니다.

  • 빨간색:

빨강

  • 파란색:

파랑

조금만 고치면 살색 사진만 모을 수도 있겠네요! (라고 Y님이 뒤에서 조언해주셨습니다.)

정리하며

배치 스크립트 탓에 예제가 조금 길어졌지만, 이렇게 간단한 서비스를 완성해 보았습니다.

물론 이것이 전부는 아닙니다. 다양한 데이터베이스와 세션 및 인증 모듈을 CPAN에서 얻을 수 있습니다. Task::Dancer를 설치하거나 이것의 펄독 문서를 참고하면 좋은 모듈을 찾는데 수월합니다. Dancer의 플러그인들은 모두 초간단 설정을 통해 동작하며 간단한 신택스를 통해 접근할 수 있습니다.

Dancer는 PSGIPlack을 지원합니다. 이것으로 여러분은 Dancer와 서버 사이에 통합적인 게이트웨이를 둘 수 있습니다. 이것이 있기 때문에 DotCloudStakato와 같은 클라우드 플랫폼 서비스에 Dancer 어플리케이션을 쉽게 deploy할 수 있습니다. 다른 환경으로 쉽게 이주할 수도 있을 것입니다.

댄서도 올해 크리스마스 달력을 진행하고 있습니다. 오늘 예제를 만들면서 댄서에서는 카탈리스트와 같이 라우트 핸들러를 체인으로 구성하는 것이 쉽지 않다는 것을 느꼈습니다. 이 부분에 대해 논의하면 좋겠습니다.

아래 첨부한 참고 링크에서 슬라이드는 꼭 읽어보세요. Dancer를 쉽게 이해하는데 도움이 됩니다. 또 Mojolicious라는 매력적인 웹 프레임워크도 있으니 한 번 둘러보세요. 마지막으로 필요하신 분들을 위해 이번 기사의 코드도 올려놓았습니다.

Let's Dance!

blog comments powered by Disqus