keyword: 자바스크립트
, 정적 분석
, 동적 언어
, 코드 최적화
1995년 당시는 컴퓨터 리소스가 비쌌을 뿐만 아니라 네트워크 인프라의 낮은 대역폭 등 다양한 문제들로 인하여 대부분의 작업을 서버에서 처리하였습니다. 그러다 컴퓨터 리소스의 비용이 낮아지면서 웹 브라우저에서 입력 검증처럼 작은 작업들을 처리할 수 있게 되면서, 해당 작업들을 처리하기 위해 자바스크립트가 등장했습니다.
자바스크립트는 인터프리터 언어로 동적인 특징을 가지고 있습니다. C나 Java에서 볼 수 있는 Int와 같은 자료형 변수가 존재하지 않습니다. 포인터로 존재하는 var 키워드는 변수, 함수 등 타입과 종류를 가리지 않고 다 저장할 수 있습니다.[^1] 하지만 이런 특성으로 인해 var 을 통해 선언된 개체가 가지고 있는 값이 무엇인지 추론하기 어려워졌고, 실제로 실행을 해봐야지만 그 값을 판단할 수 있게 되었습니다. 이 특징을 duck typing
이라고 부르는데, C나 Java에서 가능한 개발중 코드 분석을 통한 최적화를 어느정도 포기해야 하는 상황을 가져왔습니다.
본질적으로 문제가 되는 부분은 언어가 모듈을 다룰 수 있는 기능이 없다는데 있습니다. C에서는 include
를 통해서 Java에서는 import
를 통해서 모듈을 불러올 수 있는데, 자바스크립트에서는 해당 기능이 없습니다. 다른 파일들의 기능을 사용하려면 해당 파일들을 모두 브라우저에서 읽어온 다음에 실행을 해야만 했습니다. 하지만 파일들 사이의 의존관계를 정의할 수 있는 방법이 없어 조금이라도 복잡한 기능을 가진 서비스를 만들면 파일을 읽어올 때 의존관계가 꼬여버리는 문제가 자주 발생했습니다. 이러한 의존관계 문제를 풀기 위해서는 하나의 파일안에 모든 기능을 구현하던가, 의존관계를 다른 곳에 정리해서 해당 순서대로 브라우저에 포함하는 방법을 이용해야 했습니다.
다행히도 자바스크립트 커뮤니티의 노력으로 의존관계를 쉽게 선언할 수 있는 형식이 만들어졌고, 현재는 크게 CommonJS와 AMD라는 2가지 형식이 자바스크립트 세상을 이끌어 가고 있습니다. 그러나 의존관계는 해결했지만 위에서 언급했던 코드 최적화문제는 해결하지 못했습니다. 다행히도 최근 표준에 의존성 문제와 코드 최적화 문제를 해결할 수 있는 방법이 도입되었고, 해당 표준을 이용하는 실험적인 라이브러리가 많이 등장했습니다. 이번 논문에서도 그중 하나인 RollUp을 사용하였습니다.
본 논문에서는 기존의 정적분석의 특징과 그 한계를 실험을 통해서 소개한 후 RollUp에서 도입한 방식의 장점, 단점을 소개합니다. 그 후에는 자바스크립트 코드 최적화 문제 해결 방법이 나아갈 방향을 제안하려 합니다.
자바스크립트 코드 최적화 문제는 의존성 문제와 필연적으로 엮이게 됩니다. 예를 들어 A, B, C 라는 파일이 있을 때 서로 간의 의존성을 파악하여 실제로 사용하지 않는 기능을 제거한다면, 파일 크기를 작게 만들 수 있게 되고 브라우저 엔진이 더 빠르게 코드해석을 완료할 수 있게 됩니다. 다르게 이야기하면 웹 서비스를 더 빨리 로딩하여 더 빠르게 이용할 수 있다는 뜻입니다. 서비스 제공자 입장에서는 이 작은 차이가 큰 차이를 만들 수 있기 때문에 상당히 중요한 문제입니다. 패이지 로딩과 관련해 놀라운 결과를 보여주는 연구가 있습니다
"페이지 반응이 1초 지연될 경우 전환율이 7% 감소할 수 있다."
"47%의 소비자는 웹 페이지가 2초 이내에 로드되기를 기대한다."
다르게 이야기하면 페이지가 느리게 로드된다면, 사용자는 그만큼 다른 사이트로 많이 빠져나가게 된다는 뜻입니다. 특히 경쟁 사이트가 동일한 콘텐츠를 제공한다면 그 가능성은 더욱 높아질 수 밖에 없습니다. 이러한 산업계의 요구에 따라 페이지 로딩시간을 줄이기 위한 다양한 연구들이 진행되었고 코드 최적화를 위한 방법들이 많이 소개되었습니다.
먼저 기존 연구에서 나타났던 정적 분석의 한계를 소개하려 합니다. 자바스크립트에서 흔히 나타나는 코드 형태는 다음과 같습니다.
// 모듈 A, 외부 라이브러리
var library = {
getWorld: function () { return 'world'; },
getUniversity: function () { return 'university'; },
getStudent: function () { return 'student'; }
};
// 모듈 B, 비즈니스 로직 객체 - 라이브러리와 결합
var myModule = (function (reference) {
var module = {};
var names = ['World', 'University', 'Student'];
var fn1, fn2;
names.forEach(function (name) {
fn1 = 'hello' + name;
fn2 = 'get' + name;
module[fn1] = reference[fn2]
});
return module;
})(library);
// 모듈 A, B 사용
myModule.helloWorld();
library
객체는 내부적으로 helloWorld
, helloUniversity
, helloStudent
라는 3가지 함수들을 가지고 있습니다. 각 함수들은 다른 모듈에 있는 함수를 새롭게 매핑한 함수입니다. 코드에서 모듈 B는 외부 라이브러리를 참고해 자신만의 기능을 만들어갑니다. 라이브러리에 있는 기능은 라이브러리에 처리를 위임하고 자신에게 맞는 추가 처리를 하기도 하며 기능들의 관심사 분리를 진행합니다. 소프트웨어 엔지니어링 학문에서 언급한 낮은 결합도 높은 응집도를 만들어가는 과정입니다.
그런데 여기에는 문제가 있습니다. 바로 외부 라이브러리를 가져올 때, 실제로 사용하는 함수들이 어떤 함수들인지 판별할 수 없다는데 있습니다. 코드에서 17번 줄을 보면 외부 라이브러리 함수를 비즈니스 로직 객체 함수로 매핑하면서 두 모듈 사이의 상관관계를 정의하고 있습니다. 상관관계에 대한 선언은 15, 16번 줄에서 name
변수와 문자열의 연결을 통해 동적으로 일어나기 때문에 24번 줄에서 사용하는 함수는 한가지임에도 불구하고, 코드 최적화를 통해 그 함수만 가져올 수 없는 상황이 발생합니다.
기존에는 이러한 문제를 해결하기 위해 크게 두가지 접근 방법을 취했습니다. 첫번째 방법은 민감도를 올려 계산을 더 많이 수행해 자세한 분석을 시행하는 방법입니다. 민감도를 올린다는 것은 프로그램의 흐름, 함수 호출 정보, 반복문 상태 등을 더 자세하게 분석하여 함수들 사이의 상관관계를 파악한다는 뜻입니다. 특히 위 예제와 같은 형태에서는 반복문 상태 분석이 핵심입니다. 1차원 반복문이지만, 다중 반복문으로 바라보고 맥락이 비슷한 반복문끼리 분할해야 합니다. 최근 연구에서는 이러한 부분에 중점을 두고 분석하는 방법을 다양화 하고 있습니다.[^2,3] 이 방법의 가장 큰 단점은 폭발하는 계산량입니다. 반복문을 Head, Body, End 3가지 스텝으로 분할하여 각 스텝 특징을 통해 맥락을 결정하기 때문에 반복문의 깊이를 고려한다면 추가적인 오버헤드가 많이 발생합니다. 따라서 적용할 수 있는 케이스는 한계가 있을 수 밖에 없습니다. 그리고 민감도를 올리는 부분에서도 한계가 있어서 첫번째 방법은 추가적인 돌파구가 요구되는 상황입니다.
두번째 방법은 정확한 분석을 포기하고 객체의 필드만을 기준으로 어림짐작하여 포함 여부를 결정하는 방법입니다.(field-based analysis) 이 방법의 장점은 계산 비용이 낮고 속도가 빠르다는 점입니다.[^4] 실제 제품으로써 적절한 품질의 빠른 속도의 결과물이 필요한 때가 있습니다. 일반적으로 프로그램을 개발할 때 통합개발환경(IDE)을 사용하는데, IDE 내부적으로 같은 개체에 대해서 코드 하이라이팅을 해주는 기능을 기본으로 사용합니다. 이때 같은 이름이더라도 다른 스코프에 있거나, 다른 모듈에 있으면 다르게 표시해줘야 하는데, 바로 이 두번째 방법을 사용해서 빠르게 처리할 수 있습니다. 이 방법은 호출의 깊이의 빈도에 중점을 두고, 평균적으로 검증된 적절한 검색 깊이를 설정합니다.[^5] 해당 연구 결과로 3번 함수 호출 깊이를 제안하는데 첫번째 방법과 비교할 때 훨씬 낫은 수의 검색 깊이이기 때문에 속도는 훨씬 빠르게 적절한 품질의 결과물을 얻을 수 있습니다. 하지만 두번째 방법의 한계도 분명히 있습니다.
var one = {
hello: function () { return 'function one'; }
};
var other = {
hello: function () { return 'function other'; }
};
one.hello();
분명히 호출하는 함수는 one
객체의 hello
함수인데, 필드를 기준 값으로 잡기 때문에 other
객체의 hello
함수도 사용된다고 판단을 해버립니다. 이 경우 정확도가 많이 떨어지게 됩니다.
그런데 두 방법을 살펴보면서 중요하게 생각해 볼 부분은 두 방법 모두 동적 언어에서 정적 분석을 수행하는 어려움을 겪었다는 점입니다. 위의 예제에서 언급했던 것처럼 코드의 실행 맥락에 따라 결정되는 부분이 있기 때문에 최대한 방어적인 접근 방법을 취할 수 밖에 없습니다. 이 부분이 자바스크립트 정적분석의 가장 큰 걸림돌입니다.
기존에 있는 도구를 살펴보기 전에 먼저 정적 분석 과정에서 중요한 개념인 추상구문트리를 설명하겠습니다. 추상구문트리(이하 AST)는 프로그래밍 언어로 작성된 코드에서 구문들을 추상화하여 트리 형태로 만든 데이터 구조입니다. 추상화의 의미는 실제 코드 문법에 맞춰 트리를 생성하는 것이 아니라 실제 코드를 특정 범주로 매핑하여 추상화를 한다는 의미입니다. 예를 들면 코드 구문에서 if-else
구문은 하나의 중심 노드와 가지로 뻗어 나가는 형태로 매핑을 합니다. 실제 문법에서 추상화를 하기 때문에 실제 코드를 분석하는 구문분석파서가 만들어내는 결과물인 parse tree
의 형식으로 적합합니다. 대부분 AST는 파싱 이후 소스코드로부터 필요한 의미정보들을 분석해내는 의미분석을 하기 위해 사용됩니다. 의미분석 과정에서는 심볼 테이블
을 생성하여 구문들 사이의 의존관계를 파악하여 프로그램의 적합성을 검증하기도 합니다.
도입부에서도 언급하였지만 자바스크립트에서는 duck typing
특징 때문에 컴파일 시간이 아닌 실행시간에 값의 문맥이 결정되는 경우가 많습니다. AST를 통해서 어느정도 추상화를 거치면 분석이 가능한 부분이 생기기 때문에, AST를 통해 심볼 테이블
을 만들어냅니다.
현재 자바스크립트 세상에서 가장 유명한 코드 정적 분석 도구는 Uglifyjs
입니다. Uglifyjs는 기본적은 추상구문트리(AST)에 기반을 두고 있습니다. Uglifyjs
는 파서가 자바스크립트 코드에서 AST를 만들고, AST를 더 작은 AST로 최적화합니다. 실제로 Uglifyjs는 압축화, 난독화를 추가적으로 진행하지만 본 논문에서는 코드 최적화에만 중점을 두겠습니다.
Uglifyjs
에는 코드 최적화를 위한 옵션이 dead_code
라는 값으로 존재합니다. 해당 값이 설정되면 내부적으로 eliminate_dead_code
함수를 호출합니다. eliminate_dead_code
함수는 AST의 특정 노드로부터 코드 최적화를 시작합니다. 그리고 죽은 코드라고 판별하는 경우는 다음 두가지 경우 입니다.
- 현재 반복문 내에 있고, 지금 실행하는 구문이
break
인 경우- 이 경우
break
이후 코드들은unreachable
, 즉 절대 실행될 수 없는 영역이라고 볼 수 있습니다. 따라서 죽은 코드로 판별합니다.
- 이 경우
- 현재 반복문 내에 있고, 지금 실행하는 구문이
continue
인 경우- 위의 1번과 같은 논리로
continue
이후 코드들은 죽은 코드로 판별합니다.
- 위의 1번과 같은 논리로
가장 간단한 예제를 살펴보면 다음과 같습니다.
// case 1
function f() {
a();
b();
x = 10;
return;
if (x) {
y();
}
}
// return 구문 이후는 다 제거된다.
function f() {
a();
b();
x = 10;
return;
}
// case 2
function f() {
g();
x = 10;
throw "foo";
if (x) {
y();
var x;
function g(){};
(function(){
var q;
function y(){};
})();
}
}
// throw 아래 구문들 중 호이스팅으로 인해 영향을 주는 구문을 제외하고 다 제거된다.
function f() {
g();
x = 10;
throw "foo";
var x;
function g(){};
}
Uglifyjs
는 정적분석의 한계를 인식하고 코드최적화 영역을 도달하지 않는 영역
에만 제한합니다. 먼저 의존관계에 있는 파일들을 다 하나도 합친 후에, 그 중에서 최적화를 시도합니다. 현재 자바스크립트가 시도하는 방법 중 첫번째 방법이라고 볼 수 있습니다. 프로그램의 흐름, 함수 호출 정보, 반복문 상태 등을 더 자세하게 분석하여 함수들 사이의 상관관계를 파악하기 때문입니다. 실행중에 발생하는 의도하지 않은 사이드이펙트를 없애기 위해 최대한 방어적으로 죽은 코드
를 골라냅니다.
이 부분이 Uglifyjs
코드 최적화 기능의 가장 큰 장점이자 단점입니다. 자바스크립트의 문법에만 관심을 집중하기 때문에 문법적인 측면에서 도달하지 않는 영역을 골라내는 기능은 충분하지만, 실행 문맥을 좀 더 똑똑하게 골라낼 수 있는 부분이 분명히 있음에도 활용하지 못하고 있기 때문입니다.
최근에는 좀 더 나아가 언어 차원에서 모듈 시스템이 도입되었습니다. 자바스크립트 커뮤니티와 ECMA 국제기구의 노력으로 표준[^7]에 import
, export
구문이 정의되었습니다. 표준에서도 해당 구문을 정적인 문법으로 정의했습니다. 코드 최적화 관점에서 이 모듈 시스템을 주목해야 할 이유는 바로 사용하는 기능들을 명시적으로 선언하기 때문입니다. 다르게 표현하면 죽은 코드라고 판단할 수 있는 또 하나의 특징이 등장했다고 볼 수 있습니다.
자바스크립트 모듈시스템이 CommonJS
, AMD
의 동적인 구조에서 ES6 모듈의 정적인 구조로 바뀐 이유는 모듈을 정적 단계에서 좀 더 명확히 할 때 나타나는 단점보다, 이점이 크기 때문입니다. 정적인 모듈 구조는 필요한 의존관계를 컴파일 타임에 결정할 수 있습니다. 동적인 모듈 시스템과 비교했을 때 그저 파일 가장 위에 있는 import
구문만 살펴보면 필요한 의존관계를 알아낼 수 있습니다. 이부분은 정적인 단계에서 파악이 가능합니다. 코드로 살펴보면 다음과 같습니다.
- 동적인 구조
var library; // 전역 공간에 선언되어 라이브러리가 외부로 노출됩니다.
if (Math.random()) {
library = require('foo');
} else {
library = require('bar');
}
- 정적인 구조
if (Math.random()) {
// 'foo'를 직접 노출합니다.
exports.foo = '...';
} else {
exports.bar = '...';
}
동적인 구조에서는 실행했을 때 require
구문을 통해 외부 라이브러리를 읽어와야지만, 의존관계를 명확히 할 수 있습니다. 반면에 정적인 구조에서는 foo
, bar
가 다른 import
를 통해서 정의가 되었든, 아니면 동일한 스코프 내에 정의가 되었든 출처를 분명히 할 수 있고 컴파일 단계에서 확인이 가능합니다. 또한 유일한
이름을 가지고 외부로 노출되기 때문에 의미분석 단계나 심볼 테이블을 생성하는 단계에서도 독립적인 문맥을 가질 수 있게 됩니다. 단점은 정적인 단계에서 의존관계를 반드시 명시함에서 오는 유연하지 못함입니다. 동적인 구조에서는 유연하게 상황에 따라 조건적으로 다른 라이브러리들을 가져다 사용할 수 있었다면, ES6 모듈에서는 그렇지 못합니다. 따라서 유연함을 조금은 포기해야만 했습니다.
ES6 모듈에서는 모듈들을 하나의 스코프 안에서 다룰 수 있다고 가정합니다. 하나의 독자적인 스코프 안에 존재해야지만 모듈
이 가지는 의미를 분명히 할 수 있고, 또한 각 모듈들을 효율적으로 통합하여 다시 하나의 스코프 안에서 관리를 할 수 있게 됩니다. 두번째로 import
를 단지 읽기만 가능한 뷰
로 정의하여 모듈을 통합할 때 복사
하는 것이 아니라 바로 포함
하여 참조를 통해 접근하는 방법을 사용합니다. 이는 첫번째 가정에 근거를 두고 있습니다. 모듈이 하나의 독립적인 스코프안에서만 존재하고, 통합되었을 때 다시 하나의 독립적인 스코프 안에 존재한다면 모든 모듈들은 독립적으로 존재하기에 복사할 필요없이 바로 참조해서 사용할 수 있습니다.
위에서 언급한 죽은 코드를 판별할 수 있는 또 하나의 특징이 등장했다는 건, 곧 여러개의 새로운 방법들을 시도해 볼 수 있다는 뜻입니다. RollUp.js
는 이 특징을 이용하여 코드 최적화 문제를 새롭게 풀어냅니다.
직역하면 나무 흔들기인데, 말 그대로 의존관계를 분석해 사용하지 않는 기능들을 떨어뜨려 제거한다는 뜻입니다. 하지만 실제 접근방법은 단어와는 차이가 있습니다. 실제로는 사용하지 않는 기능들을 제거 하는 것이 아니라 사용하는 기능들을 선택합니다. 기존과 정반대의 접근방법을 취해서 코드 최적화 문제를 해결합니다. import
구문을 살펴봄으로써 사용하는 기능들만 선택할 수 있게 됩니다. 코드로 보면 좀 더 분명해집니다.
- 기존 방식
var utils = require( 'utils' );
var query = 'Rollup';
utils.ajax('https://api.example.com?search=' + query)
.then(handleResponse);
- ES6 모듈 (Tree-shaking)
import { ajax } from 'utils';
var query = 'Rollup';
ajax('https://api.example.com?search=' + query)
.then(handleResponse);
ES6 모듈시스템에서는 분명하게 사용하는 함수인 ajax
만 불러오고 있습니다. 이를 통해 컴파일 단계에서 코드 최적화가 가능하게 됩니다. 물론 코드 최적화에 대한 노력은 개발자가 추가로 해야하는 측면이 있습니다. 분석 과정에서 false positive
가 나오지 않도록 submodule
을 전략적으로 선택해야 하는 경우도 있습니다.
// DONT
import { map } from 'lodash-es';
// DO
import { map } from 'lodash-es/map';
최종적으로 이 모든 방법들을 결합해보면 다음과 같은 결론을 내릴 수 있습니다.
- 기존 방식은 최대한 방어적인 코드 최적화에서 강점을 보인다.
- ES6 모듈 방식은 사용하는 코드를 뽑아내는데 강점이 있지만,
false positive
가 나올 수 있는 가능성이 있다.
따라서 본 논문에서는 사용하는 코드 포함 + 사용하지 않는 코드 제거 두 방법 모두 사용해 코드 최적화를 진행합니다. 이를 통해 사용하는 코드를 뽑아낸 후 방어적인 코드 최적화를 통해 false positive
를 걸러내려 합니다.
앞서 이야기했던 방법을 다음처럼 구체적으로 풀어볼 수 있습니다.
- 의존관계에 있는 모든 ES6 모듈 파일들을 하나로 합칩니다. 이때 모듈 스코프 내에서
exports
가 되어도 다른 파일에서import
가 되지 않았다면 합치지 않습니다. - 합쳐진 파일 가운데서
export
되지 않았거나 내부에서 사용되지 않는 코드를 제거하여 죽은 코드를 제거합니다.
특히 주목할 부분은 1번 부분입니다. 1번에서 파일들을 합치는 과정에서 1차적으로 tree-shaking
이 일어나기 때문입니다. 정적 스코프 내에서 의존관계를 어느 정도 분석해내기 때문에 2번에서 기존 방식으로 사용하지 않는 코드를 걸러낼 때 더 효과적인 적용이 가능해집니다. 실험을 진행할 환경은 다음과 같습니다.
OS | Mac OS 10.12 Sierra |
---|---|
node | 5.6.0 |
Uglifyjs2 | 2.4.10 |
RollUp | 0.36.3 |
underscore | 1.8.3 |
jasmine | 2.5.0 |
jQuery | 3.0.0 |
실험을 진행할 코드입니다. 현재 자바스크립트 오픈 소스 중 가장 널리 사용되고 있는 underscore와 jasmine, jQuery를 선정해, 해당 라이브러리를 사용하는 코드를 작성하였습니다.
// case 1 - underscore
import _ from 'underscore';
var msg = _.map(['hello', 'world'], function(str) {
return _.upperFirst(str);
});
var random = _.range(100).filter(_.isOdd);
var result = _.result(random, 'keys');
function negate(fn) {
return (!fn) ?
function noop () {} :
function () {
return !fn.apply(this, arguments);
};
}
// ------------------------------------------------
// case 2 - jasmine
import jasmine from 'jasmine';
describe('test suite #1 - hello world', function () {
var base = ['olleh', 'dlrow'];
it('reverse should works on hello world', function () {
var expected = 'hello world';
var result = base.forEach(function (str, i, array) {
array[i] = reverse(str);
}).join(' ');
expect(result).toBeDefined();
expect(result.length).toBe(Number);
expect(expectes).toBe(result);
});
});
// ------------------------------------------------
// case 3 - jquery
import $ from 'jquery';
var body = $('body').addClass('not-dead-section');
body.on('click', function(e) {
alert('body clicked');
});
이 테스트 케이스들은 모두 해당 모듈의 일부 기능만 가져다 쓰고 있습니다. 이 코드로 최적화를 진행했을 때 다음과 같은 결과가 나옵니다.
underscore | jasmine | jQuery | |
---|---|---|---|
원래 크기 | 52Kb | 97kb | 257Kb |
Uglifyjs | 52kb | 90kb | 257Kb |
rollup | 32kb | 50kb | 257Kb |
Uglifyjs + rollup | 28kb | 48kb | 257Kb |
결과를 확인해보면 underscore는 원래 크기 대비하여 47%, jasmine은 51% 정도 크기가 감소하였고, jQuery는 하나도 감소하지 않았습니다. 결과에서 두가지 부분을 주목할만 합니다.
- underscore, jasmine는 Uglifyjs를 적용했을 때는 크기 감소가 없었지만, rollup을 적용할때 마다 크기 감소가 있었습니다. 그리고 둘 다 적용했을 때는 더 큰 최적화를 성공했습니다.
- jQuery는 Uglifyjs, rollup 모두 아무런 효과를 얻지 못했습니다.
먼저 첫번째 케이스인 underscore에 대한 rollup의 효과는 앞서 언급한 tree-shaking
특징이 가져온 결과입니다. underscore는 유틸리티 라이브러리이기 때문에, 내부적으로 큰 객체들로 분리가 되어 있습니다. 전체적으로 Array
, Collection
, utils
, prototype
, Object
등 으로 분리가 되어 있기 때문에 잘라낼 수 있는 영역을 확보할 수 있었습니다. 또한 Uglifyjs를 적용함으로써 합쳐진 파일안에서 죽은 코드를 판별해 냅니다. 이렇게 제외되는 함수들은 내부에서 사용되지 않는 코드들이 대부분입니다. 하지만 실제로 rollup을 통해서 많이 걸러졌기 때문에 방어적인 코드나 메타 정보들이 제거되는 정도입니다.
두번째 케이스인 jasmine도 상황은 underscore와 비슷합니다. 테스트 헬퍼 함수들은 core
, matcher
, asymmetric equality
등 크게 분리가 되어 있기 때문에 잘라낼 수 있는 영역을 확보할 수 있었습니다.
세번째 케이스인 jQuery가 최적화 버전과 원래 버전이 같은 이유는 분명합니다. jQuery는 underscore와 다르게 유틸리티 라이브러리가 아닌, 즉시실행함수로 감싸진 하나의 독립적인 헬퍼 라이브러리입니다. 선택자를 파싱하는 기능도 Sizzle
을 이용하여 의존관계를 가져가고 있고, 다른 내부 기능들도 underscore, jasmine처럼 모듈화가 잘 되어 있습니다. 하지만 서로가 서로에게 의존관계가 강하게 형성이 되어 있어서 모든 기능들은 살아있는 코드로 판별하게 되어 원래 버전과 최적화 버전이 같은 크기가 되게 됩니다. 죽은 코드를 판별해낼 때도 동일합니다. 의존관계가 명확하게 정의되어 있고 모듈화가 분명하고 또한 CommonJS 형태로 작성이 되어 있기 때문에 동적으로 판단되는 부분들은 제거될 수 없어서 결과물에 전부 포함이 됩니다.
결과로부터 얻을 수 있는 인사이트는 분명합니다. ES6에서 도입된 네이티브 모듈 시스템이 자바스크립트 코드 최적화의 새로운 길을 열었다는 것입니다. CommonJS 형태로 작성된 라이브러리는 현재 코드 최적화의 한계가 있지만, Uglifyjs와 rollup을 함께 적절히 사용을 한다면 개발과 빌드, 배포 과정에서 많은 이점을 가져올 수 있습니다. 첫번째와 두번째 케이스를 대상으로 평균을 내본다면 평균적으로 49%의 감소된 크기의 라이브러리를 얻을 수 있습니다. 이는 현재 코드 최적화가 아닌 minify
라는 다른 단계를 거친다면 더욱 효율적인 리소스 배포가 가능하다고 볼 수 있습니다.
지금까지 논문에서는 자바스크립트에서 코드 최적화가 어떤 의미가 있는지, 현재까지 연구된 방법은 어떤 방법인지, 그리고 그 방법과 비교해서 새로운 접근 방법이 어떤 것이 있는지 알아보았습니다. 그리고 이전 방법과 새로운 방법, 그리고 제가 제시한 방법을 통해 코드 최적화를 진행하였고 그 결과를 분석하였습니다. 그리고 그 결과를 토대로 몇가지 인사이트를 얻었습니다.
결과에서 얻은 인사이트가 ES6의 tree-shaking 을 주목하고 있습니다. 따라서 향후에는 tree-shaking을 이용할 수 있는 형태로 라이브러리를 포팅하는 방법을 도입한다면 논문에서 나타났던 49%의 크기 감소를 얻을 수 있을 것으로 기대할 수 있습니다. 현재 레거시 포맷인 CommonJS는 자바스크립트 진영에서 점점 비중이 줄어들고 ES6 모듈 포맷은 점점 비중이 늘어나고 있습니다. 그 이유는 ES6 모듈 포맷이 자바스크립트 표준 명세이기 때문에 대부분의 라이브러리가 포맷을 맞춰가고 있기 때문입니다. 따라서 앞으로는 코드 최적화의 이점을 더 크게 얻을 수 있다고 기대할 수 있습니다.
[1]: Duck Typing. Available: Wikipedia.
[2] C. Park and S. Ryu, "Scalable and precise static analysis of JavaScript applications via loop-sensitivity," Proc. of the European Conference on Object-Oriented Programming, 2015.
[3]: E. Andreasen and A. Moller, "Determinacy in static analysis for jQuery," Proc. of the ACM International Conference on Object Oriented Programming Systems Languages and Applications, 2014.
[4]: M. Sridharan, J. Dolby, S. Chandra, M. Schafer, and F. Tip, "Correlation tracking for points-to analysis of JavaScript," Proc. of the European Conference on Object-Oriented Programming, 2012.
[5]: A. Feldthaus, M. Schafer, M. Sridharan, J. Dolby, and F. Tip, "Efficient construction of approximate call graphs for JavaScript IDE services," Proc. of the International Conference on Software Engineering, 2013.
[6]: Object Management Group, "Architecture-driven Modernization: Abstract Syntax Tree Metamodel (ASTM)", 2011. Available: http://www.omg.org/spec/ASTM
[7]: ECMA-International,"Standard ECMA-262", 2016. Available: http://www.ecma-international.org/publications/standards/Ecma-262.htm
[8]: E. Wittern, P. Suter and S. Rajagopalan, "A Look at the Dynamics of the JavaScript Package Ecosystem", IBM T. J. Watson Research Center, 2016.
[9]: M. Madsen, B. Livshits and M. Fanning, "Practical Static Analysis of JavaScript Applications in the Presence of Frameworks and Libraries", Microsoft Research Technical Report, pages 499-509, 2015.
[10]: I.O. Zolotareva, O.O. Knyga Simon, "Modern JavaScript Project Optimizers", Системи Обробки Інформації, pages 60-63, 2014.
[11]: UglifyJS2, "JavaScript parser, minifier, compressor or beautifier toolkit", Available: https://github.com/mishoo/UglifyJS2
[12]: RollUp, "the next-generation JavaScript module bundler", Available: https://github.com/rollup/rollup