Interop Madness

클라이언트 서버간 암호화  관련해서 개념 증명 + 몇 가지 성능 측정을 해야해서 몇 가지 언어 + 플랫폼에서 libsodium 1 을 호출해봤다. 그 와중에 겪은 일 정리.

C++

이쪽은 평범한 C/C++ 인터페이스니 문제 없이 끝.

Python

위에서 구현한 부분을 클라이언트 쪽 구현을 (빨리) 만들어서 테스트하는 작업을 했다.

여기서부터 문제가 생기기 시작. Ubuntu 16.04 (xenial) 이 제공하는 libsodium 래퍼가 python-nacl 2 인데, 써보니 내가 쓰려는 함수가 여럿 없다. 특히 crypto_stream* 류 함수가 없더라. 그래서 다른 래퍼가 있나하고 PyPI 에서 몇 가지 써보고는 빠른 포기.

이제 cffi 를 쓸 타이밍. C 코드로 정의한 함수를 변환해서 만든  .py 파일로 동적 라이브러리 (.so, .dll, .dylib) 에서 가져오는 방식을 썼다. 3 예~전에 ctypes 로 로딩할 때 괴로웠던 부분이 확실히 줄어들었다. ABI 대신 API 수준에서 접근하는 차이는 정말 컸다. cdata의 buffer 인터페이스도 쓸만했다.

cffi 로 libsodium 에서 필요한 함수들을 가져다 쓰기 시작한 이후로는 금방 일이 정리.

…하지만 이 시점에선 어떤 비참한 미래가 기다리는지 모르고 있었다. (?)

C#

(나로써는) 슬프게도, 클라이언트 쪽 구현 중 하나는 C#으로 해야한다. (Unity3d + mono) 게다가 이건 여러 플랫폼을 지원해야 한다:

  • Unity Editor: Windows, MacOS
  • Android: 다행히도 ARMv7만. Unity가 ARMv6는 지원하지 않는다 (?)
  • iOS: 기기 + 시뮬레이터에서 모두 돌아야 한다.

Windows는 (내가) 당장 쓸 머신이 없어서 일단 미뤄두고, 나머지에 대해서 잘 도는지 시험하기 시작. 꼬박 2일 걸린 것 같다 — 생각해보니 절반은 C++ 기반인 cocos2d 랑 같이 처리하는 것 때문이었던 것 같기도?

우선 각 플랫폼 별로 크로스 빌드를 시도…해야하나 했는데, libsoidum 저장소에 플랫폼 별 빌드 스크립트가 있다. 4 iOS/Android 쪽에 내가 쓰려던 함수 중 몇 개가 빠져서 컴파일 플래그를 조금 고친 것 말고는 별 일 없이 진행. 그리고 Unity 에서 (native) 동적 라이브러리를 어떻게 가져가나 고민했는데 경험 많은 동료분이 설명해주셔서 매우 간단한게 통과.

대략,

  • DllImport 로  C에 있는 함수들을 CLR 쪽에 노출하고
  • C# 코드로 이 함수들을 래핑하고
  • 다시 이 함수들을 써서 실제 개발자가 쓸 고수준 API로 바꾸자

라는 방향으로 진행했다. 에디터(=랩탑 위)에서 잘 도는걸 보고 android  기기에 올리기 시작. 왠걸, 초기화 (=sodium_init()) 은 성공했고 몇몇 초기화도 했는데, 그 이후에 호출하는 함수에서 크래시 (SIGSEGV).  (당연하게도) 콜 스택은 native code에서 죽는 것.

원인을 추측하기 시작:

  • C 함수 원형을 잘못보고 옮겼는지 확인 시작. C는 타입 무시하고 심볼 이름만 보니 내가 잘못 옮겼다면 크래시할 수도 있으니 우선 여길 의심하고 C 함수 원형과 하나씩 대조. 별 이상 없어 보였다 (불행의 단초)
  • 크로스 컴파일을 잘못했는가 확인: 생각해보니 C/C++ 로 올렸을 때는 잘 돌았다 (…)

…대략 정신이 멍해져오는데 다시 함수 원형을 보다가 불현듯 깨달은 사실:

C 쪽의 size_t 를 C#의 ulong 으로 지정했는데 Unity + ARMv7 은 32bit ABI를 쓰니 uint 여야하는 것. Orz.

그래서 이걸 iOS / Android 플래그로 ulong / uint 처리하게 했다가, iOS도 아직 32bit인게 남아있는 걸 보고는 (iPhone 5 및 그 이전 + iPhone 5C) UIntPtr 로 처리하고 강제 캐스팅하게 수정해서 마무리했다. 그러고나니 잘 돌더라.

 

3줄 요약

  • ABI 맞추기는 삽질이다. 어떻게든 API 수준으로 해결할 수 있는 법을 찾자 — python의 cffi 처럼. 아니면 기계적으로 처리할 방법을 찾아야?
  • (부득이하게) ABI를 맞춰야 한다면 가능한 arch 목록을 잘 확인하자. C/C++에서 편했던 platform dependent type들이 대재앙
  • 여러 플랫폼 지원하는 라이브러리는 정말 인생에 도움이 된다. 잘 쓰고나면 널리 알려줍시다 (?)

 


  1. libsodium 페이지. 현 시점에 적합한 대칭키 암호화 + AEAD, ECC 함수들, system random source 를 래핑한 함수, side channel 공격을 피하는 비교, 해시, … 등등을 제공하는 잘 만든 라이브러리. 인터페이스도 보고 바로 쓰는게 그다지 어렵지 않다. 
  2. GitHub 저장소. libsodium 이 fork(?)한 NaCl 을 래핑한 것 같은 이름이지만 libsodium 을 래핑한다. 
  3. API 수준에서, 소스 코드 밖에서 정의 부분을 참고해서 작성했다. 
  4. 저장소의 dist-build 디렉터리 의 스크립트들이 이에 해당한다. 

프로그래머의 일상: 험난한 python 패키징

제목처럼 거창한 건 아니지만.

웹 페이지에서 JSON web token (jwt) 를 쓸 일이 있어서 pyjwt 를 가져다 쓰기 시작했다. 근데 이걸 서비스 중인 vm들에 배포하려면 .deb 패키지로 묶어야 한다. Python 라이브러리를 .deb (간단하게) 패키징 할 때는 python-stdeb 패키지를 이용해서 간단히 묶어 버리고 있다 – 이건 기회가 되면 나중에 설명할 일이 있었으면 한다.

하지만 문제 발생:

$ python setup.py  --command-packages=stdeb.command bdist_deb
...
Traceback (most recent call last):
  File "setup.py", line 77, in 
    'jwt = jwt.__main__:main'
  File "/usr/lib/python2.7/distutils/core.py", line 151, in setup
    dist.run_commands()
  File "/usr/lib/python2.7/distutils/dist.py", line 953, in run_commands
    self.run_command(cmd)
  File "/usr/lib/python2.7/distutils/dist.py", line 972, in run_command
    cmd_obj.run()
  File "/usr/lib/python2.7/dist-packages/stdeb/command/bdist_deb.py", line 23, in run
    self.run_command('sdist_dsc')
  File "/usr/lib/python2.7/distutils/cmd.py", line 326, in run_command
    self.distribution.run_command(command)
  File "/usr/lib/python2.7/distutils/dist.py", line 972, in run_command
    cmd_obj.run()
  File "/usr/lib/python2.7/dist-packages/stdeb/command/sdist_dsc.py", line 139, in run
    remove_expanded_source_dir=self.remove_expanded_source_dir,
  File "/usr/lib/python2.7/dist-packages/stdeb/util.py", line 1061, in build_dsc
    -- %(maintainer)s  %(date822)s\n"""%debinfo.__dict__)
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 3: ordinal not in range(128)

문제의 원인이 좀 어이 없었는데 – 달력을 확인한다면 데미지 두 배,

  • 저자 이름에 non-ascii 문자가 포함되어 있고 (저자의 이름이 José)
  • stdeb 에서 문자열 치환할 때 이 처리를 하지 않고 있는 것

해당 버그는 아직 처리 중인듯.

그래서 문제를 우회할 방법이 두 가진데,

  1. stdeb 버그를 수정한다; 이 경우엔 저 위의 오류난 곳의 __dict__ 의 unicode 타입인 걸 하나 하나 찾아서 encode('utf-8') 하는 것
  2. 문제를 부정한다 (?): …이러면 안될 것 같지만 저자 이름을 수정

일단 2로 우회했는데 – stdeb 도 우분투 패키지라 이걸 덮어쓰게 배포하기가 찜찜해서 – 외부에 배포할 일이 생기거나 하면 결국 1안으로 가야할 듯 하다. 혹은 더 복잡한 다른 패키지 .deb로 만들 때 처럼 debian 파일들을 다 만들거나.

요약 (?)

  • Python 모듈을 .deb 로 변환하는 python-stdeb 패키지가 있다.
  • 근데 이 패키지가 unicode 를 제대로 못 다루는 부분이 있다.
  • 엄한 우회책은 있지만 매우 괴롭다. 지금 2016년인데…

Python 으로 JSON 빨리 처리하기

지난 며칠 동안 모 모바일 게임의 알파테스트가 있었다. 그리고 갑이 뽑아달라는 ad-hoc 통계를 뽑느라 로그 처리를 왕창 할 일이 있었다.

대략,

  • 로그 데이터는 하나의 JSON 문서로, 각 파일에 한 줄씩 차지하고 있다
  • 파일은 날짜 별로 쪼개져서 서버 로그 디렉터리에 남아있다

내부 테스트나 첫날 정도까지는 로그 처리가 순식간에 끝날 수준이라 (k초 수준; k < 10) 별로 신경 안 썼는데, 막상 데이터가 커지기 시작하니 오래 걸리기 시작.
(오래 걸린다 == 실행하고 결과 나올 때까지 걸리는 시간이 내 집중력이 유지되는 시간보다 길다)
그리고 ad-hoc 통계를 뽑으려다 보니 내가 실행하는 횟수가 계속 늘어서 걸리는 시간 줄이려고 삽질한 기록이 아래와 같다.

진행

대략 로그가 1 GiB 정도 쌓인 시점에서, 로그 처리를 완료하고 통계 데이터가 나올 때까지 55초 정도 걸리더라. (i7-2600 / 메모리 16 GiB / 샘숭 840pro)
일단 아래 두 가지로 급한 불은 껐고, 알파테스트가 끝나갈 때야 왜 빨라졌는지 알게 되었다.

일단 PyPy 를 끼얹어 본다.

메모리야 남아돌고, CPU가 100%로 튀는 상황이니 일단 PyPy 를 가져다가 써봤다.
Ubuntu 14.04 에는 PyPy 2.2.1 이 있다. 이걸 설치하고 동작하니 대략 33초까지 줄더라. (40% 향상)

최신 버전 릴리즈되는 PPA가 없나 검색해보니, 있더라: https://launchpad.net/~pypy.
여기서 PyPy 4.0.1을 가져다 쓰면 28초까지 실행 시간이 줄어들더라. (49% 향상)

mmap 도 끼얹어 봅니다.

이전에 하재승 군이 mmap 언급했던 게 생각나서 적용해보았다.
조금 더 빨라진다. 이젠 23초 수준.

여하튼 이 상태로 알파테스트 종료까지 갔더니 대략 로그 2GiB 처리하는데 53~55초 수준. 아마 처음 상태로 끝까지 갔으면 (끝나는 날 오후에도 갑이 통계 추가해달랬으니..) 내 멘탈은 더욱더 남아나지 않았을 듯 (…).

왜 빨라졌는가

왜 빨라졌는지 파악 하지 못하고 있었으니 측정해보자.
vmprof 프로파일러 를 붙여서 돌려봤다. (2:20 소요)

Python 2.7 실행 결과

loads 밑 그 하위 함수들 (decode, raw_decode) 들에서 잔뜩 시간을 쓰고 있다. 즉, 대부분의 시간을 JSON 디코딩하는데 쓰고 있다는 소리 (json.loads 함수라서)

PyPy 로 구동한건 다음과 같다. 0:55 소요.

PyPy 4.0.1 실행 결과

작은 루프를 계속해서 돌기 때문인지 대부분의 구간이 JIT 번역된 코드를 실행하고 있다. 여기서도 json.loads 호출의 비율이 크지만 Python 2.7 보다는 그 비율이 낮다.
왜 JSON 성능이 PyPy 쪽이 좋은지 확인해보니 PyPy 2.2.0 릴리즈 노트에 그런 내용이 있더란.

JSON decoding is now very fast (JSON encoding was already very fast)

근데 JSON 성능은 (보통은?) 외부 C library 에 의존해서 성능을 올린 경우가 많다. 그래서 전통의(?) simplejson 과 ujson 을 붙여서 테스트해봤다.
simplejson 으론 성능 향상이 없더라. (PyPy 의 경우 오히려 느려졌다)
C기반 JSON 구현체의 python 바인딩인 ujson 을 시도해봤다.

Python 2.7 + ujson 실행 결과

… ujson C 코드 디버깅 심볼이 없어서인지 제대로 프로파일링 안된 것 같긴한데, 같은 Python 2.7이면서도 시간은 대폭 줄었다. 1:19. (거의 절반)
그래도 통계 데이터를 실제로 해석하는 작은 루프들 성능이 JIT 쪽이 나은지 전체 시간은 여전히 PyPy 쪽이 더 좋다.

요약

  • vmprof 좋아요 씁시다.
  • Python 표준 라이브러리의 JSON은 너무 느리다. CPython을 써야한다면 ujson, 그게 아니면 PyPy 쓰자.

Scrapy로 웹사이트 크롤링 해보기

웹 사이트 몇 개(=네xx카페)를 지속적으로 크롤링 할 일이 생겨서 몇 가지 라이브러리를 뒤적여봤다.

처음 시도한 것은 selenium. 하지만 UI 요소에 의미있는 텍스트가 없거나, 반복해서 나타나는데 문맥 의존적으로 해석하게되면 완전 노가다. 그리고 내가 아는 범위 내에선 서버 데몬을 따로 띄우지 않고는 안되는 녀석이라 따로 돌리기 불편해서 중도 포기.

다음으론 항상 잘 써먹던, 비교적 규모가 작은 웹사이트에서 잘 써먹던 requests + html5lib. 근데 이것도 UI 요소에 의미있는 클래스나 아이디가 없어서 불편하긴 하더라. 그리고 동적으로 링크를 발견하고, 이걸 다시 특정 형태로 크롤링하게 하려니 작업량이 많아서 작업 중단.

마지막으로 scrapy를 썼고, 이런 류의 작업이 맞춘 툴이어서 이걸로 완성.
다음과 같은 방식으로 동작한다:

1. 시작 url 지정
2. 페이지를 긁고, 이것에 대한 parse 콜백
2-a. HTTP 요청에 대한 응답을 xpath로 뒤져서 적당한 값을 읽기
2-b. 추가로 크롤할 url을 yield. (별도로 파싱할 때 쓸 콜백도 지정)
2-c. 원하는 값을 찾으면 이걸 미리 지정한 아이템 타입으로 yield.
3. 처리할 url이 남으면 2로.

2-b, c를 혼용할 수 있는게 정말 편했다.

더불어, 실재로 작업을 하는 2-a~c만 쓸 수 있는 scrapy shell {URL} 명령이 무진장 유용했음. 해당 url에 대해서 딱 콜백 시작 부분까지 진행한 ipython shell이 뜨더라. 여기서 이거저거 테스트해보고 파싱 코드를 작성하면 되더라.

그리고 작업하다가 깨달음을 하나 얻었는데, 모바일 페이지를 크롤링하는게 몇 배 쉽다. 노가다 좀 하면 requests + html5lib 로도 가능할 듯.

세 줄 요약:

– scrapy가 웹 크롤링 작업의 반복적인 부분을 줄여주고 꽤 편하게 만들어주더라
– scrapy shell {URL or file path} 완전 편함
– 모바일 페이지가 있고, 필요한 정보가 다 나온다면 이쪽을 크롤링하는게 쉽다

gerrit 남은 리뷰 수 시각화하기

회사에서 gerrit 이라는 git 기반의 코드 리뷰 도구를 쓴다.

gerrit 최근 버전들이 RESTful API를 제공하기 시작했는데, 이를 이용해서 각 날짜 별로 남아있는 리뷰 수를 보여주는 스크립트를 작성했다.

예를 들어 android 프로젝트의 하위 프로젝트인 platform/sdk 에 대해서 그려보면 이런 그래프가 나온다.

gerrit-graph

대략 작년 1월부터 오늘까지 남은 리뷰 수 통계다 — 실제론 merge 된 것만 계산해서 최근 통계는 좀 안 맞을 수도 있다.

해당 코드는 https://github.com/reinkim/gerrit-graph 에 올려두었다. 좀 naive하긴하지만 svg로 그리니 그리긴 참 편하다 (…).

git 통계 / jenkins 통계 / gerrit 통계가지고 조금 손보면 적당히 발표할만한 주제를 잡을 수 있을 것 같다.

Python 기반의 static page 생성기로 블로그를 옮기려는 중

원래 내 의도는 최근에 읽은 책 (=”해커를 위한 디자인 레슨”) 과 게임 (=”어쌔신 크리드 2 및 그 확장팩, 3″)에 대한 리뷰를 쓰려는 거였다.
하지만 블로그 관리자 페이지 로딩이 너무 느려서 — WordPress 버전이 올라갈 수록 더 심한듯? — 좀 괴롭다.

(일반 유저가 볼) 페이지 로딩 자체는 큰 차이가 없어서 그냥 쓸 생각이 많았는데 오늘 좀 추가 많이 기운 듯 하다.

그래서 Pelican 이란 도구를 다음과 같은 이유로 시도해봤다: (이 순서대로 고려한 것은 아님)

  • OpenSource
  • History가 길다 (+5yr)
  • 현재도 개발이 진행되고 있다
  • Python 기반
  • Markdown 지원 (rst가 주 언어지만)
  • WordPress import 지원

이걸 가지고 내 블로그를 import 해보니 몇 가지 문제가 있다: (WordPress 가 정의한 xml을 rst로 변환하는 도구가 포함되어 있다)

  • 멀티바이트 인코딩된 블로그 제목이면 rst 의 #### 이 부분을 짧게 계산해서 망한다. (글자 수를 세서 그런가)
  • 일부 글에서 글자를 해석 못해서 망하고 있음
  • 글 제목(정확히는 WordPress의 slug 값)을 이용해서 .rst 를 만드는데 urlencode 당한 문자열을 쓴다 -_-

딱히 다른 대안이 없으면 importer를 고치고 어떻게 해봐야겠다;

2012, 내 프로그래밍 언어는…

작년에 이어, 올해에도 twitter에 적은 #code212에 덧붙여 글 하나 쓰기.

올해 주로 사용한 언어는,

  • Python
  • Bash script
  • C++
  • JavaScript
  • C#
  • Go

정도의 순서인 듯 하다.

작년의 순서와는 상당히 달라졌는데 그 배경은 역시 이직.

Windows 에서 C++ 코드베이스에서 작업하던 환경에서, linux 위에서 python 코드베이스를 작업하니 바뀐 게 많지(…).

 

Python

회사를 옮긴 이후로 C++ 코드는 한 줄도 안 짜고 python을 주 언어로 작업했다.

분산 서비스 (w/ restful interface)와 그 프런트엔드에 해당하는 웹 앱, 그리고 이를 테스트하는 코드들을 다 python으로 구성했다. 올해는 정말 python은 징그럽게(?)만진 듯.

감상을 적자면,

  • 편하다; 사실 이건 작년의 주 언어가 C++이라?
  • 생각을 코드로 표현하는데 부분에 걸리는 시간엔 거의 C++ 수준에 다다를 정도로 쓴 것 같다
  • 상대적으로 “뭘 만들지”에 집중하고 “어떻게 만들지”에는 조금 덜 신경 쓴다

여하튼 python으로 작성한 서비스가 내년 중엔 아마 라이브로 전환 할 듯…

Bash script

Python으로 프로그램을 만들긴 하지만 이걸 실제로 배포하고, 설치하고, 설정하는 부분은 전부 bash script. 특히나 debian packaging하면서 갈수록 bash script 짜는 기술이 늘어가는 것 같다. 어흑

C++

이전 회사에선 주로 C++을 썼다. 그리고 2월 말까지는 C++로 코드 짰음 (사실 3월에 남은 날 수 출근할 때 C++ 코드 디버깅도 했다(…) )

JavaScript (w/ html, css, …)

Python으로 짠 서비스의 프런트엔드는 결국 html + JavaScript + css.

들어가서 바닥부터 만든 한 프런트엔드의 css를 직접 짜며 삽질하다가 bootstrap으로 바꾸고 신세계가 펼쳐졌다 (…).
내가 작성한 JavaScript는 IE에서 안 돌기도 하고 – 나중에 합류한 프런트엔드 엔지니어 분이 해결해 주심 – 하여간 삽질이 Orz.

초반엔 사람이 없어서 짰는데 요즘은 거의 손 댈 일이 없는 듯. 그래도 어제 포스팅 한 일 같은 류를 처리하려면 어느 정도 알긴 해야 할 것 같은데;

C#

작년에 C# 배우면서 짰던 프로그램을 퇴사하기 전까지 유지 보수하다가 나왔음. 지금은 그 회사의 A모 게임과 B모 게임이 쓰고 있을 거라던데 난 모르겠다 (야).

이젠 Windows 환경이 아니라서 더 이상 만질 일은 없을 듯. (작년 감상을 보고 생각해도 이건 잘 된 일?)

Go

Go로 동작하는 서비스를 작성하겠다는 야망을 품었으나… (내년에 계속?)

 

정리:

  • Python 코드베이스를 작성하는 일로 옮겨왔다
  • Linux로 옮겨왔으니 bash 나 기타 등등 만질 일이 늘어남
  • Go로 서비스 작성하고 싶다 (…)
  • 내년엔 C++ 만질 일이 생길지도…

WebApp 안에 다른 WebApp 넣어서 보여주기

최근 몇 일간 이미 만들어진 WebApp A안에 다른 WebApp B이 하나의 페이지처럼 들어가 보이도록 하는 작업을 했다. 사용자는 A에는 접근할 수 있지만, B에는 (대부분) 접근할 수 없는 상태.

정석(?)은 사실 이런 식으로 해야 할 것 같지만:

  • B를 RESTful API 혹은 어떤 web API를 갖는 서비스로 작성하고 (Service B)
  • B의 RESTful API를 보여주는 view에 해당하는 WebApp을 작성하고 (App B)
  • A는 ‘Service B’를 써서 특정 페이지를 만든다

하지만 그렇게 하지는 않고, 여름에 인턴으로 잠시 일했던 puzzlet군이 사용했던 방법을 좀 고쳐서 썼다.

Web B가 html5로 작성한거라서 http proxy 비슷한거 만들기는 편했다.

인증

다행히 이 두 웹앱은 인증 서비스는 공유한다. 그래서 A에서 인증한 shared-secret을 가지고 만든 데이터를 B에게 주고 이걸로 인증하게 했다.
그리고 B에 접속할 떄 사용하는 세션 키를 A의 세션에 저장하고 꺼내서 쓰게 했다.

페이지 접근

A 의 특정 URL 밑에 오는 것은 B의 URL로 접근하게 했다. 다만 일부 접근할 수 없는 URI에 대한 처리는 있다.
이런게 없었다면 그냥 reverse proxy쓰거나 하면 될 듯도.

web_app_A_host/page_B/… 이면 실제론  B의 페이지에 접근하게 했다. 실제론 URI가 완전히 맵핑되는 건 아니고, 특정 변환을 해주긴해야 해서 reverse-proxy를 쓰진 않았다.

HTTP 응답 rewrite

http 응답은 몇 가지 조건을 걸고 rewrite했다.

1.  http 헤더에 Location 필드가 있는 경우

30x나 201, 202인 경우 이게 붙어서 오는데, 이번 경우엔 201/202는 안 써서 그냥 30x에 Location field를 rewrite하게 했다.

2. html 페이지

href, src 등으로 오는 것 전부 rewrite.

그리고 나서 css, js, 실제 내용에 해당하는 부분을 찾았다. 그리곤 다음과 같이 합쳐서 내보냈다. html5lib이 xpath 로 DOM에 접근할 수 있어서 엄청 간단히 했다.

rewrite-html

3. css 추가 처리 

CSS의 경우 골치 아픈 문제가 생기더라. B에서 온 CSS 셀렉터에 해당하는게 A 안에도 있어서 레이아웃까지 깨지고 있었다. Orz.

그래서 위에서 한 것처럼 특정 class 밑에 B의 내용이 오게 강제하고, B에서 오는 모든 css 에는 셀렉터마다 (…) 맨 앞에 저 embedded가 붙게 했다.
cssutils로 해석한 다음 각 셀렉터를 하나하나 업데이트하는 코드를 짰다.

약간 느려지긴 하던데 어차피 저건 정적으로 캐싱할 수 있으니까 신경 끄기로 (야).

 

결론(?)

  • http 요청하는 클라이언트 흉내내기는 재밌더라
  • CSS 때문에 귀찮아…
  • B가 사실 상 페이지 1개짜리 앱이니 그냥 했지, 안 그랬으면 RESTful API로 서비스 분리하는게 훨씬 속 편할 것 같다.