MathJax 서버측 렌더링

내 이 세상 도처에서 쉴 곳을 찾아보았으나, 마침내 찾아낸, 컴퓨터가 있는 구석방보다 나은 곳은 없더라.

MathJax 서버측 렌더링

블로그 글에 포함된 수식을 표현하기 위해 MathJax를 사용한다. MathJax 덕분에 수식을 멋지게 표현할 수 있지만 수식 렌더링 속도가 매우 느리다. 브라우저에서 처음 로드했을 때 페이지 안에 \rm\TeX 소스 코드가 그대로 있다. 이후 로딩된 JavaScript가 \rm\TeX 코드를 HTML/CSS, SVG 또는 MathML로 변환하고, 브라우저가 변환된 결과를 화면에 표시한다.

%math f(a) = \frac{1}{2\pi i}\oint_{\gamma} \frac{f(z)}{z-a} dz

KaTex는 MathJax보다 훨씬 빠르게 수식을 표시하고 서버측 렌더링도 지원한다. 그러나 Katex는 MathJax에 있는 모든 기능을 제공하지 못한다. 수식 엔진을 KaTex로 바꾼다면 페이지에 있는 수식이 제대로 표현되는지 하나하나 확인해야 할 것이다. 확인 작업이야 한 번 하면 끝날 일이지만 표현 못하는 수식이 하나라도 있다면 다시 MathJax로 롤백해야 할 것이다.

수식이 포함된 블로그 페이지의 느려터진 페이지 렌더링 속도를 볼 때마다 어떻게 개선할 수 없을까? 사이트를 생성할 때 수식도 미리 변환해 놓을 수 없을까? 불현듯 MathJax로도 서버쪽 렌더링을 할 수 있지 않을까 해서 검색해 보았다. 그동안 MathJax는 서버측 렌더링을 지원하지 않는 줄 알았는데, 잘못 알고 있었다. 왜 진작 찾아보지 않았을까?

내가 찾은 도구는 mathjax-node-page다. 다른 도구가 있는지는 모르겠다. mathjax-node-page로 다음과 같이 HTML 파일에 들어있는 수식을 렌더링하는 CLI를 제공한다. 속도는 빠르지 않다. 시험 삼아 HTML 파일 하나를 테스트 해봤는데 1초는 걸리는 것 같다.

$ mjpage --help
Usage: mjpage [options] < input.html > output.html
...

사용법이 이렇다면 HTML 파일을 하나씩 렌더링할 수밖에 없다. node.js 기반 사이트 생성기라면 내부적으로 mathjax-node-page를 직접 사용할 수 있겠지만, Hugo와 통합은 어려워 보인다. Hugo가 생성한 HTML을 mathjax-node-page CLI로 하나씩 읽어 수식을 렌더링할 셸 스크립트를 작성해야 한다.

#!/usr/bin/env bash

SRC_ROOT=public
TARGET_ROOT=rendered-public

FILES=$(find $SRC_ROOT)

for src in $FILES; do
  target=${src/$SRC_ROOT/$TARGET_ROOT}
  if [ -d $src ]; then
    echo "create $target"
    mkdir -p $target
  elif [[ $target =~ $TARGET_ROOT/(tags.*|archive.*|about.*|[0-9]{4}/index.html) ]]; then
    echo "copy $src"
    cp $src $target
  elif [[ ! $target =~ .*html$ ]]; then
    echo "copy $src ... non html resource"
    cp $src $target
  else
    echo "render $src"
    mjpage --format svg --dollars true < $src > $target
  fi
done;

mjpage는 소스 HTML 페이지 안에 수식이 있든 없든 페이지 하나를 처리하는 데 거의 1초가 걸린다. 이 블로그에는 현재 330개의 페이지가 있으니 이 스크립트로 모든 페이지를 처리하는 데는 5분 넘게 걸릴 것이다. 대상 파일을 조금이라도 줄이려고 수식이 없는 페이지를 제외했지만 효과가 크지는 않다. 확인해보니 제외되는 페이지가 별로 많지 않다.

수식 렌더링 속도가 이렇게 느리다면 이미 처리한 파일을 반복 처리하지 않도록 해서 속도 향상을 꾀할 수 있다. 소스 파일의 체크섬을 구해 저장해놓고 나중에 체크섬을 비교해 파일이 변했는지 확인한 다음, 변한 파일만 처리하면 시간을 많이 단축할 수 있을 것이다. 다만 이걸 bash로 할 수 있을 만큼 bash를 잘 아는 건 아니라는 게 문제다.

어떻게 할 수 있을까?. 차라리 node.js로 작성하는 게 낫지 않을까 생각해 잠깐 시도해 보았지만, node.js도 모르긴 마찬가지. 조금 찾아보니 파일의 체크섬은 shasum 같은 명령을 사용하면 되지만 Mac OS X에는 해당 명령이 없었다. 대신 md5가 있는데 이걸로 충분할 듯 하다. 파일의 체크섬은 그냥 파일이 있는 디레터리 안에 확장자 .md5로 저장하기로 했다.

위 스크립트에서 마지막 else 블록을 다음과 같이 수정한다. 처음 실행하면 모든 페이지를 처리해야 하므로 시간이 오래 걸린다. 그러나 그 다음부터는 새로 추가된 페이지나 변경된 페이지만 처리하므로 비교적 빠르게 동작한다.

  ...
  else
    checksum_new=$(md5 -q $src)
    checksum_old=$(cat $src.md5 2> /dev/null)
    if [ "$checksum_new" = "$checksum_old" ]; then
      echo "already rendered..."
    else
      echo "render $src"
      echo "$checksum_new" > $src.md5
      mjpage --format svg --dollars true < $src > $target
    fi
  fi
done;

이제 블로그를 생성하는 절차가 조금 바뀐다. 예전에는 hugo 명령을 실행하면 public 디렉터리에 블로그가 생성되고 그걸 GitHub에 푸시하면 됐다. 지금부터는 위에서 작성한 스크립트를 실행해 public 디렉터리를 읽어 수식을 렌더링하고 결과를 rendered-public 디렉터리에 저장한 후, rendered-public의 파일을 GitHub에 푸시해야 한다.

아, md5 파일을 rendered-public 디렉터리로 복사하는 작업은 불필요하므로 다음 elif 블록을 추가해 md5 파일을 무시하도록 하는 게 좋겠다.

  ...
  elif [[ $target =~ .md5$ ]]; then
    :                       # skip checksum file
  ...

블로그 생성 절차가 조금 번거로워 졌지만, 블로그에서 수식이 빠르게 표시되는 걸 보니 이 정도 불편은 충분히 감수할 수 있겠다.