[첫화면으로]Perl/Mojolicious삽질기

마지막으로 [b]

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

1. 개요

주인장은 웹에 대해서는 거의 UseModWiki소스수정 하면서 기존 소스 코드와 /CGI모듈 문서 보고 웹 뒤지면서 알아낸 정도밖에 모른다.

... 내가 어떻게 위키소스수정을 해오고 있는지 신기할 정도다.

어쨌거나 이런 상태에서, Catalyst, Dancer, Mojolicious 등의 문서를 읽어보면... 첫 문서, 특히 튜토리알 문서는 다들 "이젠 정말 쉽고 빠르게 웹서비스를 만들 수 있다!"라고 말을 한다. 첫번째 예제를 보면 정말 그런 것 같다. 그런데 당장 두번째 예제부터... "도대체 무슨 소리를 하는 건지"도 잘 모르겠다 -_-; 막상 이걸 써서 뭘 만들어보려고 해도 무엇부터 시작해야 하는지도 모르겠다.

이번 [KPW2012]에서도 Twitter:keedi님 발표에서 간단한 웹 애플리케이션을 만드는 용도로 Mojolicious를 강력 추천하셨는데... 그래서 튜토리알 문서를 보면서 "음음 그렇군"하고는 응용하려고 변수 이름 하나 바꿨더니 또 동작을 안 하질 않나... 그 원인을 알았는데 이것도 또 시간 지나면 잊어버릴 것 같고 해서, 이런 거 기록해둘 겸 하여 적어두기로 함.

2. 관련 문서

http://mojolicio.us/perldoc 페이지에 있는 문서들 중:

[Minimal Perl WebApp for Your Minimal Life] - Twitter:keedi님의 KPW2012 발표자료 [5]

3. app 파일 생성

서버에 적당한 디렉토리를 만들고, 거기서 앱 파일을 만들어보자. 그냥 빈 파일로 시작해도 되지만 [5]나 튜토리알 문서대로 생성기를 사용

$ mojo generate lite_app index.pl
그러면 index.pl 파일이 생긴다.

#!/usr/bin/env perl
use Mojolicious::Lite;

# Documentation browser under "/perldoc"
plugin 'PODRenderer';

get '/' => sub {
  my $self = shift;
  $self->render('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>

그런데 서버에 설치된 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');
};
__DATA__
@@ foo.html.ep
<!DOCTYPE html>
<html>
  <body>bar is <%= $bar %></body>
</html>

이제 /foo/test 등의 경로로 접근하면 bar is test와 같이 출력이 된다.

여기까지는 좋은데... 내가 별 생각 없이 :bar라는 이름 대신 :text라는 이름을 사용했다:

get '/foo/:text' => sub {
    my $self = shift;
#     my $text = $self->stash('text');
    $self->render('foo');
};

__DATA__
@@ foo.html.ep
<!DOCTYPE html>
<html>
  <body>text is <%= $text %></body>
</html>

그랬더만 앞에 "text is"는 출력이 되지 않고 계속 "test"만 나오더라.

나중에야 알았는데 text는 예약된 stash value이고, render(text => '...') 구문에서 쓰인 text 와 같은 맥락이다. 그래서 곧바로 저 경로명의 "test" 부분만이 출력되었던 것.

예약어 목록은 [Mojolicious::Controller 문서의 stash 항목]에서 볼 수 있다.

5. 요청,응답의 인코딩과 한글로 된 파일명 인코딩 문제

이 섹션의 내용은 주인장이 뭔가 잘못 알고 있거나 더 편한 해결책을 모르고 있는 것일 수 있음.

5.1. 입출력되는 내용은 자동으로 인코딩, 디코딩 된다.

Cpan:Mojolicious::Lite은 자체에 strict, warnings, utf8 프라그마 등이 내장되어 있다. 이 모듈을 써서 만든 코드의 라우트 핸들러 내부에서는, 들어온 요청은 이미 다 디코드되어 있는 '펄 내부 스트링1'의 형태가 된다. 반대로, 출력할 때는 UTF-8로 인코딩된 형태로 출력된다(이건 바꿀 수 있는 듯).

즉 브라우저 주소창에 http://127.0.0.1:3000?var=한글 이렇게 넣는다면 저 "한글"을 라우트 핸들러 내부에서 my $param = $self->param('var')이렇게 읽었을 때 $param 은 펄 스트링 "한글"이 들어간다는 얘기. (그나마도 이걸 확인하려면 IE로는 안 됨2)

어쨌거나 요청이 그렇게 들어오는 거야 편하다 치고,

출력할 때도 자동으로 UTF-8로 인코딩되는 것 때문에, 윈도에서 실행할 때 좀 묘하다.

get '/' => sub {
    app->log->debug("디버그");                   # 자동으로 UTF-8로 인코딩되어 나가니까,
                                                 # cp949를 쓰는 윈도 콘솔에서는 깨진다.

    app->log->debug( encode('cp949',"디버그") ); # 미리 인코드하면, 이게 한번 더 인코드되니까 더 깨진다.

    say "한글";                                  # 역시 UTF-8이라서 깨지고, Wide character 출력 경고도 뜬다.

    say encode('cp949',"한글");                  # 이건 잘 나온다.

    $self->render_text("Hello 안녕하세요");      # 브라우저로 나가는 거야 뭐 UTF-8로 잘 나온다.
}

5.2. 정적 파일 서비스의 파일명 인코딩

정적 파일 서비스를 할 때도 신경을 써주어야 한다. 아래 코드는... 이상하게 지난 주말에 할 때는 죽어도 안 되던데, 오늘(2013년 4월2일) 이 글을 작성하려고 하니까 잘된다 -_-;;; 내가 주말에 뭔가를 잘못하고 있었거나, 그 사이에 Mojolicious 모듈을 업데이트를 했는데 그게 관련이 있을지도 모르겠다.

# 정적 파일은 기본적으로 "스크립트가 있는 디렉토리/public"을 기준으로 찾기 때문에,
# 그 외 경로를 지정해주려면 최상위 경로의 위치를 추가해주어야 한다. 일종의 document root 랄까.
push @{app->static->paths}, 'D:/Work/Perl/Mojolicious';

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

  # 한글이 들어있는 경로
  my $p;
  $p = '한글디렉토리/한글.txt';               # 이건 D:/Work/Perl/Mojolicious/한글디렉토리/한글.txt

  $self->render_static(encode('cp949',$p));   # encode가 필요.
};

5.3. Mojo::Path 모듈 테스트 결과 - 실패

Cpan:Mojo::Path 모듈에서 charset이라는 속성이 있어서, 혹시 이걸 쓰면 깔끔하게 되지 않을까 했는데, 이건 한글은 %인코딩해버리더라. 이건 URL에 사용하는 용도이지 파일 경로에 쓰기에는 힘들 듯.

  $p = '한글디렉토리/한글.txt';
  my $path = Mojo::Path->new($p);
  $path->charset('cp949');

  say Dumper($path);
# 출력 결과는
# $VAR1 = bless( {
#                 'charset' => 'cp949',
#                 'path' => "\x{d55c}\x{ae00}\x{b514}\x{b809}\x{d1a0}\x{b9ac}/\x{d55c}\x{ae00}.txt"
#                          <-- path는 유니코드로 들어가있다.
#               }, 'Mojo::Path' );

  $self->render_static(              "$path"           );   # stringfy 해봐도,
  $self->render_static(         $path->to_string       );   # to_string 메쏘드를 불러 스트링으로 뽑아도,
  $self->render_static(encode('cp949',$path->to_string));   # 스트링을 다시 encode 하려 해도,
  # 위 세가지는 다 "%C7%D1%B1%DB%B5%F0%B7%BA%C5%E4%B8%AE/%C7%D1%B1%DB.txt"를 찾으려 한다.
  # 이미 %인코딩되어 있으니 encode를 다시 불러도 소용 없음

5.4. Directory 플러그인을 사용한 디렉토리 인덱스 서비스에서 한글 깨짐 - 패치 필요

Mojolicious 플러그인 중에, Cpan:Mojolicious::Plugin::Directory이란 게 있다. 이것은 아예 특정 디렉토리 이하를 아파치 웹서버에서 directory index로 보여주는 것처럼 똑같이 보여준다.

plugin Directory => {
    root => 'D:/Work/Perl/Mojolicious',   # 여기가 DocumentRoot
    charset => 'cp949',
};

app->start;

브라우저에서 http://127.0.0.1:3000/english_dir/ 을 부른다면 실제 경로는 D:/Work/Perl/Mojolicious/english_dir이 되기는 하는데...

Upload:mojo_screenshot01.png

그림에서 보다시피 한글은 와장창 깨진다. 브라우저 인코딩을 고쳐도 소용없다. 이미 저 출력 자체가 (아마도) 엉뚱하게 인코딩되면서 깨진 것이다. 보기에만 깨져보이는 게 아니라 실제 클릭을 해도 제대로 파일을 읽거나 디렉토리 안으로 들어가지 못한다.

이와 관련해서는 Twitter:aer0 님이 저 모듈을 패치를 하셨다.

잠깐 테스트해 본 바로는 아주 잘 동작한다. :-)

6. static 파일 렌더링 시 파일이 없는 경우 처리

    $self->render_static($filepath) || $self->render_not_found;

not found 처리를 해 주지 않을 경우, $filepath에 해당하는 파일이 없으면, 브라우저가 꽤 오랜 시간 응답을 기다리다가 타임아웃 걸리게 되더라. render_static이 그 처리를 따로 해주진 않는 듯.

7. URI Escape

Cpan:Mojolicious::Plugin::TagHelpers에 있는 여러 헬퍼들 중 예를 들어 imagelink_to 같은 것들은 자기가 알아서 적절하게 URI escape 처리를 한다. 그러나, 이스케이프된 부분과 되지 않은 부분이 섞여 있을 경우 문제가 됨

__DATA__
@@ index.html.ep
% my $str = "";   # decode되어 있는 스트링

%# 이 두 가지는 출력이 동일하게 잘 나온다
%= link_to "link to $str" => "/$str"
%= link_to "link to $str" => '/%EA%B0%80'
%# <a href="/%EA%B0%80">link to 가</a>

%# 그러나 아래처럼 일부는 이스케이프되어 있고 일부는 아니라면
%= link_to "link to $str" => "/%EA%B0%80/$str"
%# 이미 이스케이프되어 있던 부분이 한번 더 가공되면서 엉뚱한 값이 된다.
%# <a href="/%C3%AA%C2%B0%C2%80/%EA%B0%80">link to 가</a>

그러니 인자로 들어가는 곳은 전부 이스케이프되어 있든지, 아니면 전부 안 되어 있든지 해야 함.

보통은 굳이 번거롭게 이스케이프하지 않아도 알아서 잘 처리해주는데, 예를 들어 경로에 # 이 들어간 경우가 있다(파일경로명에 #이 허용되니까). 그런데 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 기능을 쓰려면, Cpan:Mojolicious::Plugin::BasicAuth 모듈을 따로 설치한다.

저 모듈 문서의 시놉시스에는 '/' URL에 대해서만 적용이 되게 하고 있는데, 모든 경로에 다 저렇게 코드를 적어주는 건 말이 안 되니까, Cpan:Mojolicious::Lite#Under 에 나온 under 구문을 사용하자.

그리고 코드에 암호를 그대로 적는 게 좀 보기 안 좋으니, .htpasswd 에 저장되는 형태로 암호를 저장한 걸 가지고 비교를 하게 해보자.

    plugin 'basic_auth';

# htpasswd 로 아이디 "user" 암호 "pass"를 저장한 형태
    my $str = 'user:J4Cuzq8zvX6dA';
    my ($user, $pass) = split(':', $str);

    under sub {
        my $self = shift;

        return 1 if $self->basic_auth(
# 'realm'은 정해진 키가 아니라 그냥 브라우저에 출력되는 메시지이니까 맘대로 지어도 됨
                          realm => sub {
                              return 1 if ( $_[0] eq $user
                                            and
# 사용자가 입력한 암호를 crypt 를 써서 변환 후에 비교
                                            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>

Cpan:Mojo::Upload 객체로 들어오며, 다음과 같이 처리할 수 있다. 그 외 메소드는 모듈 문서 참고.

my $upload = $self->param('upload');    # Mojo::Upload 객체
$upload->move_to('filename');           # 저장

# 주의
my $hash = $self->req->params->to_hash;
# 이 해시에는 $hash->{upload} 키가 없어서 마치 파일이 전송되지 않은 것처럼 착각하게 됨

11. 로그

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

이름:  
Homepage:
내용:
 

기타분류

각주:
1. Perl/한글 참고
2. 유니코드논의/파일명인코딩에서 언급했던 상황이 여전히 생긴다

마지막 편집일: 2017-10-31 9:54 am (변경사항 [d])
1906 hits | Permalink | 변경내역 보기 [h] | 페이지 소스 보기