Mojolicous 삽질기
-
- 1. 개요
-
- 2. 관련 문서
-
- 3. app 파일 생성
-
- 4. placeholder 이름 조심
-
- 5. 요청,응답의 인코딩과 한글로 된 파일명 인코딩 문제
-
-
- 5.1. 입출력되는 내용은 자동으로 인코딩, 디코딩 된다.
-
- 5.2. 정적 파일 서비스의 파일명 인코딩
-
- 5.3. Mojo::Path 모듈 테스트 결과 - 실패
-
- 5.4. Directory 플러그인을 사용한 디렉토리 인덱스 서비스에서 한글 깨짐 - 패치 필요
-
6. static 파일 렌더링 시 파일이 없는 경우 처리
-
- 7. URI Escape
-
- 8. 정규식 사용할 때 /o 옵션 주의
-
- 9. Basic authentication
-
- 10. 파일 업로드
-
- 11. 로그
-
- 12. 리버스 프록시를 쓸 때 URL 프로토콜 반영
-
- 13. Comments
-
주인장은 웹에 대해서는 거의 UseModWiki소스수정 하면서 기존 소스 코드와 /CGI모듈 문서 보고 웹 뒤지면서 알아낸 정도밖에 모른다.
- html, head, title, body, i, b, ol, ul 등의 태그만 대충 안다. 속성이니 스타일이니 이런 건 위키 소스 수정하면서 그때그때 잠깐 알았다가 며칠 지나면 다 잊어버린다.
- CGI가 뭔지 조금 안다.
- javascript는 거의 모른다.
- mod_proxy니, FastCGI니, 웹 프레임워크니, MVC 등은 이름만 들어봤다.
... 내가 어떻게 위키소스수정을 해오고 있는지 신기할 정도다.
어쨌거나 이런 상태에서, Catalyst, Dancer, Mojolicious 등의 문서를 읽어보면... 첫 문서, 특히 튜토리알 문서는 다들 "이젠 정말 쉽고 빠르게 웹서비스를 만들 수 있다!"라고 말을 한다. 첫번째 예제를 보면 정말 그런 것 같다. 그런데 당장 두번째 예제부터... "도대체 무슨 소리를 하는 건지"도 잘 모르겠다 -_-; 막상 이걸 써서 뭘 만들어보려고 해도 무엇부터 시작해야 하는지도 모르겠다.
이번 [KPW2012]에서도 keedi님 발표에서 간단한 웹 애플리케이션을 만드는 용도로 Mojolicious를 강력 추천하셨는데... 그래서 튜토리알 문서를 보면서 "음음 그렇군"하고는 응용하려고 변수 이름 하나 바꿨더니 또 동작을 안 하질 않나... 그 원인을 알았는데 이것도 또 시간 지나면 잊어버릴 것 같고 해서, 이런 거 기록해둘 겸 하여 적어두기로 함.
2. 관련 문서
http://mojolicio.us/perldoc 페이지에 있는 문서들 중:
[Minimal Perl WebApp for Your Minimal Life] - keedi님의 KPW2012 발표자료 [5]
3. app 파일 생성
서버에 적당한 디렉토리를 만들고, 거기서 앱 파일을 만들어보자. 그냥 빈 파일로 시작해도 되지만 [5]나 튜토리알 문서대로 생성기를 사용
$ mojo generate lite_app index.pl
그러면 index.pl 파일이 생긴다.
#!/usr/bin/env perl
use Mojolicious::Lite;
plugin 'PODRenderer';
get '/' => sub {
my $self = shift;
$self->render('index');
};
app->start;
@@ index.html.ep
% layout 'default';
% title 'Welcome';
Welcome to the Mojolicious real-time web framework!
@@ layouts/default.html.ep
<html>
<head><title><%= title %></title></head>
<body><%= content %></body>
</html>
그런데 서버에 설치된 Perl 은 5.8이고, Mojolicious 는 5.10 이상이 필요하다. 첫 줄 쉬뱅라인을 내 홈디렉토리에 설치된 펄로 바꿔준다.
#!/home/gypark/perl5/perlbrew/perls/perl-5.14.2/bin/perl
실행은
$ perl index.pl daemon # 이 경우 스크립트를 수정하면 재시작해야 함
또는
$ morbo index.pl # 스크립트를 수정하면 자동으로 로드됨
기본적으로 3000번 포트를 쓴다. 바꿔주고 싶다면 -l
옵션을 준다. 또는 MOJO_LISTEN
환경변수에 지정해 준다.
$ perl index.pl daemon -l http://*:9999
[Tue Nov 20 23:28:07 2012] [info] Listening at "http://*:9999".
Server available at http://127.0.0.1:9999
이제 웹브라우저에서 http://서버주소:포트번호
로 접속해서 첫화면이 나오는 걸 확인할 수 있다.
4. placeholder 이름 조심
튜토리알[1]에서 placeholder 예제를 조금 수정해서, __DATA__ 섹션에 있는 html을 렌더링하게 해 보았다.
get '/foo/:bar' => sub {
my $self = shift;
$self->render('foo');
};
@@ foo.html.ep
<html>
<body>bar is <%= $bar %></body>
</html>
이제 /foo/test
등의 경로로 접근하면 bar is test
와 같이 출력이 된다.
여기까지는 좋은데... 내가 별 생각 없이 :bar
라는 이름 대신 :text
라는 이름을 사용했다:
get '/foo/:text' => sub {
my $self = shift;
$self->render('foo');
};
@@ foo.html.ep
<html>
<body>text is <%= $text %></body>
</html>
그랬더만 앞에 "text is"는 출력이 되지 않고 계속 "test"만 나오더라.
나중에야 알았는데 text
는 예약된 stash value이고, render(text => '...')
구문에서 쓰인 text 와 같은 맥락이다. 그래서 곧바로 저 경로명의 "test" 부분만이 출력되었던 것.
예약어 목록은 [Mojolicious::Controller 문서의 stash 항목]에서 볼 수 있다.
5. 요청,응답의 인코딩과 한글로 된 파일명 인코딩 문제
이 섹션의 내용은 주인장이 뭔가 잘못 알고 있거나 더 편한 해결책을 모르고 있는 것일 수 있음.
5.1. 입출력되는 내용은 자동으로 인코딩, 디코딩 된다.
Mojolicious::Lite은 자체에 strict, warnings, utf8 프라그마 등이 내장되어 있다. 이 모듈을 써서 만든 코드의 라우트 핸들러 내부에서는, 들어온 요청은 이미 다 디코드되어 있는 '펄 내부 스트링'의 형태가 된다. 반대로, 출력할 때는 UTF-8로 인코딩된 형태로 출력된다(이건 바꿀 수 있는 듯).
즉 브라우저 주소창에 http://127.0.0.1:3000?var=한글
이렇게 넣는다면 저 "한글"을 라우트 핸들러 내부에서 my $param = $self->param('var')
이렇게 읽었을 때 $param 은 펄 스트링 "한글"이 들어간다는 얘기. (그나마도 이걸 확인하려면 IE로는 안 됨)
어쨌거나 요청이 그렇게 들어오는 거야 편하다 치고,
출력할 때도 자동으로 UTF-8로 인코딩되는 것 때문에, 윈도에서 실행할 때 좀 묘하다.
get '/' => sub {
app->log->debug("디버그");
app->log->debug( encode('cp949',"디버그") );
say "한글";
say encode('cp949',"한글");
$self->render_text("Hello 안녕하세요");
}
5.2. 정적 파일 서비스의 파일명 인코딩
정적 파일 서비스를 할 때도 신경을 써주어야 한다. 아래 코드는... 이상하게 지난 주말에 할 때는 죽어도 안 되던데, 오늘(2013년 4월2일) 이 글을 작성하려고 하니까 잘된다 -_-;;; 내가 주말에 뭔가를 잘못하고 있었거나, 그 사이에 Mojolicious 모듈을 업데이트를 했는데 그게 관련이 있을지도 모르겠다.
push @{app->static->paths}, 'D:/Work/Perl/Mojolicious';
get '/' => sub {
my $self = shift;
my $p;
$p = '한글디렉토리/한글.txt';
$self->render_static(encode('cp949',$p));
};
5.3. Mojo::Path 모듈 테스트 결과 - 실패
Mojo::Path 모듈에서 charset이라는 속성이 있어서, 혹시 이걸 쓰면 깔끔하게 되지 않을까 했는데, 이건 한글은 %인코딩해버리더라. 이건 URL에 사용하는 용도이지 파일 경로에 쓰기에는 힘들 듯.
$p = '한글디렉토리/한글.txt';
my $path = Mojo::Path->new($p);
$path->charset('cp949');
say Dumper($path);
$self->render_static( "$path" );
$self->render_static( $path->to_string );
$self->render_static(encode('cp949',$path->to_string));
5.4. Directory 플러그인을 사용한 디렉토리 인덱스 서비스에서 한글 깨짐 - 패치 필요
Mojolicious 플러그인 중에, Mojolicious::Plugin::Directory이란 게 있다. 이것은 아예 특정 디렉토리 이하를 아파치 웹서버에서 directory index로 보여주는 것처럼 똑같이 보여준다.
plugin Directory => {
root => 'D:/Work/Perl/Mojolicious',
charset => 'cp949',
};
app->start;
브라우저에서 http://127.0.0.1:3000/english_dir/
을 부른다면 실제 경로는 D:/Work/Perl/Mojolicious/english_dir
이 되기는 하는데...
그림에서 보다시피 한글은 와장창 깨진다. 브라우저 인코딩을 고쳐도 소용없다. 이미 저 출력 자체가 (아마도) 엉뚱하게 인코딩되면서 깨진 것이다. 보기에만 깨져보이는 게 아니라 실제 클릭을 해도 제대로 파일을 읽거나 디렉토리 안으로 들어가지 못한다.
이와 관련해서는 aer0 님이 저 모듈을 패치를 하셨다.
잠깐 테스트해 본 바로는 아주 잘 동작한다. :-)
6. static 파일 렌더링 시 파일이 없는 경우 처리
$self->render_static($filepath) || $self->render_not_found;
not found 처리를 해 주지 않을 경우, $filepath에 해당하는 파일이 없으면, 브라우저가 꽤 오랜 시간 응답을 기다리다가 타임아웃 걸리게 되더라. render_static이 그 처리를 따로 해주진 않는 듯.
7. URI Escape
Mojolicious::Plugin::TagHelpers에 있는 여러 헬퍼들 중 예를 들어 image
나 link_to
같은 것들은 자기가 알아서 적절하게 URI escape 처리를 한다. 그러나, 이스케이프된 부분과 되지 않은 부분이 섞여 있을 경우 문제가 됨
@@ index.html.ep
% my $str = "가";
%
%= link_to "link to $str" => "/$str"
%= link_to "link to $str" => '/%EA%B0%80'
%
%
%= link_to "link to $str" => "/%EA%B0%80/$str"
%
%
그러니 인자로 들어가는 곳은 전부 이스케이프되어 있든지, 아니면 전부 안 되어 있든지 해야 함.
보통은 굳이 번거롭게 이스케이프하지 않아도 알아서 잘 처리해주는데, 예를 들어 경로에 #
이 들어간 경우가 있다(파일경로명에 #이 허용되니까). 그런데 link_to는 이걸 URL의 앵커를 나타내는 마크로 간주하여 그냥 놔두기 때문에, 브라우저에서 클릭을 할 때 요청 경로명은 저 #과 뒷부분의 내용이 잘려버린 상태가 된다.
8. 정규식 사용할 때 /o 옵션 주의
GET또는 POST요청에서 받은 인자를 가지고 정규식 패턴으로 사용하려는데, 희한하게 스크립트를 실행하고 처음 한 번은 잘 되는데 그 다음부터는 매치가 안 되는 증상이 나오더라.
my $pattern = $self->param('pattern');
foreach my $file ( @files ) {
if ( $file =~ /$pattern/o ) {
...
}
}
루프 안에서 패턴이 바뀌지 않기 때문에, 효율을 높인다고 /정규표현식의 /o
옵션을 준 게 문제였다. 모조 서버는 계속 실행중이기 때문에, $pattern 값이 바뀌어도 한 번 컴파일된 저 정규식은 바뀌지 않기 때문에 계속 제일 처음 만들어졌던 패턴을 가지고 매치를 시도하고 있었던 것.
CGI를 쓰던 습관 때문에 매번 브라우저 요청이 들어갈 때마다 스크립트가 재시작하는 것처럼 여기다보니 생긴 문제. 저 옵션은 없애고 루프 밖에서 qr/$pattern/
으로 컴파일하도록 하였다.
9. Basic authentication
아파치 등에서처럼 basic auth 기능을 쓰려면, Mojolicious::Plugin::BasicAuth 모듈을 따로 설치한다.
저 모듈 문서의 시놉시스에는 '/' URL에 대해서만 적용이 되게 하고 있는데, 모든 경로에 다 저렇게 코드를 적어주는 건 말이 안 되니까, Mojolicious::Lite#Under 에 나온 under 구문을 사용하자.
그리고 코드에 암호를 그대로 적는 게 좀 보기 안 좋으니, .htpasswd 에 저장되는 형태로 암호를 저장한 걸 가지고 비교를 하게 해보자.
plugin 'basic_auth';
my $str = 'user:J4Cuzq8zvX6dA';
my ($user, $pass) = split(':', $str);
under sub {
my $self = shift;
return 1 if $self->basic_auth(
realm => sub {
return 1 if ( $_[0] eq $user
and
crypt($_[1], $pass) eq $pass );
}
);
$self->render_text('NOT OK');
return undef;
};
get '/' => sub {
my $self = shift;
return $self->render_text('ok')
};
10. 파일 업로드
참고: [열여덟번째 날: Mojolicious - 폼 파라메터와 파일 업로드 처리 | Seoul.pm 펄 크리스마스 달력 #2015]
다음과 같은 폼을 써서 업로드를 할 때
<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="upload" />
<input type="submit" />
</form>
Mojo::Upload 객체로 들어오며, 다음과 같이 처리할 수 있다. 그 외 메소드는 모듈 문서 참고.
my $upload = $self->param('upload');
$upload->move_to('filename');
my $hash = $self->req->params->to_hash;
Mojo::Log를 사용한 로그는 표준에러로 나오는데, 만일 log
디렉토리가 있다면 그 디렉토리 아래에 현재모드.log
파일에 기록된다. 어느 순간 화면에 로그가 나오지 않아 당황했는데 파일로 기록되고 있었음. 이때 log 디렉토리는 "현재 작업 디렉토리" 아래가 아니라, 앱 스크립트가 있는 디렉토리 아래에 있어야 적용되더라.
12. 리버스 프록시를 쓸 때 URL 프로토콜 반영
Nginx를 리버스 프록시로 사용해서 그 위에서 Mojo 앱이 실행되는데, 웹브라우저-Nginx간은 HTTPS를 쓰는데 모조 앱에서 `url_for(..)->to_abs`를 해보면 프로토콜이 http로 나오는 문제가 있었다. 모조 앱 입장에서는 프록시와 자신간의 연결이 그냥 http 라서. 이것은 `redirect_to`등 URL을 자동으로 생성해주는 루틴들에 공히 적용되는 듯.
해결책:
Nginx 쪽에서는 `X-Forwarded-Proto` 헤더를 써서 현재 프로토콜을 앱으로 전달한다.
upstream ssltest {
server 127.0.0.1:9000;
}
server {
listen 80;
listen 443 ssl;
server_name ssltest.server.com;
location / {
proxy_pass http://ssltest/;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 이것과
proxy_set_header X-Forwarded-Proto $scheme; # 이것
}
}
이것을 모조 앱 쪽에서 이용해야 하는데,
앱을 실행하는 데 Hypnotoad 를 쓴다면(나는 안 써봤지만)
proxy => 1
# myapp.conf
{hypnotoad => {proxy => 1}};
앱을 실행하는 데 Plack을 쓴다면
MOJO_REVERSE_PROXY=1 plackup ./script/my_app
실행 환경 쪽을 손대지 않고 앱 자체에서 넘어온 헤더값을 반영하는 경우는
hook 'before_dispatch' => sub {
my $c = shift;
$c->req->url->base->scheme( $c->req->headers->header('X-Forwarded-Proto'))
if $c->req->headers->header('X-Forwarded-Proto');
};
13. Comments
기타분류