AMP(Accelerated Mobile Pages) 기여

2018-11-28

들어가며

이 문서는 2부로 구성되어 있다. 1부는 2018학년도 2학기에 동아리에서 스터디한 내용을 정리하는 것이고, 2부는 그 도중 내가 AMP 프로젝트에 한 기여를 소개한다.

1부. AMP(Accelerated Mobile Pages) 사용

들어가며

웹 문서를 작성할 일은 많다. 그런데 특별한 기능이 있는 문서를 원하는 것이 아닌, 블로그와 같이 일반적인 '글'을 작성하고 싶을 때의 경우를 생각해 보자. 크게 다음과 같은 방법들이 있을 것이다.

포털 서비스의 경우 글 백업과 이전 등이 상당히 어려울 수 있으므로 쳐다보지조차 않았다. 설치형 CMS를 쓴다고 했을 때, 언젠가 HTML 기술이 필요해진다면 소스 편집 기능을 활용할 수도 있을 것이다. 그런데 양식이 바뀌는 등의 큰 변화가 있으면 처리하기 어려운 부분이 존재할 수 있다.

Jekyll을 사용하면 Markdown 등의 문법을 활용하여 정적 페이지를 만들 수 있다. GitHub Pages는 gh-pages branch 등을 통해 사용할 수 있다. 그런데 마냥 쓰자니 테마 등 여러 가지로 고민할 것들이 또 있다.

이에 바닥부터 블로그 등의 글을 위한 프레임워크라고 부르기도 뭐한 간단한 것을 만들어 보기로 했고 PHP의 Parsedown을 이용하여 만들었다. 물론 그 과정에서 Parsedown에 기여도 하게 되었다.

한편 작성된 문서에 접근하기 쉽게 만드는 것도 필요하다. 모바일에서 구글 검색을 할 때 ⚡ 표시가 붙은 항목을 본 적이 있을 것이다. 그리고 그것은 클릭하면 다른 항목보다 훨씬 빨리 로드된다. 이 기능을 지원하는 것이 목적이다.

그림 1 AMP를 도입한 효과

Markdown

그림 2 Markdown in Atom
그림 3 Markdown in Mou

Markdown은 간단한 몇 가지 문법을 지원하는 문서 형식이다. 공식적인 표준은 없지만, 흔히 Simple Markdown, GFM(GitHub Flavored Markdown), Extra Markdown 등으로 분류한다. 편집기에 따라서 아래와 같이 수식 등을 추가로 지원하기도 한다.

그림 4 Markdown in Typora

이제 Markdown을 HTML로 변환하는 방법을 소개한다.

Showdown.js

JavaScript단에서 동작하는 라이브러리로 클라이언트와 node.js 서버 모두에서 사용할 수 있다. 처음에는 이것을 사용하여 XHR로 Markdown 파일을 받아 변환하는 방식을 썼는데, 느리고 검색엔진에도 잡히지 않는 등 큰 문제점들이 있었다.

그림 5 Showdown.js

Strapdown.js

<xmp> 태그 안의 내용을 자동으로 변환해주는 라이브러리이다. 위와 달리 검색엔진에 잡히겠지만, 정적 페이지만 사용하려면 관련 내용을 매번 적어줘야 하므로 적용이 곤란했고, 동적 페이지 생성으로 넘어가면 어차피 더 좋은 것들이 있어서 안 쓰게 되었다.

그림 6 Strapdown.js

Parsedown

동적 페이지 생성을 하기로 생각한 후 찾은 라이브러리이다. PHP 라이브러리로, 자체 Markdown 관련 기능보다 더 빠르고 오픈 소스라서 커스터마이징이 가능하다. 필자도 표의 셀 병합을 지원하는 버전을 만들고 배포한 바 있다.#

그림 7 Parsedown

AMP(Accelerated Mobile Pages)

AMP는 구글이 정해 놓은 웹페이지를 빠르게 로드하기 위한 표준 정도라고 보면 되겠다. 추가 리소스 다운로드 없이도 레이아웃을 미리 결정할 수 있고, 구글이 콘텐츠를 캐싱해준다는 점 등이 있다. 이 글은 AMP를 설명하는 것보다 이를 어떻게 적용할지에 대해 다루기에 자세한 설명은 생략한다.

기능

특정 규칙을 사용해 자동으로 연결하고 싶은 경우 아래와 같이 현재 요청 중인 페이지의 URL을 알아낼 수 있다.

$url=((@$_SERVER["HTTPS"]==="on")?"https":"http")."://".$_SERVER["HTTP_HOST"].$_SERVER["REQUEST_URI"];

연결에 성공하면 다음과 같이 확인할 수 있다.

그림 8 AMP 페이지로의 링크
그림 9 원본 페이지로의 링크

<img>, <video>, <audio> 등을 사용할 수 없다.

대신 <amp-img> 등을 사용할 수 있고 이는 필요할 때 로드된다. 이런 요소들은 문서의 레이아웃을 미리 결정하기 위해 width와 height가 반드시 주어져 있어야 한다. 창 크기에 따른 레이아웃 모드는 다음과 같다. 이미지를 위한 가장 무난한 모드는 intrinsic으로 보인다.

그림 10 레이아웃 모드

구현은 PHP Simple HTML DOM Parser를 사용해 다음과 같이 할 수 있다.

foreach($dom->find("img") as $img)
{
    $img->tag="amp-img";
    $size=getimagesize(realpath(dirname($_GET["md"]).DIRECTORY_SEPARATOR.$img->src));
    if(!$img->hasAttribute("width"))$img->setAttribute("width",$size[0]);
    if(!$img->hasAttribute("height"))$img->setAttribute("height",$size[1]);
    $img->setAttribute("layout","intrinsic");
}

유효성 검사

제대로 된 AMP 문서가 맞는지 확인하기 위해서는 유효성 검사가 필요하다. 여러 방법으로 할 수 있다.

그림 11 AMP 라이브러리 내장 유효성 검사 도구
그림 12 AMP 유효성 검사 도구 웹 인터페이스
그림 13 AMP 유효성 검사 도구 브라우저 확장 프로그램

유효성 검사를 통과한 모습이다.

그림 14 외부 도구 AMP 유효성 검사 통과
그림 15 내장 AMP 유효성 검사 통과

CSS

CSS는 외부 링크는 안 되며 하나의 <style amp-custom> 안에 모든 내용이 포함되어 있어야 한다. 대부분 속성이 지원되지만 다른 곳에서 가져온 경우 !important 키워드는 사용할 수 없으므로 이를 제거해 주어야 한다.

외부 링크의 내용을 직접 포함해서 주기 위해 PHP에서는 다음 두 가지의 방법을 사용할 수 있다. 먼저 첫 번째는 PHP 설정의 allow_url_fopen이 true인 경우 사용할 수 있다. Remote fread()를 사용한다.

$fp=fopen("https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/2.10.0/github-markdown.min.css","r");
$css=str_replace("!important","",stream_get_contents($fp));
fclose($fp);

해당 설정을 조정할 수 없는 경우에는 cURL 확장이 사용 가능하다면 이를 활용할 수도 있다.

$curl=curl_init();
curl_setopt($curl,CURLOPT_URL,"cdnjs.cloudflare.com/ajax/libs/github-markdown-css/2.10.0/github-markdown.min.css");
curl_setopt($curl,CURLOPT_RETURNTRANSFER,1);
$css=str_replace("!important","",curl_exec($curl));
curl_close($curl);

이렇게 $css에 외부 CSS 내용을 받아온 후 아래와 같이 전송해주면 된다.

<style amp-custom><?php echo($css); ?></style>

2부. AMP 프로젝트 기여

들어가며

그동안 블로깅을 하면서 추가한 기능들을 AMP로 옮기려고 보니 가장 문제가 되는 것이 외부 스크립트를 쓰는 것들이었다. 크게 두 가지로 구문 강조와 수식이 있는데, 전자는 찾아보니 그냥 GitHub Gist를 쓰라는 이야기가 많아서 일단은 그냥 두기로 했다. 이제 수식을 쓰기 위한 내용이다.

일반 웹페이지에서는 다음과 같이 MathJax를 사용하면 된다.

그림 16 MathJax 데모

그러나 AMP의 정책상 내부에서 Javascript를 사용할 수 없기에, Extension을 하나 만들고 본 프로젝트에 인정을 받으면 된다. 이에 이미 만들어져 있던 것이 <amp-mathml> 이었고 형식은 다음과 같다.

그림 17 amp-mathml 문서

오류 사항

이제 다음 내용을 렌더링해 보았더니 아래와 같이 되었다.

  <amp-mathml
    layout="container"
    data-formula="\[x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\]">
  </amp-mathml>
  <p>MathJax <amp-mathml layout="container" inline data-formula=""></amp-mathml> MathML <amp-mathml layout="container" inline data-formula=""></amp-mathml> AMP</p>
그림 18 amp-mathml 렌더링

Inline 수식이 뭔가 이상하다. 일반 MathJax로 다음 문단을 렌더링하면 이렇게 된다.

  <p>\[x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\]</p>
  <p>MathJax  MathML  AMP</p>
그림 19 MathJax 렌더링

원인을 찾아보니 태그의 inline 속성이 MathJax에서 inline 형태로 만들어 주는 것에 대해 대응이 되지 않고 있었다. 단지 display형 수식을 넣을 것만 생각하고 만든 것으로 보였다. 그렇다면? 고쳐야지.

고치기

아무래도 대단위 프로젝트이다 보니 코드만 고치면 되는 것이 아니고 그에 해당하는 문서와 테스트도 같이 맞춰주어야 한다. 그러다 보니 커밋도 여러 개로 나뉘었다.

그림 20 커밋 로그
git diff 5dfb0e08b56e77d627195789de4b59d0d656f3d1 373a2608d5f9e35bb8f310768f8d1762b764d467

위 명령으로 직접 비교해보면 바꾼 내용이 많지는 않다.

diff --git a/3p/mathml.js b/3p/mathml.js
index a4b76e072..bd7ed79fe 100644
--- a/3p/mathml.js
+++ b/3p/mathml.js
@@ -46,13 +46,13 @@ export function mathml(global, data) {

   getMathmlJs(
       global,
-      'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML',
+      'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML',
       mathjax => {
         // Dimensions are given by the parent frame.
         delete data.width;
         delete data.height;
         const div = document.createElement('div');
-        div.setAttribute('id','mathmlformula');
+        div.setAttribute('id', 'mathmlformula');
         div.textContent = data.formula;
         setStyle(div, 'visibility', 'hidden');
         global.document.body.appendChild(div);
@@ -62,15 +62,20 @@ export function mathml(global, data) {
         mathjax.Hub.Queue(function() {
           const rendered = document.getElementById('MathJax-Element-1-Frame');
           // Remove built in mathjax margins.
-          const display = document.getElementsByClassName('MJXc-display');
-          if (display[0]) {
-            display[0].setAttribute('style','margin-top:0;margin-bottom:0');
-            context.requestResize(
-                rendered./*OK*/offsetWidth,
-                rendered./*OK*/offsetHeight
-            );
-            setStyle(div, 'visibility', 'visible');
+          let display = document.getElementsByClassName('MJXc-display');
+          if (!display[0]) {
+            const span = document.createElement('span');
+            span.setAttribute('class', 'mjx-chtml MJXc-display');
+            span.appendChild(rendered);
+            div.appendChild(span);
+            display = document.getElementsByClassName('MJXc-display');
           }
+          display[0].setAttribute('style','margin-top:0;margin-bottom:0');
+          context.requestResize(
+              rendered./*OK*/offsetWidth,
+              rendered./*OK*/offsetHeight
+          );
+          setStyle(div, 'visibility', 'visible');
         });
       }
   );
diff --git a/examples/amp-mathml.amp.html b/examples/amp-mathml.amp.html
index 26a0b6e97..35b2e466f 100644
--- a/examples/amp-mathml.amp.html
+++ b/examples/amp-mathml.amp.html
@@ -23,9 +23,9 @@
   <h2>Double angle formula for Cosines</h2>
   <amp-mathml
     layout="container"
-    data-formula="\[ \cos(θ+φ)=\cos(θ)\cos(φ)−\sin(θ)\sin(φ) \]">
+    data-formula="$$ \cos(θ+φ)=\cos(θ)\cos(φ)−\sin(θ)\sin(φ) $$">
   </amp-mathml>
-  <h2>Inline formula.</h2>
-  This is an example of a formula placed  inline in the middle of a block of text. <amp-mathml layout="container" inline data-formula="\[ \cos(θ+φ) \]"></amp-mathml> This shows how the formula will fit inside a block of text and can be styled with CSS.
+  <h2>Inline formula</h2>
+  <p>This is an example of a formula of <amp-mathml layout="container" inline data-formula="`x`"></amp-mathml>, <amp-mathml layout="container" inline data-formula=""></amp-mathml> placed inline in the middle of a block of text. <amp-mathml layout="container" inline data-formula=""></amp-mathml> This shows how the formula will fit inside a block of text and can be styled with CSS.</p>
 </body>
 </html>
diff --git a/extensions/amp-mathml/0.1/amp-mathml.css b/extensions/amp-mathml/0.1/amp-mathml.css
index 4003be606..394a233c6 100644
--- a/extensions/amp-mathml/0.1/amp-mathml.css
+++ b/extensions/amp-mathml/0.1/amp-mathml.css
@@ -16,4 +16,5 @@

 amp-mathml[inline] {
   display: inline-block;
+  vertical-align: middle;
 }
diff --git a/extensions/amp-mathml/0.1/test/validator-amp-mathml.html b/extensions/amp-mathml/0.1/test/validator-amp-mathml.html
index c1cd42dd7..d373640e9 100644
--- a/extensions/amp-mathml/0.1/test/validator-amp-mathml.html
+++ b/extensions/amp-mathml/0.1/test/validator-amp-mathml.html
@@ -30,7 +30,7 @@
 <body>
   <!-- Valid -->
   <amp-mathml layout="container" data-formula="\[x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\]"></amp-mathml>
-  <amp-mathml layout="container" inline data-formula="\[x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\]"></amp-mathml>
+  <amp-mathml layout="container" inline data-formula=""></amp-mathml>
   <!-- Invalid: unsupported layout value -->
   <amp-mathml layout="responsive" width="10px" height="10px" data-formula="\[x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\]"></amp-mathml>
   <!-- Invalid: missing formula -->
diff --git a/extensions/amp-mathml/0.1/test/validator-amp-mathml.out b/extensions/amp-mathml/0.1/test/validator-amp-mathml.out
index d7accb56d..c27da4f37 100644
--- a/extensions/amp-mathml/0.1/test/validator-amp-mathml.out
+++ b/extensions/amp-mathml/0.1/test/validator-amp-mathml.out
@@ -31,7 +31,7 @@ FAIL
 |  <body>
 |    <!-- Valid -->
 |    <amp-mathml layout="container" data-formula="\[x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\]"></amp-mathml>
-|    <amp-mathml layout="container" inline data-formula="\[x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\]"></amp-mathml>
+|    <amp-mathml layout="container" inline data-formula=""></amp-mathml>
 |    <!-- Invalid: unsupported layout value -->
 |    <amp-mathml layout="responsive" width="10px" height="10px" data-formula="\[x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\]"></amp-mathml>
 >>   ^~~~~~~~~
diff --git a/extensions/amp-mathml/amp-mathml.md b/extensions/amp-mathml/amp-mathml.md
index 36012b992..3c8a73706 100644
--- a/extensions/amp-mathml/amp-mathml.md
+++ b/extensions/amp-mathml/amp-mathml.md
@@ -55,12 +55,12 @@ This extension creates an iframe and renders a MathML formula.
 #### Example: Double angle formula for Cosines

```html
-<amp-mathml layout="container" data-formula="\[ \cos(θ+φ)=\cos(θ)\cos(φ)−\sin(θ)\sin(φ) \]">
+<amp-mathml layout="container" data-formula="$$ \cos(θ+φ)=\cos(θ)\cos(φ)−\sin(θ)\sin(φ) $$">
 </amp-mathml>
```
 #### Example: Inline formula

-This is an example of a formula placed  inline in the middle of a block of text. `<amp-mathml layout="container" inline data-formula="\[ \cos(θ+φ) \]"></amp-mathml>` This shows how the formula will fit inside a block of text and can be styled with CSS.
+This is an example of a formula of ``<amp-mathml layout="container" inline data-formula="`x`"></amp-mathml>``, `<amp-mathml layout="container" inline data-formula=""></amp-mathml>` placed inline in the middle of a block of text. `<amp-mathml layout="container" inline data-formula=""></amp-mathml>` This shows how the formula will fit inside a block of text and can be styled with CSS.

 ## Attributes

(주: diff 내에 Code fence 문법이 들어가 있어서 처음으로 ~~~ code fence를 만들어 보았다.)

중요한 내용만 보자면 3p/mathml.js:L65-78에서 MJXc-display가 있을 때만 적용되는 것이 아니라 없으면 직접 만들고 적용하도록 했고, extensions/amp-mathml/0.1/amp-mathml.css:L19에서 vertical-align: middle; 스타일을 추가했다. 이게 해결 전부고 나머지는 라이브러리 버전 bump, 코딩 스타일 맞추기, 문서/테스트 업데이트 정도다.

Pull Request#

사실 위 작업에 앞서 Pull Request를 염두에 두고 Issue를 먼저 만들었다.#

그림 21 Issue

열심히 영어로 작문하며 커뮤니케이션을 한 결과 Merge에 성공했다. 처음에는 이상하게 테스트 통과를 못 해서 한참 더 기다려야 했다.

그림 22 Pull Request

나가며

Merge된 직후이니 가장 최근 커밋에 내가 있는 모습이다.

그림 23 amphtml 메인

완료하고 ampproject의 멤버도 함께 되었다. 하려면 2FA(Two-factor authentication)를 활성화시켜야 하는데 약간 귀찮다.

그림 24 ampproject 멤버

따로 설명은 안 했지만 빌드 환경 구축에도 사실 조금 시간이 걸렸는데, 기여한다고 생각하니 즐거운 시간이었다. 앞으로 구문 강조에 관해서도 기여해 보고 싶다.

돌아가기