JS프로그램의 압축(Compression of JavaScript Program)

원문은 http://j.mp/gl4jDq 에서 보실 수 있습니다. 오역의 가능성이 있으니 원문과 비교하며 보세요~

왜 자바스크립트를 압축해야 할까?

JS파일을 압축해야하는데에는 여러 이유가 있을 수 있을 수 있습니다. 네트워크 대역폭을 줄이고자 하는 것이 가장 명백한 이유가 될 수 있는데, 이는 HTTP 압축을 통해 달성할 수 있습니다. 그러나 HTTP 압축에 의존할 수 없거나 허용되지 않는 예외적인 경우가 있을 수 있습니다. 제 흥미를 끈 것은 JS파일 크기에 강제적인 제한을 둔 몇몇 데모 공모전이었습니다. 예를 들어, WebGL 64k Contest 에는 다음과 같은 규칙이 있습니다.

  • 제출되는 파일의 사이즈는 64k 이하일 것(65536바이트)
  • 외부요청이 없을 것, 모든 것은 JS내에 인라인으로 포함시킬 것.

두번째 포인트를 보면, 모든 이미지나 음악 등의 자원들이 JS 소스코드로 인라인되어야 하고, 첫번째 포인트에 따르면 소스코드는 65536바이트를 넘지 않아야 합니다. 꽤 도전적이네요! 재미있는 프로그램을 만들기 위해 여러분은 주어진 용량을 효율적으로 사용해야 합니다. 따라서 압축이 필요합니다.

The Google Closure Compiler

가장 중요한 것을 먼저 봅시다. Google에서 공개한 Google Closure Compiler라는 도구가 있습니다. 이 도구는 기본적으로 아래와 같은 방법을 통해 JS코드의 용량을 최소한으로 줄입니다.

  • 모든 주석과 불필요한 공백 제거
  • 변수명과 프로퍼티명을 더 짧게 변경
  • 표현문과 구조를 더 압축된 형태로 리팩토링

말할 것도 없이 JS소스코드를 압축하려는 모든 시도는 closure compiler를 사용하는 것으로부터 시작됩니다. 아울러 이 글에서는 이미 closure compiler로 압축되어있는 코드에서 더 나아가 바이너리 압축을 적용시키는 것에 초점을 맞출 것입니다.

압축방법 결정하기

우리가 당면한 주요문제 중 하나는 JS 프로그램이 스스로 압축해제가 가능해야 한다는 것입니다. 따라서 압축해제 루틴이 너무 많은 공간을 차지하면 압축의 이득을 잃게 됩니다. 또다른 문제는 바이너리로 압축된 데이터 스트림이 JS소스로 저장이 가능한 형태여야 한다는 것입니다.

기존의 라이브러리들

압축해제 루틴이 작아야 하므로, 기존의 (효율적이지만 크기가 큰) JXGraph, js-deflate와 같은 압축해제 라이브러리들은 실격입니다.

liblzg 사용하기

저의 첫번째 해결책은 liblzg 압축 라이브러리를 사용하는 것입니다. 이 라이브러리는 경량화된 압축해제 루틴을 가질 수 있도록 특별히 디자인되었습니다. 압축해제루틴은 불과 450바이트 정도에 불과하며(조금 손을 보면 더 작아질 수 도 있습니다) 우리의 필요에 잘 맞습니다.

바이너리 데이터 다루기

이제 다른 문제가 있습니다. 어떻게 바이너리 데이터를 js 소스 파일로 담을 수 있을까요?

Base64

바이너리 데이터를 텍스트 파일로 저장할 수 있는 검증된 방법은 물론 base64 를 통한 방법입니다. 그러나, 이 방법은 텍스트 한글자마다 6비트밖에 사용할 수 없기 때문에 33%의 용량(8비트 문자 인코딩인 경우)을 더 써야 합니다. 이런 용량증가는 압축률에 심각한 영향을 미칠 것 이므로 더 좋은 방법을 찾아봐야 합니다.

Latin 1

첫번째로 시도해봤던 방법은 Latin 1 인코딩을 사용하는 방법이었습니다. 즉, 압축된 바이트 데이터를 Latin 1으로 인코딩해 JS 문자열로 저장한 것입니다. 물론, 인코딩의 한계로 몇몇 바이트 값이 금지(더 정확하게 말하면, 0~31, 39, 92, 127~159번)되어 있기 때문에 조금 손보지 않으면 작동하지 않습니다. 사용할 수 없는 바이트코드를 2바이트 문자로 대체시키고, (liblzg로 압축된 데이터의 통계에 기반해) 자주 쓰이지 않는 바이트코드를 사용할 수 없는 코드와 교환하게 되면 용량을 증가시킬 요소는 5~10% 정도로 줄어들 수 있습니다. 자, 이것보다 더 줄일 방법은 없을까요?

UTF-16

할 수 있습니다! Latin 1 인코딩을 사용했을 때 0~255번 코드 사이의 26%는 사용할 수 없다는 것에 주목하면, UTF-16 인코딩의 사용할 수 없는 코드는 3%에 불과 합니다(0~65535번 코드 사이에 2151개의 코드). 그럼 UTF-16를 사용해 JS 프로그램을 줄여봅시다. 브라우저가 UTF-16을 이용하는 것을 인지할 수 있도록 파일 시작 부분에 2바이트의 BOM이 추가됩니다(사실 클라이언트 프로그램이나 텍스트에디터에서 Latin 1 인코딩을 UTF-8로 혼돈하는 경우가 있기 때문에 UTF-16이 Latin 1을 사용하는 것보다 더 안전합니다). 이제 압축된 데이터 스트림 2바이트를 1개의 UTF-16 글자로 줄일 수 있게 되었고, 다시 한번 사용할 수 없는 코드에 대한 처리를 하고 나면, (사용할 수 없는 코드를 사용할 수 있는 UTF-16 코드 2글자로 대체), 4%미만의 용량이 증가합니다 (liblzg 압축된 데이터의 경우에는 거의 1%미만 입니다).

UTF-16 인코딩의 단점

UTF-16인코딩을 사용하는 데에 심각한 문제가 하나 있습니다. 압축해제 루틴도 UTF-16으로 저장되어야 한다는 것입니다. 즉, 압축해제 로직이 2배의 용량을 차지하게 된다는 뜻입니다.(900바이트 이상). 작은 파일에서는 최종 파일의 크기가 Latin 1 인코딩을 사용하는 것보다 더 크게 될 겁니다. 하지만 걱정할 것은 없습니다. 해결책은 가까이 있으니까요. 압축해제 루틴도 줄일 수 있습니다. 어떻게 가능할까요? 바이너리 데이터에 했던 것과 같이 2개의 글자를 UTF-16 글자 한개로 만들어봅시다. 압축해제 루틴은 순수한 ASCII코드(36~126번 사이의 글자들)이라는 좋은 특징을 가지고 있습니다. 이것은 두개의 연속된 바이트를 한개의 유니코드로 합치면 “언제나” 유효한 글자가 된다는 뜻입니다(2020-7e7e 사이의 UTF-16코드는 전부 유효합니다). UTF-16 문자열로 표현된 압축해제 루틴이 사용할 수 없는 코드를 만들지 않기 때문에 공간을 전혀 낭비하지 않게 되고, 이 루틴을 다시 되돌리는 작업도 매우 간단합니다. 네, 되돌리기 위해 또다른 루틴이 필요하고, 이 루틴은 순수한 UTF-16 형식이어야 합니다. 하지만 새로 만들 루틴은 그렇게 많은 코드가 필요치 않고 아주 간단합니다. 이 루틴은 아래와 비슷할 겁니다(closure compiler를 통하지 않은 모습입니다).

liblzg를 통한 접근의 결론

지금까지 우리는 스스로 압축을 해제할 수 있는 JS 프로그램을 만들기 위해 최대한으로 liblzg 압축을 활용했습니다. 요약하면 :

  • 스스로 압축해제가 가능한 JS 모듈(약 600바이트)
  • (원래의 바이너리 크기보다 3%정도 더 큰) 아주 잘 압축된 바이너리 데이터
  • (크로스 브라우징이 아주 잘 되는) 순수 ECMAScript 구현
  • (DEFLATE, Bzip2, LZMA 등 보다는 떨어지지만) 적절한 압축률

좋은 결과에 도달하긴 했지만, 더 잘할 수도 있습니다.

더 잘 해보기 – PNG

JS로 브라우저 API에 접근해 DEFLATE로 인코딩 된 데이터를 풀어낼 수 있으면 얼마나 좋을까요? 대부분의 브라우저는 여러가지 작업을 위해 내부적으로 zlib을 사용하지만, zlib을 JS로 직접접근하는 방법은 아직 없습니다. 하지만 우리가 IE 6,7, 8과 같은 기존 브라우저에 대한 호환성을 희생시키면 엘리먼트를 사용한 다른 멋진 트릭을 사용할 수 있습니다. JS프로그램을 PNG 이미지 파일 내에 저장시키는 것입니다. 이 방법은 어떤 장점이 있고, 어떻게 사용할 수 있을까요?

PNG 아이디어

자, PNG 이미지 파일 포맷에 대해 약간의 배경지식을 가져볼까요.

  • PNG는 압축하는데에 DEFLATE알고리즘을 사용합니다(이것은 이를테면 ZIP과 gzip에 사용되는 것과 비슷합니다).
  • 무손실 파일 포맷입니다(어떤 픽셀도 왜곡시키지 않습니다).
  • 모든 최근의 브라우저들은 Image 객체로 로딩 될 때 PNG 이미지를 해석할 수 있습니다.
  • PNG 이미지를 canvas 엘리먼트로 그릴 수 있고, getImageData() 메서드를 통해 픽셀들을 다시 읽어들일 수 있습니다.

다시 말하면, canvas 엘리먼트의 도움으로 PNG 이미지를 압축해제할 수 있고 그 내용을 (eval을 사용해)실행 가능한 JS의 String 객체로 풀어낼 수 있다는 것입니다. 우리는 다시한번 원래의 PNG 이미지 데이터를 JS 문자열로 바꾸는데 너무 많은 용량 증가 없이 UTF-16트릭을 사용할 수 있습니다.

압축해제 로직

liblzg 해결책을 사용했을 때는 압축해제 코드가 별도로 필요했습니다. 이번에는 실제 압축해제 알고리즘은 필요가 없고, 다만 대략 아래와 같은 순서를 따르는 작은 프로그램이 필요합니다.

  1. UTF-16 문자열을 바이너리 문자열로 디코드
  2. btoa()메서드를 사용해 바이너리 문자열을 Base64로 인코드
  3. “data:image/png;base64,”라는 작은 헤더를 앞에 붙인 data URI를 생성
  4. data URI를 Image 객체로 로드
  5. 이미지의 onload 핸들러에서
    1. 이미지를 canvas 엘리먼트로 그리기
    2. pixel 값을 읽어들여 새로운 문자열로 할당
    3. 문자열의 내용을 실행

이 압축해제 프로그램의 크기는대략 liblzg 디코더와 비슷하지만, 다른 측면에서 DEFLATE 압축 방법은 더 좋은 압축률을 보입니다(주요 원인은 liblzg는 오직 LZ77 압축만을 사용하는데 반해, DEFLATE는 LZ77과 Huffman 압축을 조합해 사용하기 때문입니다).

PNG를 통한 접근법의 결론

지금까지 위에 설명된 PNG를 통한 방법은 압축률과 관한한(압축해제 루틴을 압축된 컨텐츠의 일부로 포함했을 때), 주어진 목표 내에서 현재의 브라우저 기술을 최대한으로 이용했습니다. 이 방법의 장점들은

  • DEFLATE 압축해제 루틴을 브라우저 내에서 재 사용
    • 일반적으로 사용 가능함(PNG를 지원하는 모든 브라우저)
    • 매우 좋은 압축률(JS 코드가 잘 압축됨)
  • 순수한 PNG 데이터를 유효한 UTF-16 문자열로 인코드
    • 바이너리 데이터를 문자열로 인코딩할 때 매우 낮은 오버헤드(Base64의 33%에 비교했을 때 3~4%에 불과)
    • 브라우저가 잘못 해석할 가능성이 매우 낮음

끝내기 – CrunchMe

여기 설명된 압축방법들이 여러분께 유용하고 흥미로웠으면 합니다. 여러분이 자신의 JS 프로그램을 압축하고자 한다면, CrunchMe 라는 오픈소스 커맨드 라인툴이 있습니다. 참고 : 이 글을 쓰고 있는 현재 CrunchMe는 아직 개발중입니다. 하지만 일반적인 사용에는 충분히 안정적일 겁니다. (그렇긴하지만, 스스로 컴파일 해야할 겁니다).