JavaScript 디블로팅
(github.com/naver)- JavaScript 문법은 중첩된 괄호와 콜백으로 쉽게 복잡해지고, 작은 UI에도 많은 라이브러리를 끌어오는 비대화가 생김
- WebAssembly는 브라우저에서 다른 언어를 실행할 길을 열지만, Pyodide처럼 JavaScript 이벤트 루프와의 비동기 연결 비용이 큼
- 브라우저 자원과 WebAssembly 메모리는 제한적이어서, JavaScript를 대체하려 하기보다 협상하는 접근이 필요함
- LispE는 3.3MB WASM 바이너리 하나에 450개 이상 함수를 담고, 문자열·계산·행렬·정규표현식을 함께 제공함
evaljs와asyncjs로 JavaScript 함수와 DOM을 활용하면서, 여러 외부 라이브러리 대신 감사 가능한 단일 바이너리로 코드 비대화를 줄임
JavaScript 비대화와 브라우저 제약
- JavaScript 문법은 괄호, 중괄호, 대괄호가 겹치고 닫는 순서를 맞춰야 해서 코드가 쉽게 복잡해짐
forEach콜백 안에서 여러 객체에 값을 넣고 조건부 캐시를 갱신하는 예시가 나옴
newNames.forEach((name, i) => {
allAgentContents[name] = contents[i];
agentModes[name] = modes[i];
if (compiled[i]) agentCompiledCache[name] = compiled[i];
agentViewingCompiled[name] = viewing[i];
});
- 기본 프로그래밍 언어라면 흔히 포함하는 처리를 위해 JavaScript에서는 많은 라이브러리를 내려받게 됨
- C나 C++도 작업을 하려면
include가 필요하지만, JavaScript 라이브러리는 누가 구현했고 누가 유지보수하는지 알기 어려운 경우가 많음 - 작은 창에 _hello_를 표시하려고 “인터넷의 절반”을 로드하는 듯한 페이지도 생김
- C나 C++도 작업을 하려면
- 인터넷은 JavaScript에 의존하며, TypeScript로 감추더라도 브라우저에서 페이지를 열 때마다 거쳐야 하는 문지기로 남아 있음
- 브라우저가 궁극의 운영체제가 되어 Windows나 Mac OS를 불필요하게 만들 것이라는 꿈이 있었고, 그 꿈을 구현할 언어로 JavaScript가 선택됨
WebAssembly와 JavaScript의 관계
- WebAssembly는 자체 기계어를 가진 가상 머신에 가까워, 브라우저 안에서 다른 방식으로 코딩할 가능성을 열어줌
- Pyodide는 브라우저에서 Python을 실행하는 인상적인 엔지니어링 성과지만, JavaScript 영역 안에서 움직이는 비용도 드러냄
- Python의
asyncio와 JavaScript 이벤트 루프라는 두 비동기 세계가 서로 대화해야 함 - 두 세계 사이의 다리는 취약하며, 모든
await가 어느 세계에 속하는지 기억해야 함
- Python의
- 브라우저 자원은 제한적이어서, 초기 컴퓨터 과학 시절처럼 매 명령과 구조를 비트 단위로 살펴보는 사고방식이 필요함
- 1981년에 정확히 15772 bytes만 남은 컴퓨터에서 프로그래밍을 시작했다는 기준이 제시됨
- 현대 브라우저 메모리가 그 첫 컴퓨터만큼 제한적인 것은 아니지만, WebAssembly는 일반 프로그램처럼 메모리를 자유롭게 소유하지 못하고 제한된 방식으로 먼저 허락을 받아야 함
- 핵심 태도는 JavaScript와 싸우는 것이 아니라 협상하는 것임
LispE가 제시하는 대안
- LispE는 단일 바이너리 안에 450개 이상의 함수를 제공함
- WASM 바이너리 크기는 3.3 MB임
- 문자열, 계산, 행렬, 정규표현식 처리 기능이 한곳에 묶여 있음
- WASM 바이너리는 binaries/wasm에 있음
- 작은 인터프리터지만 반환값으로 문자열,
Float64Array, 정수, 실수, 문자열 배열을 다룰 수 있음 - LispE는 Lisp 기반이라 AST가 살아 있는 구조를 가짐
- Lisp 문법이 맞지 않으면 Python이나 Basic과 구분하기 어려운 언어를 위한 트랜스파일 문법을 사용할 수 있음
- LispE는 JavaScript와 싸우지 않고 JavaScript 함수와 DOM 기능을 활용하는 방식으로 협력함
JavaScript 호출: evaljs와 asyncjs
- LispE 안에서 JavaScript 코드를 실행하려면
evaljs와asyncjs를 사용할 수 있음evaljs는 JavaScript 코드를 실행해 값을 받음asyncjs는 페이지에 정의된 사용자 JavaScript 함수와 비동기 콜백을 연결함
(setq a (evaljs "10 + 20 + 30")) ; execute some JS code
; call_llm is a user-defined JS function in the page
(asyncjs `call_llm("Implement a piece of code in Python to sort strings");` 'mycallback)
(defun mycallback(theresult) ...)
asyncjs는 작업이 끝나면 돌아오겠다는 Promise로 동작함
코드 비대화 줄이기
- 1995년 Wirth 인용은 더 빠른 컴퓨터와 더 큰 모델이 문제를 해결하지 않으며, 코드를 날씬하게 유지해야 한다는 결론으로 이어짐
- LispE는 브라우저에 노출되는 450개 함수를 단일 감사 가능 바이너리로 제공함
- 각 기능을 따로 구현하려면 수치 계산용
mathjs, 컬렉션용lodash, 문자열 조작용voca, 통계 분포용simple-statistics같은 라이브러리를 로드해야 함 - 이런 방식은 각자 다른 작성자, 버그, 유지보수 일정을 가진 수백 MB의 코드로 커질 수 있음
- 각 기능을 따로 구현하려면 수치 계산용
- LispE는 하나의 유지보수되는 코드로 이 기능들을 제공하며, 오픈소스라 전체 코드 감사를 할 수 있음
- Garbage Collector가 최악의 순간에 코드 성능을 무너뜨리지 않는다고 봄
- JavaScript와는 단순 API 호출로 투명하게 통신하며, 미리 정의된 구조를 반환할 수 있음
// floats here is a Float64Array
const floats = callEvalLispEToFloats(0, `(normal_distribution 100)`);
- LispE와 JavaScript가 함께 동작하는 예시는 데모 페이지에서 확인 가능함
- Here Are The Files: JavaScript 애플리케이션에
lispe를 통합하는 파일 - Here Are The Demonstrator Files: 데모 파일
댓글과 토론
글이 뭔가 뜬금없어서 출처를 의심했는데 네이버였다니 ㄷㄷ
근데 역시 반응은 안 좋네요... 솔직히 자바스크립트가 과장돼서 3.3MB짜리 WASM까지 올려서 리습을 쓰겠다는 게 쉽게 이해되지 않는 오버 엔지니어링이죠 ㅋㅋㅋ
네이버 계정인 이유에 대해 Claudius라는 프로젝트 개발자가 댓글을 남겼는데 본인이 Naver Labs Europe에서 일하고 있고 네이버가 오픈소스 프로젝트로 승인해서 올라갔다고 하네요.
네이버랑 크게 상관은 없는 거 같고 그냥 리습을 진짜 사랑하는 분들인듯...
Lobste.rs 의견들
-
JavaScript를 덜 부풀리겠다면서 불투명한 3.3MB WASM 덩어리를 추가하고 앱은 Lisp로 쓰라는 건가 싶음
순수 JavaScript와 추가 의존성 0개만으로도 그 크기의 10분의 1에 닿기 전까지 꽤 많은 걸 만들 수 있음- 문서의 작은 한 부분만 보고 dismiss하지는 않았으면 함
이 활용 방식에는 공감하지 않지만, 요즘 보기 드문 기묘함이 있는 재미있는 Lisp 열정 프로젝트이고, 모든 문서가 사람 손으로 쓴 티가 나는 독특한 문장이라 좋음
- 문서의 작은 한 부분만 보고 dismiss하지는 않았으면 함
-
표준 라이브러리에 새로 넣을 만한 건 언제나 제안받고 있음
최근 추가된 것으로는 집합 합집합/교집합,sum,base64등이 있음
다만 미적분 함수 요청은 거의 못 들었고, 문자열 관련으로 꾸준히 요청되는 건.reverse나.titleCase정도임
.reverse는 장난감 문제 말고 설득력 있는 쓰임새가 많지 않고,.titleCase는 가능은 하지만 국제화 데이터가 필요해서 논의가 진행 중임
게다가 둘 다 이 라이브러리에는 없음- 많은 사람이 tc39 구성원을 직접 알지 못하거나, 그들이 우리 주변에 있다는 걸 모르는 듯함
제안을 해도 실제 반영까지 3년 걸릴 테니 시도할 가치가 없다고 느끼고,utils/폴더는 별로지만 당장 만들 수 있다고 생각하는 것도 있음 - 표준 라이브러리가 내가 추적한 것보다 많이 움직였으니 글을 조금 고쳐야겠음
내 주장은 JavaScript에 함수가 부족하다는 것보다는 배포 모델에 더 가까움
표준 라이브러리가 꽉 차 있어도 평균적인 앱은 여전히 전이적 npm 의존성을 메가바이트 단위로 싣고 나감
LispE-as-WASM은 3.3MB, 문서화된 함수 약 450개, C++ 코어, GitHub 공개 소스로, 감사 가능한 런타임이 어떤 모습일 수 있는지 보는 실험임
실제로 그 위에 에이전트 하네스를 만들었고, LispE 규칙을 브라우저 안에서 즉석 실행하고 있음
JavaScript 이벤트 루프는 아주 좋아함
- 많은 사람이 tc39 구성원을 직접 알지 못하거나, 그들이 우리 주변에 있다는 걸 모르는 듯함
-
누군가 문법을 부풀었다고 표현하면 긴장하게 됨
최소 문법은 시각적 구분을 위해 서로 다른 문자를 쓰는 문법보다 훨씬 읽기 어려울 수 있음
LispE가 대안으로 내놓는 걸 보면 이렇다:(defun gol8(⍵) ((λ(⍺) (| (& (== ⍺ 4) r) (== ⍺ 3))) (⌿ '+ (⌿ '+ (° (λ (x ⍺) (⊖ ⍺ x)) '(1 0 -1) (↑ (λ (x) (⌽ ⍵ x)) '(1 0 -1)))))))... !?
https://github.com/naver/lispe/wiki/5.3-A-la-APL- 이건 당연히 유명하게 짧은 APL 예제를 포팅한 것임 https://aplwiki.com/wiki/Conway's_Game_of_Life
- 와, 이런 건 어떤 프로덕션에도 허용하지 않을 듯함
-
이 프로젝트로 들어가는 진입점이 좀 이상하고, https://github.com/naver/lispe/wiki/1.-Introduction 쪽이 더 나아 보임
WASM을 대상으로도 제공하는 네이티브 Lisp 프로젝트이고, 프로그래밍 언어 덕후들이 좋아할 만한 재미있는 요소가 있음 -
첫인상은 꽤 억지스러운 JavaScript 예제라는 느낌이었음
JavaScript 혐오의 상당 부분은 초기 브라우저 비호환성, 고통스러운 DOM API, 문법상의 함정에서 나온 것 같고, 이런 것들은 요즘 내 일상에는 사실상 영향이 없음
현대 JavaScript, 특히 TypeScript와 합리적인 린트 규칙을 쓰면 언어로서는 충분히 괜찮음
여전히 CJS 대 ESM, 공급망 위험, 생태계 변동 같은 문제는 있지만, 대부분은 언어 자체 바깥에 있음 -
괄호 균형 맞추기가 너무 성가셔서 사람들이 편집기 확장까지 만든 바로 그 경험이 훌륭하다는 건가
이런 스타일의 프로그래밍, 즉 낮은 문법 밀도와 함수형 방식을 원한다면 방향을 뒤집어서 연결형(concatenative) 언어로 가는 편이 나음 -
Lisp 해석기가 컴파일된 결과물로 어떻게 3.3MB까지 갈 수 있는지 궁금함
이게 덜 부풀리는 건가?- 큰 표준 라이브러리가 포함돼 있어서 그럴 가능성이 큼
그렇다 해도 쓰지 않는 코드를 포함한 큰 표준 라이브러리를 함께 배포하는 건 덜 부풀리는 것과 거리가 멀다는 데 동의함
- 큰 표준 라이브러리가 포함돼 있어서 그럴 가능성이 큼
-
충격! 공포!
닫는 괄호를 올바른 순서로 닫아야 한다니 놀라움- 올바른 순서로 닫지 않아도 되는 언어를 원한다면 J가 있음
JavaScript 같은 언어의 장황함에 질색하는 Lisp 애호가들도 APL 계열 언어 앞에서는 돌아서서, 최대한 짧은 것보다 명확성과 가독성이 훨씬 중요하다고 논쟁하기 시작하곤 함
LispE 작성자는 APL 기본 연산에 어느 정도 애정이 있어 보이지만, 그렇게 간결한 표기법의 언어를 접하고도 Conway의 _Life_를 APL, J, K, 심지어 Q 버전보다 길고 성긴 깊은 중첩 s-표현식 버전으로 선호하는 이유는 이해하기 어려움
- 올바른 순서로 닫지 않아도 되는 언어를 원한다면 J가 있음