Stay Hungry. Stay Foolish. Don't Be Satisfied.

ngx_pagespeed : Nginx의 PageSpeed 모듈

Google에 만든 PageSpeed는 웹사이트의 성능을 분석하고 최적화 방법을 알려주는 분석도구이다. 예전에는 Yahoo!의 YSlow가 대표적이었지만 이제는 PageSpeed가 더 대중적이 되었다. PageSpeed Insights를 이용하면 원하는 웹사이트를 PageSpeed로 분석해 볼 수 있고 크롬 익스텐션으로 분석해 볼 수 있다. PageSpeed Insights Rules에 따라서 분석해주고 점수로 보여주는데 이 규칙만 잘 이해해도 웹사이트를 어떻게 만들어야 성능이 좋은지 이해할 수 있다.

PageSpeed 로고

PageSpeed 자체는 계속 사용하고 있었지만 얼마 전에 블로그를 최적화하면서 PageSpeed Module을 사용해봤다. 모듈은 Apache(mod_pagespeed)와 Nginx(ngx_pagespeed)용이 있는데 웹서버 차원에서 PageSpeed가 권장하는 최적화를 한다. 나는 웹서버를 Nginx를 주로 사용하므로 ngx_pagespeed만 사용해 봤다.

ngx_pagespeed 설치

Nginx에 새로운 모듈을 추가할 때는 항상 그렇듯이 ngx_pagespeed를 사용하려면 nginx를 ngx_pagespeed와 함께 다시 빌드해야 한다. 설치문서대로 ngx_pagespeed 소스와 psol(PageSpeed Optimization Libraries)을 받고 Nginx를 빌드할 때 --add-module로 함께 빌드하면 된다.

ngx_pagespeed 설정

나도 사용한 지 얼마 안 되었고 모든 기능을 다 테스트해 본 것은 아니므로 사용해본 기능 위주로만 설명한다.(문서를 보면 정말 많은 기능이 있다.) ngx_pagespeed로 Nginx를 빌드했다고 하더라도 사용하려면 nginx.confserver부분에서 다음과 같이 PageSpeed를 켜주어야 한다.(다 테스트해보지는 못했는데 일부 설정은 각 server부분이 아니라 다른 부분에 설정해야 하는 설정도 있는 것으로 보인다.)

server {
    # ...
    pagespeed on;

    pagespeed FileCachePath "/path/to/cache";
    # ...
}

pagespeed on;로 활성화를 해야 하고 FileCachePath는 필수값이라서 설정하지 않으면 오류가 난다. 속도를 위해서 PageSpeed가 캐시파일을 생성하는데 이를 저장할 위치를 지정해야 한다. Nginx를 시작하면 [1024/162206:INFO:google_message_handler.cc(35)] No threading detected. Own threads: 1 Rewrite, 1 Expensive Rewrite.같은 오류메시지가 나오는데 실제 오류는 아니므로 신경안 써도 된다.

<html>
<head>
    <title>ngx-pagespeed test</title>
    <link rel="stylesheet" href="bootstrap.css" media="screen"/>
    <link rel="stylesheet" href="common.css" media="screen"/>
    <link rel="stylesheet" href="style.css" media="screen"/>
    <link rel="stylesheet" href="print.css" media="screen"/>
    <link rel="stylesheet" href="page.css" media="screen"/>
    <script type="text/javascript" src="init.js"></script>
    <script type="text/javascript" src="inline.js"></script>
</head>
<body>
    <h1>Hello PageSpeed</h1>
    <img src="image1.jpg">
    <script type="text/javascript" src="jquery-2.1.1.js"></script>
    <script type="text/javascript" src="bootstrap.js"></script>
    <script type="text/javascript" src="common.js"></script>
    <script type="text/javascript" src="plugin.js"></script>
</body>
</html>

다음과 같은 HTML이 있다고 해보자. 간단한 HTML이지만 CSS 파일도 많고 JavaScript 파일도 많은 데다가 헤더와 바디에 나누어져 있어서 최적화가 안 되어 있다. 여기에 PageSpeed를 적용하면 다음과 최적화를 해준다.

<html>
<head>
  <title>ngx-pagespeed test</title>
  <link rel="stylesheet" href="A.bootstrap.css.pagespeed.cf.uQnrHqVB52.css" media="screen"/>
  <link rel="stylesheet" href="A.common.css+style.css+print.css+page.css,Mcc.-X_l7tLwo2.css.pagespeed.cf.U60M2ysh0E.css" media="screen"/>
  <script src="init.js+inline.js.pagespeed.jc.-l4ywmKJ4l.js"></script>
  <script>eval(mod_pagespeed_PTslYvo__n);</script>
  <script>eval(mod_pagespeed_WewiyizJBt);</script>
</head>
<body>
  <h1>Hello PageSpeed</h1>
  <img src="ximage1.jpg.pagespeed.ic.U44YUjkGKM.webp">
  <script type="text/javascript" src="jquery-2.1.1.js.pagespeed.jm.V5dmkPfnRj.js"></script>
  <script src="bootstrap.js+common.js+plugin.js.pagespeed.jc.epuwIIsfqE.js"></script>
  <script>eval(mod_pagespeed_uKtTZWV22S);</script>
  <script>eval(mod_pagespeed_u$r4PZINvp);</script>
  <script>eval(mod_pagespeed_2gPYGymd2v);</script>
</body>
</html>

프론트앤드 최적화를 하는 방법은 여러 가지가 있지만 ngx_pagespeed를 적용하는 것만으로도 서버에서 HTML을 전혀 건드리지 않고 최적화를 수행해 준다. 파일명은 복잡해 보이지만 위에서 보듯이 여러 개의 CSS와 JS 파일을 하나의 파일로 합쳐주고 압축까지 수행해준다. eval()로 된 부분은 파일을 합쳤으므로 이를 순서대로 실행하는 부분으로 보인다. 심지어 JPEG였던 이미지를 WebP로 바꾸어준다. 기본 설정만으로도 이러한 최적화를 수행해 주지만 문서에 따르면 PageSpeed용 파일의 라우팅을 제대로 하도록 다음과 같은 핸들러를 추가하도록 가이드 하고 있다.

server {
    # ...
    pagespeed on;

    pagespeed FileCachePath "/path/to/cache";

    location ~ "\.pagespeed\.([a-z]\.)?[a-z]{2}\.[^.]{10}\.[^.]+" {
      add_header "" "";
    }
    location ~ "^/pagespeed_static/" { }
    location ~ "^/ngx_pagespeed_beacon$" { }
}


캐시 설정

위에서 캐시파일을 저장할 위치를 지정했지만, 서버성능에 맞게 캐시의 상세 설정을 추가할 수 있다.

server {
    # ...
    pagespeed on;

    pagespeed FileCachePath "/path/to/cache";
    pagespeed FileCacheSizeKb            102400;
    pagespeed FileCacheCleanIntervalMs   3600000;
    pagespeed FileCacheInodeLimit        500000;
    pagespeed LRUCacheKbPerProcess     8192;
    pagespeed LRUCacheByteLimit        16384;

    # ...
}

캐시 설정에 대한 옵션은 문서에 잘 나와 있다.

CSS나 JavaScript 파일을 수정해서 캐시를 갱신하려면 /path/to/cache 폴더 아래 cache.flush파일을 생성하면 된다. touch /path/to/cache/cache.flush같은 식으로 파일만 만들면 기존 캐시파일을 지우고 새로운 파일을 만들어 준다.

필터

기본으로 최적화를 해주는 부분이 있지만 필터를 사용하면 상황에 맞게 세부 조정을 하면서 최적화를 할 수 있다. 필터 목록을 보면 아주 많은 필터를 제공하고 있음을 알 수 있다. 필터를 통해서 LocalStorage에 리소스를 저장하게 한다거나 CSS는 스크립트보다 상위에 위치하게 한다든가 헤더가 여러 개인 경우 헤더를 합치는등의 작업을 할 수 있다. 필터는 nginx.confpagespeed EnableFilters FILTER_NAME;같은 형식으로 추가하면 된다. 몇 가지 필터의 동작만 확인해 보자.

자바스크립트 지연 실행

자바스크립트가 헤더나 본문 등에 섞여 있는 경우 자바스크립트가 렌더링을 막기 때문에 렌더링 후에 자바스크립트를 실행하기도 한다. 자바스크립트 지연 실행을 위해 pagespeed EnableFilters defer_javascript;를 추가하면 HTML이 다음과 같이 바뀐다.

<html>
<head>
    <title>ngx-pagespeed test</title>
    <link rel="stylesheet" href="A.bootstrap.css.pagespeed.cf.uQnrHqVB52.css" media="screen"/>
    <link rel="stylesheet" href="A.common.css+style.css+print.css+page.css,Mcc.-X_l7tLwo2.css.pagespeed.cf.U60M2ysh0E.css" media="screen"/>

    <script src="init.js+inline.js.pagespeed.jc.-l4ywmKJ4l.js" type="text/psajs" orig_index="0"></script>
    <script type="text/psajs" orig_index="1">eval(mod_pagespeed_PTslYvo__n);</script>
    <script type="text/psajs" orig_index="2">eval(mod_pagespeed_WewiyizJBt);</script>
</head>
<body>
    <noscript>
        <meta HTTP-EQUIV="refresh" content="0;url='http://test.sideeffect.kr:2000/?PageSpeed=noscript'" />
        <style><!--table,div,span,font,p{display:none} --></style>
        <div style="display:block">Please click <a href="http://test.sideeffect.kr:2000/?PageSpeed=noscript">here</a> if you are not redirected within a few seconds.</div>
    </noscript>
    <h1>Hello PageSpeed</h1>
    <img src="ximage1.jpg.pagespeed.ic.U44YUjkGKM.webp">

    <script pagespeed_orig_type="text/javascript" src="jquery-2.1.1.js.pagespeed.jm.V5dmkPfnRj.js" type="text/psajs" orig_index="3"></script>
    <script src="bootstrap.js+common.js+plugin.js.pagespeed.jc.epuwIIsfqE.js" type="text/psajs" orig_index="4"></script>
    <script type="text/psajs" orig_index="5">eval(mod_pagespeed_uKtTZWV22S);</script>
    <script type="text/psajs" orig_index="6">eval(mod_pagespeed_u$r4PZINvp);</script>
    <script type="text/psajs" orig_index="7">eval(mod_pagespeed_2gPYGymd2v);</script>
    <script type="text/javascript" src="/pagespeed_static/js_defer.pbrP1whUgE.js"></script></body>
</html>

위 코드를 보면 스크립트 부분이 type="text/javascript"대신 type="text/psajs"이 붙어있고 orig_index=""로 순서대로 번호가 붙어 있는 것을 볼 수 있다. 이를 마지막의 js_defer.js에서 실행해서 지연 실행을 할 수 있도록 해준다.

DNS 프리패치

웹페이지에서 다양한 도메인을 처리하는 경우 DNS를 처리하는 시간이 걸리기 때문에 DNS를 미리 처리해주면 속도를 높일 수 있다. 이를 위해 pagespeed EnableFilters insert_dns_prefetch;로 필터를 활성화해 주면 본문에 현재 도메인과 다른 <img src="https://developers.google.com/_static/f12482462b/images/developers-logo.png">같은 리소스가 있는 경우 이를 헤더에 <link rel="dns-prefetch" href="//developers.google.com">같은 식으로 dns-prefetch를 추가해준다.

PageSpeed 어드민 페이지

PageSpeed는 1.8.31.2버전부터 어드민 기능을 제공하고 있다.(현재 버전은 1.9.32.1) 어드민 페이지에서는 PageSpeed의 설정상황이나 통계 등을 파악할 수 있다. 화면이 좀 복잡하긴 하지만 초기에 사용할 때 PageSpeed의 상태를 보면서 최적화 할 때 도움이 된다.

pagespeed GlobalStatisticsPath /ngx_pagespeed_global_statistics;
pagespeed GlobalAdminPath /pagespeed_global_admin;

server {
    # ...
    location /ngx_pagespeed_statistics { allow 127.0.0.1; deny all; }
    location /ngx_pagespeed_global_statistics { allow 127.0.0.1; deny all; }
    location /ngx_pagespeed_message { allow 127.0.0.1; deny all; }
    location /pagespeed_console { allow 127.0.0.1; deny all; }
    location ~ ^/pagespeed_admin { allow 127.0.0.1; deny all; }
    location ~ ^/pagespeed_global_admin { allow 127.0.0.1; deny all; }

    pagespeed StatisticsPath /ngx_pagespeed_statistics;
    pagespeed MessagesPath /ngx_pagespeed_message;
    pagespeed ConsolePath /pagespeed_console;
    pagespeed AdminPath /pagespeed_admin;
}

디렉티브는 StatisticsPath, MessagesPath, ConsolePath, AdminPath, GlobalStatisticsPath, GlobalAdminPath가 있는데 필요한 기능별로 설정해서 사용하면 된다. 여기서 GlobalStatisticsPath, GlobalAdminPath는 전역설정이므로 server부분이 아니라 전역으로 설정해야 한다. 먼저 각 페이지를 받을 URL에 따라 location 설정을 추가하고 아무나 접속하면 안 되니까 자신의 IP만 접속할 수 있도록 설정한다. 이후에 각각 디렉티브를 설정하면서 location에 설정한 경로와 일치하게 설정하면 된다.

이렇게 설정하고 위 설정한 /pagespeed_admin로 접속하면 아래와 같은 화면을 볼 수 있다.

Statistics

Statistics 화면

Configuration

Configuration 화면

Histograms

Histograms 화면

Caches

Caches 화면

Graphs

Graphs 화면

2014/10/25 03:19 2014/10/25 03:19

angular-summernote v0.3.0 릴리즈

지난 릴리즈 후에 당분간 건드리지 않으려고 했는데 AngularJS 1.3.0이 나오면서 오류가 생기는 부분이 있어서 패치를 했다.(1.3.0이 이렇게 금방 나올 줄이야...)

일단 오류를 수정하기 전에 Karma 테스트를 정리해야 할 필요가 있었다. 테스트를 꽤 충실이 작성했는데 AngularJS의 새 버전이 나온 관계로 angular-summernote를 1.2.x와 1.3.x을 모두 테스트해야 했다. 새 버전을 쓰는 사람도 있지만, 기존의 1.2.x를 그대로 사용하는 사람도 당연히 있을 테니까... 어떻게 할까 고민하다가 Angular.js의 Karma 설정을 참고해서 공통부분과 의존성 부분을 따로 분리해서 버전별로 의존성을 관리해서 테스트가 동작하도록 만들었다.

일단 하나로 되어 있던 karma.conf.js파일에서 공통 부분은 karma-shared.conf.js로 분리하고 karma.conf.js에서 karma-shared.conf.js를 불러온 뒤 의존성 파일만 추가로 지정하는 방식을 취했다. 같은 방식으로 karma-angular-1-2.x.conf.js를 만들어서 AngularJS 1.2.x에 대한 의존성을 갖게 만들자 Grunt 명령어만으로 두 가지 버전을 테스트할 수 있었다. 이제 1.3에 맞춰서 개발할 때마다 틈틈이 1.2.x도 한 번씩 확인해 보면 될 것 같다.(귀찮아서 아직 Travis CI에는 연결하지 않았다.)

이번에 발생한 오류error:isecdom Referencing a DOM node in Expression에 관련된 오류인데 보안 문제로 Angular 표현식에서 DOM을 반환하지 못하도록 변경된 부분 때문에 발생했다. summernote의 콜백과 angualr-summernote의 콜백을 연결해 주는 부분이 있는데 이때 summernote에 콜백을 실행하면서 DOM 객체는 인자로 전달하는 부분이 문제가 되었다.
원인은 파악했지만 어떻게 수정해야 할지가 좀 어려웠다. summernote 콜백에서 DOM을 사용해야 하니 넘겨주기는 해야 하는데 어떤 식으로 전달해 주는 것이 좋은지 몰라서 여러 가지를 시도하다가 사용자가 바인딩할 수 있도록 추가하고 이 바인딩 객체에 넘겨주는 방식을 취했다. 전에는 DOM 객체($editable)가 필요한 콜백을 사용하는 경우 인자로 받았지만 0.3.0부터는 editable="bindingObject"같은 식으로 지정해서 이 객체로 전달받게 된다. 구현하고 나서도 좋은 해결책인지 좀 고민되지만 일단 동작은 정상적으로 하므로 사용자가 피드백 주면 그때 다시 고민해 보기로.. ㅎ

수정내용 자체는 많지 않지만 AngularJS 1.3.0을 지원하는 버전이므로 v0.3.0으로 간만에 마이너 버전을 올려서 릴리즈했다.

2014/10/20 23:50 2014/10/20 23:50