Mascara: ECMAScript 4 미리 써보기
Sunday, June 22nd, 20081주일 전쯤에 Ajaxian에 흥미로운 포스팅이 올라왔다. Mascara라는 프로젝트 소개였는데, ECMAScript 4 코드를 클래식 JavaScript 코드로 변환해주는 프로그램이었다. Python으로 구현되어 있었고,
You can already download an early release. It is however of alpha-qualiy, and only suited for the adventurous.
라고 되어 있길래 급히 흥미가 생겨 뒷문장은 한 귀로 흘려버리고; 프로그램을 받아보았다. 몇달 전에 스펙 조금만 읽어서 기억이 가물가물한 ECMAScript 문법을 겨우겨우 떠올리며 몇가지 코드들을 작성하고 변환하며 재밌게 놀았다. 그러다가 이걸 써먹을만한 곳이 없을까 하고 생각해보니, 혼자서 Google App Engine으로 끄적거리고 있는 이르니아 연대기 홈페이지 리뉴얼 작업에 적용해보기로 했다. (이르니아 연대기 홈페이지 리뉴얼은 거의 4년 넘게 말만 하고 있는 만년 프로젝트;)
Mascara 사이트에서 파일을 받아서 풀어보면 굉장히 너저분하게 구성되어 있다. 나는 일단 이것을 GAE 프로젝트 폴더 안쪽에 mascara라는 이름의 폴더를 생성하여 집어넣었다.
dahlia$ touch ecmascript4.py
dahlia$ tree
.
|-- app.yaml
|-- ecmascript4.py
|-- mascara
| |-- cgitranslate.py
| |-- cgitranslate.pyc
| |-- config.py
| |-- config.pyc
| |-- django.py
| |-- es4translator
| | |-- __init__.pyc
| | |-- ast.pyc
| (...omitted)
`-- static
`-- Person.es4
2 directories, 89 files
그리고 위처럼 ecmascript4.py라는 이름으로 파일을 만들었다. 이 파일은 static 폴더 안의 *.es4 파일을 요청할 경우 그것을 JavaScript로 번역해줄 것이다. 하지만 일단은 아무 것도 하지 않고 냅두자. 우선 그렇게 하려면 app.yaml을 수정해야 한다.
아래처럼 핸들러 하나를 더 추가한다.
application: teamernia
version: 1
runtime: python
api_version: 1
handlers:
- url: /static/.*\.es4
script: ecmascript4.py
- url: /static
static_dir: static
expiration: 1d
app.yaml 파일에 대한 자세한 설명은 GAE 사이트의 해당 문서에서 볼 수 있으니 참고하시라. 이렇게 해두면 원한대로 /static/*.es4 패턴의 요청에 대해 ecmascript4.py 파일을 라우팅하게 된다. 그러면 이제 ecmascript4.py 파일을 작성해보자. GAE에서는 Django를 사용할 수도 있고, 또 대부분 그렇게들 하는 것 같지만, 나는 기본적으로 제공되는 최소주의적인 webapp 프레임워크에 더 호감이 가서 그것을 사용했다.
일단 Mascara를 사용해야 하는데, 사용가능한 Python 모듈 형태는 아니다. 그래서 import mascara라고 써봤자 없는 모듈이라면서 ImportError 예외가 난다. 사용하려는 모듈은 Mascara 폴더 안에 있는 cgitranslate.py다. 그래서 그냥 sys.path에 mascara 폴더를 추가해서 cgitranslate 모듈을 임포트 하기로 했다.
from os.path import dirname
import sys, re
sys.path.append(dirname(__file__) + '/mascara')
그 다음 webapp.RequestHandler를 서브클래싱했다.
import re
from google.appengine.ext import webapp
class ECMAScript4(webapp.RequestHandler):
def get(self):
import cgitranslate
self.response.headers['Content-Type'] = 'text/javascript'
cgitranslate.httpTranslate(
re.sub('^/', '../../', self.request.path, 1),
self.response.out
)
별 내용은 없고, Mascara에서 제공된 기능을 가져다 쓰기만 했다. Content-Type은 당연히 text/javascript로 지정했다. JavaScript로 변환되어 출력될 것이기 때문이다.
cgitranslate.HttpTranslate 함수는 변환할 ECMAScript 4 파일 경로와 그것을 출력할 유사 파일 객체(file-like object)를 인자로 받는다.
유사 파일 객체란 C++의 std::ostream과 비슷한 것으로 보면 된다. 다만 Python에서는 따로 무엇을 서브클래싱할 필요 없이 write 메서드를 구현하면 된다. (물론 입력 받기 위해서는 read 메서드 등도 구현해야 하지만, 여기서는 출력만 필요하므로…) 덕 타이핑(duck typing)에 기반한 Python다운 인터페이스라고 할 수 있다. 참고로 이런 유사 파일 객체는 굉장히 다양한 곳에서 쓰일 수 있다. 이를테면, print>>f, o은 f.write(str(o))와 동일한 작동을 한다.
마침 webapp 프레임워크의 Response는 out이라는 속성으로 유사 파일 객체를 가지고 있다. 따로 StringIO.StringIO를 쓴다거나 직접 유사 파일 객체를 만들 필요 없이, self.response.out을 전달하기만 하면 된다.
경로 문자열을 전달하기 전에 self.request.path를 약간 가공하고 있는데, cgitranslate.HttpTranslate 함수에 절대 경로를 전달할 경우 보안상 허용되지 않는다고 오류를 내기 때문이다. 그래서 맨 앞 슬래시를 ../../로 치환해준다. 왜 ../../냐면… 중요도에 비해 설명하기 귀찮으므로 생략.;;
아무튼 저렇게만 해줘도 잘 작동한다. static 폴더에 Person.es4라는 이름으로 아래 예제 코드를 저장해봤다.
class Person {
var name: !String, birthday: !Date;
function Person(n: !String, dob: !Date): name = n, birthday = dob {}
function get age(): int {
var now: Date = new Date;
var year: int = now.getFullYear() - this.birthday.getFullYear(),
nmon: int = now.getMonth(), bmon: int = this.birthday.getMonth(),
ndate: int = now.getDate(), bdate: int = this.birthday.getDate();
if(nmon < bmon || ndate < bdate)
--year;
return year;
}
}
var dahlia: Person = new Person('Hong, MinHee', new Date(1988, 8, 4));
보다시피 getter도 한번 써보고 타입 명시도 해보고 언어에 포함된 클래스도 써봤다. 브라우저에서 http://localhost:8080/static/Person.es4로 접근해보니 아래와 같이 다소 흉칙하게 변환된 JavaScript 코드가 출력된다.
function $class($SuperInstanceCtor, $ClassCtor) {
var $InstanceCtor = function() { if ($init_instance) this.$init.apply(this, arguments); };
if ($SuperInstanceCtor) { $SuperInstanceCtor = $SuperInstanceCtor.$prototype;
$ClassCtor.prototype = $SuperInstanceCtor; }
var $tmpl = new $ClassCtor($SuperInstanceCtor, $InstanceCtor);
$InstanceCtor.prototype = $tmpl; $init_instance = false;
$instancePrototype = new $InstanceCtor(); $InstanceCtor.$prototype = $instancePrototype;
$init_instance = true; return $InstanceCtor; }
function $coerceTo(x, type) { if (x instanceof type) return x;
throw new Error("coercion error"); }
var Person=$class(null, function($super, $static) {
this.name;
this.birthday;
this.Person=function(n,dob){
this.name=n;
this.birthday=dob;
};
this.get_age=function(){
var now=new Date;
var year=now.getFullYear()-this.birthday.getFullYear();
var nmon=now.getMonth();
var bmon=this.birthday.getMonth();
var ndate=now.getDate();
var bdate=this.birthday.getDate();
if(nmon<bmon||ndate<bdate)--year;
return year;
};
this.$init = this.Person;
});this.Person=Person;
var dahlia=new Person('Hong, MinHee',new Date(1988,8,4));
/* TRANSLATION DONE: ../../static/Person.es4 */
이제 이걸 HTML에서 <script> 태그로 불러와 사용하면 된다.
<script type="text/javascript" src="/static/Person.es4"></script>
type 속성을 application/ecmascript; version=4가 아닌 text/javascript로 지정한 것에 주의하자. 뭐, 당연한 소리지만…….
이렇게 하고 나서 혼자 짝짝짝 박수치며 좋아하다가 변환이 꽤 비용도 크고 속도도 느릴 것이라는 생각이 들었다. GAE에서 지원하는 Memcache로 캐시도 되게끔 코드를 수정했다.
from google.appengine.api import memcache
class ECMAScript4(webapp.RequestHandler):
half_day = 3600 * 12
def get(self):
key = 'ecmascript4\t' + self.request.path
js = memcache.get(key)
if not js:
from cgitranslate import httpTranslate
from StringIO import StringIO
buffer = StringIO()
httpTranslate(re.sub('^/', '../../', self.request.path, 1), buffer)
js = buffer.getvalue()
memcache.set(key, js, self.half_day)
self.response.headers['Content-Type'] = 'text/javascript'
self.response.out.write(js)
한번 생성한 캐시는 한나절 동안 가지고 있는다. 사실 해당 소스 파일의 저장 시각을 기준으로 비교하면 더 좋겠지만, GAE에서는 파일 접근을 허용하지 않아서 어쩔 수 없었다. 아무튼 이렇게 해놓고 브라우저로 접근하니, 첫번째 접속할 때는 여전히 느렸다. 하지만 그 이후로 새로고침을 하면 JavaScript 코드가 매우 빨리 보이는 것을 확인할 수 있었다. 대신 Person.es4 파일을 아무리 수정해도 브라우저에서는 반영되지 않았다. 흑흑.
Mascara를 가지고 놀다가 삼항연산자를 타입 명시로 해석해버리는 버그도 하나 발견했다. 아까 한귀로 흘렸던 alpha-qualiy
라던가 only suited for the adventurous
라는 말들이 생각나는 순간이었다. (;;)
2008년 6월 27일 추가: 위와 같이 할 경우 스크립트의 컴파일 에러가 날 경우, 에러 메세지 자체를 캐시하게 된다. 에러 메세지에 대해서는 캐시하지 않고, 성공했을 때만 캐시하도록 하려면 아래처럼 작성해야 한다.
class ECMAScript4(webapp.RequestHandler):
half_day = 3600 * 12
def get(self):
key = 'ecmascript4\t' + self.request.path
js = memcache.get(key)
if not js:
from cgitranslate import *
try:
path = re.sub('^/', '../../', self.request.path, 1)
output = translateInCgi(path)
if output.succeeded():
js = output.resultcode
memcache.set(key, js, self.half_day)
else:
js = errorResponse(
"Compilation failed",
"".join(output.getMessages())
)
except:
js = errorResponse("Translation failed", formatException())
self.response.headers['Content-Type'] = 'text/javascript'
self.response.out.write(js)
