본문으로 건너뛰기

막간: Javascript, 장난감에서 벗어나다

1990년대 후반부터, TC39 회원들은 JavaScript를 진지한 전문 프로그래머들을 위한 언어로 재설계하려는 시도를 했다. 2000년대 후반기에서야 브라우저와 관련 플랫폼 개발자들은 JavaScript가 그들의 플랫폼에서 중요한 부분을 차지하며 진지한 기술적인 고려를 필요로 한다는 사실을 마침내 깨달았다.

19.1 Javascript 성능의 혁명(The JavaScript Performance Revolution)

1995년 5월 Brendan Eich가 Mocha를 만들 때, 성능은 고려 대상이나 목표가 아니었다. 아직 JavaScript 프로그램은 존재하지 않았고, Javascript로 짜일 것으로 예상되는 프로그램은 다른 더 효율적인 언어들로 구성된 객체를 간단하게 조합한 것 정도였다. Javascript는 적당히 복잡한 알고리즘을 구현하기 위한 언어로조차 생각되지 않았다. 초기의 Javascript 엔진들은 단순한 바이트코드 인터프리터나 파스 트리 평가기를 사용하여 Javascript 함수를 직접 해석하였고 단순한 메모리 관리 체계를 사용했다. 그 엔진들은 Lisp, Smalltalk, Self 및 기타 동적 언어들을 위해 1980년대와 1990년대 초반에 개발된 정교한 고성능 구현 기술들을 전혀 활용하지 않았다. Netscape/Mozilla의 SpiderMonkey와 Microsoft의 JScript 엔진의 기본 아키텍처는 10년 동안 근본적으로 변하지 않았다. ES3 수준 언어의 새로운 기능이 추가되고 보안 문제도 다루어졌다. 하지만 그 기간 동안 볼 수 있었던 어떤 성능 향상도 무어의 법칙 [Moore 1975]에 의해 주도된 하드웨어 성능 개선을 원인으로 볼 수 있다. 그 시기의 대부분의 시간 동안 브라우저의 Javascript 엔진 유지보수는 한 명의 소프트웨어 개발자의 파트타임 작업이었다.

2000년대 초반 동안 AJAX 스타일의 Javascript 기반 대규모 웹 애플리케이션의 등장은 1세대 엔진들의 성능 한계에 심각하게 부딪치기 시작했다. 2006-2007년까지 웹 개발자들은 성능 문제에 대해 더 많이 발언하기 시작했고, 브라우저 벤더들은 자신들의 Javascript 엔진의 성능 한계를 해결하기 위해 팀을 구성하기 시작했다. 성능 개선을 위한 중요한 출발점은 성능을 측정할 수 있게 되는 것이다. 애플의 WebKitg팀은 그 목적을 위해 SunSpider라는 Javascript 벤치마크[Stachowiak 2007a]를 만들었다. SunSpider는 완벽하지는 않았고 비교적 작은 테스트 케이스로 구성되었지만, 실제 웹 애플리케이션 코드에서 파생되었다. 출시된 지 얼마 안 되어서 웹 애플리게이션 개발자들은 일상적으로 SunSpider를 사용하여 브라우저 Javascript 성능을 비교하고 결과에 대해 이야기하기 시작했다. 브라우저 게임 이론의 상황은 일반적으로 브라우저 벤더들이 독자적인 Javascript 기능을 기반으로 경쟁하는 것을 막았지만, 이제 벤더들은 javascript 성능으로 경쟁할 수 있게 되었고 그렇게 하기 시작했다.

서로 다른 벤더들은 각자 다른 길로 고성능 Javascript 엔진을 만들고자 했다. 2006년 Google은 훗날 Chromeg브라우저가 될 물건을 개발하기 시작했다. Lars Bak은 그가 Smalltalk, Self, Java 가상 머신을 개발하면서 얻은 기술을 바탕으로 Chrome의 V8g JavaScript 엔진 개발을 이끌었다[Google 2008b]. 2008년 9월 Chrome이 출시되자 Chrome은 좋은 Javascript 성능의 새로운 기준이 되었다. 당시 한 보고서 [Hobbs 2008]는 V8이 구글의 벤치마크 [Google 2008a]를 당시 릴리즈 버전 Firefox의 SpiderMonkey보다 대략 10배1 빠르게 실행했다는 것을 발견했다. 그러나, SunSpider 벤치마크에서는 V8이 대략 두 배 빨랐다.

TraceMonkey [Gal et al. 2009]는 모질라의 초기 접근이었는데 캘리포니아 대학교 어바인 캠퍼스의 Andreas Gal의 졸업논문을 기반으로 했다. 이 논문은 기존의 SpiderMonkey 인터프리터를 추적 기반으로 코드를 전문화하는 JIT 컴파일러로 보완했다. 이 JIT 컴파일러는 동적으로 확인한 실행 핫스팟에 대해 최적화된 네이티브 코드를 생성했다(역주: 자주 반복되어서 수행되는 구간을 hotspot이라고 하는데, 이 부분을 런타임에 확인하여 거기에 대해서 최적화된 네이티브 코드를 생성했다는 뜻). Nitro로도 알려진 Apple의 SquirrelFish Extreme은 Self와 고성능 Lua 구현체에서 영감을 받은 기술을 사용했다. Microsoft는 초반에는 IE 8에서 사용하기 위해 기존의 JScript 엔진을 점진적으로 재설계하려고 시도했지만, IE 9에 대해서는 완전히 새로운 JIT 기반의 JavaScript 엔진인 Chakra를 구축했다.

이러한 모든 노력은 JavaScript 성능을 최적화하기 위한 지속적인 작업의 출발점에 불과했다. 오늘날, 각 주요 브라우저의 개발 활동은 성능뿐만 아니라 보안과 ECMAScript 표준의 새로운 언어 기능에 집중하고 있는 상당한 JavaScript 팀을 갖추고 있다. 이 팀들에 의해 개발된 각 엔진은 호환 가능한 오픈 소스 라이선스 하에 출시된다. 그래서 팀들은 서로의 작업을 기반으로 엔진을 구축하고 아이디어를 공유하며, 때로는 하위 시스템까지도 완전히 공유하면서 가장 빠른 JavaScript 구현을 생산하기 위해 경쟁할 수 있다.

19.2 CommonJS와 Node.js(CommonJS and Node.js)

// moda.js - source
var modp = require("modp");
exports.n = modp.p++;
exports.modName = "prefix" + exports.n;

// modb.js - source
var modx = require(require("moda").modName);
var propName = Object.keys(modx)[0];
exports[propName] = modx[propName];

그림 28. CommonJS 모듈은 모듈 로더에 의해 모듈 패턴을 구현한 함수로 변환된다. 모듈 간의 공유는 동적으로 구성된 exports 객체의 속성을 통해 이루어진다.

JavaScript는 처음 생겼을 때부터, 기본적인 스크립팅 기능을 제공하기 위해 서버 플랫폼에서도 호스팅되었다. 하지만 각 플랫폼은 자체적인 Javascript API를 제공했고 각각이 고유했다. Javascript가 나오고 15년 동안 브라우저 외의 환경에서의 Javascript 애플리케이션을 위한 도메인 독립적이고 상호 호환 가능한 공통의 환경은 없었다. 2009년 1월 Adobe와 Mozilla에서 일했으며 Khan Academy의 개발자였던 Kevin Dangoor는 그런 상황을 바꾸기로 했다. 그는 문제를 설명하는 블로그 포스트 [Dangoor 2009]를 작성했다. 그리고 온라인 토론 그룹과 위키를 통해서 이런 문제를 해결하는 데에 참여하도록 서버사이드 Javascript 커뮤니티에 권유했다. 1년 후 후속 블로그 포스트[Dangoor 2010]에서 그는 원래 만들고자 했던 것을 다음과 같이 요약했다.

  • 모듈 시스템
  • 크로스-인터프리터 표준 라이브러리
  • 몇몇 표준 인터페이스
  • 패키지 시스템
  • 패키지 레포지토리

첫 주에 224명의 멤버가 토론 그룹에 가입했고 [Kowal 2009a] 그 중 많은 사람들이 프로젝트에 기여할 의사를 표현했다. 이 계획은 처음에 ServerJS라고 불렸지만 2009년 8월 CommonJSg로 이름이 변경되었다. 이 기술은 서버 이외의 환경에도 사용될 수 있었기 때문이다. 계획은 구현보다는 명세 작성에 초점이 맞춰져 있었다.

2009년 4월까지, 그룹은 초기 모듈의 명세 [CommonJS Project 2009]를 만들었다. CommonJS 모듈 명세는 Kris Kowal과 Ihab Awad [2009a]의 설계를 기반으로 했다. CommonJS 모듈은 여러 바인딩을 포함하는 스코프를 가진 JavaScript 함수 본문이었다. 이 바인딩들은 함수 본문이 다른 모듈들과 상호 작용할 수 있게 해주었다. 이는 동기적인 모듈 로더에 의해서 구현되었다. 그 로더는 모듈의 소스코드를 가져오고 코드를 함수 정의로 래핑한 후 합성된 함수를 파싱하고 호출하여 모듈을 초기화하고 다른 모듈들과의 연결도 초기화했다. 그림 28과 같이 모듈 스코프의 선언은 합성된 함수의 지역 변수가 되었고 시스템의 제어 훅은 로더에 의해 값이 제공되는 함수 매개변수를 통해 노출되었다. require 매개변수는 요청된 모듈을 위해 컨텍스트를 고려한 로딩 프로세스를 동기적으로 수행하고 요청된 모듈의 exports값을 반환하는 함수이다2. 기본적으로 exports 값은 로더에 의해 제공된 객체이다. 모듈 코드는 exports 객체에 속성을 생성함으로써 값을 내보낸다(export). 이는 런타임에 이루어지는 동적인 프로세스이다. 모듈 이름, exports의 실제 값, 그리고 그 속성의 이름과 값은 모두 동적으로 생성될 수 있다. 이는 애플리케이션에서 어떤 모듈이 필요하고 모듈들 사이에 어떤 엔티티가 공유되고 있는지 사전에 결정하기 어렵게 만들고 때로는 아예 불가능하게 만든다.

CommonJS 모듈을 초기에 도입한 곳 중 하나는 Node.jsg였다. Node.js는 2009년 초에 Ryan Dahl에 의해 개발되고 있었다. Node.js는 대규모의 클라이언트 동시 연결을 처리할 수 있는 서버 애플리케이션을 구축하기 위한 오픈 소스 JavaScript 기반 플랫폼으로 구상되었다. Node.js는 전반적으로 비동기 I/O 모델을 사용하는 라이브러리와 함께 JavaScript 프로그래밍 환경을 제공했다. 이는 공통 POSIX 인터페이스와 JavaScript 콜백, 그리고 브라우저 이벤트 루프의 간소화된 버전을 결합했다. 그 구현은 독립적 사용을 위해 래핑된 Google의 V8 JavaScript 엔진, CommonJS 모듈 로더, 그리고 논블로킹 버전의 POSIX API와 고수준의 파일 및 네트워크 작업을 제공하는 모듈(C로 구현됨)의 집합으로 구성되었다. 첫번째 공개 버전은 2009년 5월 [Node Project 2009]에 나왔지만 Dahl이 2009년 11월에 jsconf.eu에서 발표를 한 후에야 크게 주목받기 시작했다. 그 직후 Dahl은 Node.js의 추가 개발을 관리하고 지원한 Joyent에 의해 고용되었고 이는 2015년 재단으로 책임이 옮겨갈 때까지 지속되었다[Node Project 2018].

Node.js는 서버 애플리케이션을 구축하기 위한 기술로 구상되었다. 하지만 이는 Javascript를 소형 임베디드 장치까지 포함하는 다양한 플랫폼에서 범용 목적의 프로그래밍 언어로 사용할 수 있게 해준 플랫폼이 되었다. Node.js의 I/O 모듈과 고성능 V8 엔진의 조합은 Python이나 Ruby같은 다른 동적인 애플리케이션 언어와 그 역량에서 비슷했으며 종종 성능 면에서 우월했다. Node.js는 커맨드라인 Javascript 애플리케이션을 작성하는 사실상의 표준이 되었다. Node.js는 JavaScript를 마스터한 웹 프로그래머들이 그들의 기술을 다른 종류의 애플리케이션과 브라우저 외의 환경에서도 쓸 수 있게 했다. 원래 클라이언트 웹 애플리케이션의 개발자들은 다른 대안이 없기 때문에 JavaScript로 프로그래밍했다. 그러나 많은 Node.js 개발자들은 JavaScript로 프로그래밍하는 것을 선호하기 때문에 이를 사용하기로 '선택'했다.

19.3 Javascript: 브라우저의 보편적인 런타임

Javascript는 호환 가능한 브라우저 플랫폼을 정의하는 표준들의 집합 중 하나인 프로그래밍 언어이다. Javascript는 웹 페이지 개발자들이 모든 브라우저에서 사용 가능3할 것으로 기대할 수 있는 유일한 언어이다. Java, Adobe Flash, Microsoft Silverlightg와 같은 다른 언어 환경은 이 표준 플랫폼의 일부가 아니며 해당 언어를 지원하는 브라우저에서만 브라우저의 고유한 애드온 매커니즘을 이용해서 브라우저에 통합될 수 있다. 일반적으로 언어 엔진은 브라우저 사용자가 별도로 설치해야 하며 DOM 기반의 그래픽 모델과 같은 브라우저의 표준 서비스로 완벽하게 통합되지 못할 수 있다.

브라우저 게임 이론은 표준 브라우저 플랫폼을 확장하기 위해 다른 프로그래밍 언어를 추가하는 그 어떤 시도도 성공 가능성이 매우 낮다고 예측한다. 웹을 위한 새로운 언어를 설계, 구현, 그리고 홍보하기 위해서는 브라우저 벤더의 큰 투자가 필요하며, 웹 개발자들이 이를 의미 있는 수준으로 도입할 것이라는 보장도 없다. 새로운 언어가 도입되려면 모든 주요 브라우저가 경쟁자에 의해 설계된 언어를 지원하기로 합의해야 한다. 사용자 기반도 적거나 없는 언어를 영구적인 유지보수 부담을 떠안으면서 도입해야 한다는 말이다. 예를 들어, 2011년 Google은 Dartg 언어를 내놓고 웹을 위한 더 나은 프로그래밍 언어 [Krill 2011]로 홍보했다. 그리고 Dart 가상 머신을 포함한 실험적인 버전의 Chromium(Chrome 브라우저의 기반이 되는 오픈 소스) [Google 2012a]를 배포했지만 이는 Chrome이나 다른 어떤 브라우저의 프로덕션 버전에도 통합되지 못했다.

2005년 AJAX와 Web 2.0 스타일의 애플리케이션이 등장했고 웹 개발자들은 더 크고 복잡한 웹 애플리케이션을 작성하기 시작했다. 그들 중 일부는 ES3 수준의 Javascript보다 이런 대규모 애플리케이션에 더 적합해 보이는 프로그래밍 언어를 찾고 있었다. 개발자가 어떤 브라우저에서나 웹 페이지의 일부로 실행될 수 있는 코드를 작성해야 하지만 Javascript가 아닌 다른 언어로 코드를 작성하고 싶거나 그래야 할 경우 어떻게 해야 할까? 유일한 방법은 어떤 방식으로든 JavaScript를 사용하여 대체 언어에 대한 런타임 지원을 제공하는 것이다. 이는 대체 언어의 인터프리터를 JavaScript로 작성함으로써 이루어질 수 있다. 그러나 2000년대 중반에 Javascript 엔진들은 여전히 비교적 느린 인터프리터로 구현되어 있었다. 그래서 Javascript는 효율적인 인터프리터를 작성하기에 특별히 좋은 언어가 아니었다. 2배로 느리게 동작하는 인터프리터는 그렇게 매력적인 해결책이 아니었다. 대체 언어를 지원하기 위한 더 그럴듯한 방법은 소스 간 번역기를 이용하는 것이었다. 컴파일러를 통해 대체 언어의 소스코드를 Javascript로 번역하여 브라우저의 Javascript 엔진에서 네이티브로 실행할 수 있도록 하는 것이다. 대체 언어와 Javascript의 시맨틱이 상당히 일치할 경우, 이런 방식으로 컴파일된 프로그램의 런타임 성능은 수작업으로 작성된 Javascript에 상대적으로 가까울 수 있었다.

2006년 5월에 공개적으로 출시된 Google Web Toolkit (GWT) [Google 2006]는 처음으로 널리 사용된 AJAX 툴킷으로 소스 간 번역을 사용했다. GWT는 Java에서 JavaScript로 컴파일하는 컴파일러를 내장했다. GWT는 Google의 여러 중요한 대외적 웹 애플리케이션에 성공적으로 사용되었으며 Google 외부에서도 상당히 사용되었다. GWT의 성공은 Javascript를 생성하는 소스 간 번역이 실현 가능함을 증명했다. 이후 많은 다른 언어들을 위한 소스 번역기가 나왔다. 2011년 1월 JS로 컴파일되는 언어들의 목록 [Ashkenas et al. 2011]에는 19개의 항목이 있었다. 2018년의 같은 목록 [Ashkenas et al. 2018]에는 Javascript로 번역되거나 호스트되는 270개 이상의 언어가 포함되어 있다. 이들 중 일부는 장난감 수준이거나 불완전한 구현이다. 그러나 많은 언어는 상당한 사용자 수를 가진 진지한 컴파일러들이다. 심지어 Dart를 Javascript로 번역하는 컴파일러도 있다.

소스 간 번역은 웹 페이지에서 기존 언어를 지원하기 위해서만 사용되지 않았다. 이는 새로운 언어를 실험하고 Javascript를 확장할 수단을 제공하기도 했다. 가장 성공적인 소스 간 번역기 중 하나는 2009년, 2010년 동안 Jeremy Ashkenas가 개발한 CoffeeScriptg [Ashkenas 2010]였다. 웹 개발자가 되기 전 Ashkenas는 Ruby 언어로 프로그래밍을 했다. 그리고 Javascript가 사용하는 C 스타일의 구문보다 Ruby의 상대적으로 구두점이 적은 구문과 Python 스타일의 의미 있는 공백을 선호했다. 그는 기존의 Javascript 런타임 시맨틱을 유지하면서 Javascript에 새로운 문법적인 스킨을 만드는 것으로 CoffeeScript를 만들었다. Ashkenas는 다음과 같은 설명과 함께 CoffeeScript 작업을 발표했다.

JavaScript는 Java와 비슷한 문법 속에 숨겨진 멋진 객체 모델을 가지고 있었습니다. CoffeeScript는 Javascript의 좋은 부분을 드러내려는 시도입니다. 표현식을 명령문보다 우선시하고 구두점의 소음을 줄이며 예쁜 함수 리터럴을 지공하는 문법을 통해서 말입니다. 이런 CoffeeScript를 봅시다.

square : x = > x * x

이 코드는 다음과 같은 Javascript 코드로 컴파일됩니다.

var square = function (x) {
return x * x;
};

"예쁜 함수 리터럴" 이외에도 CoffeeScript는 클래스 선언과 JavaScript 코드로 쉽게 번역될 수 있는 구조 분해 연산 등 많은 문법적인 프로그래밍 편의성을 도입했다. CoffeeScript의 많은 기능들은 ECMAScript Harmony를 위해 고려되고 있던 기능들과 유사했다. CoffeeScript는 이러한 기능에 JavaScript 프로그래머들이 관심이 있음을 입증했다. CoffeeScript는 빠르게 인기를 얻었으며 여러 주요 웹사이트 개발자들에 의해 도입되었다. CoffeeScript의 사용은 ES2015가 널리 사용 가능해진 이후에 감소했다.

2011년 5월 JSConf에서 Brendan Eich는 Jeremy Ashkenas와 함께 무대에 올라 CoffeeScript와, JavaScript가 Harmony(역주: ES6)로 발전하는 데에 있어서 CoffeeScript가 한 역할에 대해 이야기했다. 그의 발표에서 Eich [2011c]는 "transpiler"라는 단어를 소개했는데, 이는 CoffeeScript와 같은 소스 간 컴파일러를 설명하기 위한 것이었다. "transpiler"라는 용어가 그 용도로 쓰인 게 그때가 처음은 아니지만 Eich의 발표 이전에는 널리 알려지지 않았거나 널리 사용되지 않았다. 그 발표 이후에서야 "transpiler"라는 용어가 JavaScript 개발자 커뮤니티와 외부에서 일반적으로 사용되기 시작했다.

Alon Zakai [2011]의 Emscripten은 C/C++을 효율적인 JavaScript 코드로 번역하는 트랜스파일러이다. 이는 JavaScript의 32비트 산술 코딩 패턴(§3.7.3)과 이진 TypedArray 자료 구조를 사용하여 C 실행 환경을 정의할 수 있다는 관찰에 기반했다. 그리고 이 환경은 JIT 기반 JavaScript 엔진에 의해 쉽게 최적화되었다. Emscripten은 asm.js[Herman et al. 2014]에 영감을 주었다. asm.js는 최적화에 적합한 형태를 가진 Javascript의 부분집합으로, 컴파일러가 생성해야 하고 엔진이 인식하여 최적화해야 하는 일련의 Javascript 코드 패턴을 정의하는 명세다. asm.js의 성공은 WebAssembly [Haas et al. 2017]의 개발로 이어졌는데 이는 JS 엔진을 C/C++과 그 유사한 저수준 언어들에 대한 컴파일 타깃으로 사용할 수 있는 바이트코드 수준 인터페이스로 확장한다.

Footnotes

  1. 2018년 8월에 이 문서 저자 중 한 명이 2011식 빈티지 아이맥에서 당시 버전의 V8을 사용하여 같은 브라우저 벤치마크를 실행해 보았다. 그 결과는 Hobbs의 2008년 결과보다 20배 정도 빨랐다.

  2. 특정 모듈에 대한 맨 첫번째 require 호출만 전체 로딩 프로세스를 수행한다. exports 값은 로더에 의해 유지되고 있다가 이후 같은 모듈에 대한 반복된 require 요청이 들어오면 즉시 해당 값이 반환된다.

  3. 웹 페이지 개발자들은 브라우저 사용자가 JavaScript를 비활성화했거나 JavaScript를 지원하지 않는 프로그램이 웹 페이지를 처리하고 있을 가능성을 여전히 고려해야 한다.