Closure Compiler는 JavaScript 입력이 몇 가지 제한사항을 준수해야 합니다. 컴파일러에 요청하는 최적화 수준이 높을수록 컴파일러가 입력 JavaScript에 적용하는 제한사항이 많아집니다.
이 문서에서는 각 최적화 수준의 주요 제한사항을 설명합니다. 컴파일러에서 추가로 가정한 사항은 이 위키 페이지를 참고하세요.
모든 최적화 수준의 제한사항
컴파일러는 모든 최적화 수준에서 처리하는 모든 JavaScript에 다음 두 가지 제한사항을 적용합니다.
컴파일러는 ECMAScript만 인식합니다.
ECMAScript 5는 거의 모든 곳에서 지원되는 JavaScript 버전입니다. 하지만 컴파일러는 ECMAScript 6의 많은 기능도 지원합니다. 컴파일러는 공식 언어 기능만 지원합니다.
적절한 ECMAScript 언어 사양을 준수하는 브라우저별 기능은 컴파일러와 잘 작동합니다. 예를 들어 ActiveX 객체는 합법적인 JavaScript 문법으로 생성되므로 ActiveX 객체를 생성하는 코드는 컴파일러와 함께 작동합니다.
컴파일러 관리자는 새로운 언어 버전과 기능을 지원하기 위해 적극적으로 노력합니다. 프로젝트는
--language_in
플래그를 사용하여 원하는 ECMAScript 언어 버전을 지정할 수 있습니다.컴파일러는 주석을 보존하지 않습니다.
모든 컴파일러 최적화 수준에서 주석을 삭제하므로 특별히 서식이 지정된 주석을 사용하는 코드는 컴파일러에서 작동하지 않습니다.
예를 들어 컴파일러는 주석을 보존하지 않으므로 JScript의 '조건부 주석'을 직접 사용할 수 없습니다. 하지만 조건부 주석을
eval()
표현식으로 래핑하면 이 제한을 해결할 수 있습니다. 컴파일러는 오류를 생성하지 않고 다음 코드를 처리할 수 있습니다.x = eval("/*@cc_on 2+@*/ 0");
참고: @preserve 주석을 사용하여 컴파일러 출력 상단에 오픈소스 라이선스 및 기타 중요한 텍스트를 포함할 수 있습니다.
SIMPLE_OPTIMIZATIONS 제한사항
Simple 최적화 수준은 코드 크기를 줄이기 위해 함수 매개변수, 로컬 변수, 로컬로 정의된 함수의 이름을 바꿉니다. 하지만 일부 JavaScript 구조는 이 이름 바꾸기 프로세스를 중단할 수 있습니다.
SIMPLE_OPTIMIZATIONS
를 사용할 때는 다음 구조와 관행을 피하세요.
with
:with
를 사용하면 컴파일러가 이름이 같은 지역 변수와 객체 속성을 구분할 수 없으므로 이름의 모든 인스턴스 이름을 바꿉니다.또한
with
문장을 사용하면 사람이 코드를 읽기가 더 어려워집니다.with
문은 이름 확인의 일반 규칙을 변경하므로 코드를 작성한 프로그래머조차 이름이 무엇을 참조하는지 식별하기 어려울 수 있습니다.eval()
:컴파일러는
eval()
의 문자열 인수를 파싱하지 않으므로 이 인수 내의 기호 이름을 바꾸지 않습니다.함수 또는 매개변수 이름의 문자열 표현:
컴파일러는 함수와 함수 매개변수의 이름을 바꾸지만, 이름으로 함수나 매개변수를 참조하는 코드의 문자열은 변경하지 않습니다. 따라서 코드에서 함수 또는 매개변수 이름을 문자열로 표현하지 않아야 합니다. 예를 들어 Prototype 라이브러리 함수
argumentNames()
는Function.toString()
를 사용하여 함수의 매개변수 이름을 가져옵니다. 하지만argumentNames()
를 사용하면 코드에서 인수의 이름을 사용하고 싶어질 수 있지만 단순 모드 컴파일은 이러한 종류의 참조를 중단합니다.
ADVANCED_OPTIMIZATIONS 제한사항
ADVANCED_OPTIMIZATIONS
컴파일 수준은 SIMPLE_OPTIMIZATIONS
과 동일한 변환을 실행하며 속성, 변수, 함수의 전역 이름 바꾸기, 데드 코드 삭제, 속성 평탄화도 추가합니다. 이러한 새로운 패스는 입력 JavaScript에 추가 제한사항을 적용합니다. 일반적으로 JavaScript의 동적 기능을 사용하면 코드에 대한 올바른 정적 분석이 방지됩니다.
전역 변수, 함수, 속성 이름 변경의 영향:
ADVANCED_OPTIMIZATIONS
의 전역 이름 변경으로 인해 다음 관행이 위험해집니다.
선언되지 않은 외부 참조:
전역 변수, 함수, 속성의 이름을 올바르게 바꾸려면 컴파일러가 이러한 전역 변수에 대한 모든 참조를 알고 있어야 합니다. 컴파일되는 코드 외부에서 정의된 심볼에 관해 컴파일러에 알려야 합니다. 고급 컴파일 및 Externs에서는 외부 기호를 선언하는 방법을 설명합니다.
내보내지 않은 내부 이름을 외부 코드에서 사용:
컴파일된 코드는 컴파일되지 않은 코드에서 참조하는 모든 심볼을 내보내야 합니다. 고급 컴파일 및 Externs에서는 심볼을 내보내는 방법을 설명합니다.
문자열 이름을 사용하여 객체 속성 참조:
컴파일러는 고급 모드에서 속성의 이름을 바꾸지만 문자열의 이름은 바꾸지 않습니다.
var x = { renamed_property: 1 }; var y = x.renamed_property; // This is OK. // 'renamed_property' below doesn't exist on x after renaming, so the // following evaluates to false. if ( 'renamed_property' in x ) {}; // BAD // The following also fails: x['renamed_property']; // BAD
따옴표로 묶인 문자열이 있는 속성을 참조해야 하는 경우 항상 따옴표로 묶인 문자열을 사용하세요.
var x = { 'unrenamed_property': 1 }; x['unrenamed_property']; // This is OK. if ( 'unrenamed_property' in x ) {}; // This is OK
변수를 전역 객체의 속성으로 참조:
컴파일러는 속성과 변수의 이름을 독립적으로 바꿉니다. 예를 들어 컴파일러는 다음 두
foo
참조가 동일하더라도 다르게 취급합니다.var foo = {}; window.foo; // BAD
이 코드는 다음과 같이 컴파일될 수 있습니다.
var a = {}; window.b;
전역 객체의 속성으로 변수를 참조해야 하는 경우 항상 다음과 같이 참조하세요.
window.foo = {} window.foo;
Implications of dead code elimination
The ADVANCED_OPTIMIZATIONS
compilation level removes code
that is never executed. This elimination of dead code makes the
following practices dangerous:
Calling functions from outside of compiled code:
When you compile functions without compiling the code that calls those functions, the Compiler assumes that the functions are never called and removes them. To avoid unwanted code removal, either:
- compile all the JavaScript for your application together, or
- export compiled functions.
Advanced Compilation and Externs describes both of these approaches in greater detail.
Retrieving functions through iteration over constructor or prototype properties:
To determine whether a function is dead code, the Compiler has to find all the calls to that function. By iterating over the properties of a constructor or its prototype you can find and call methods, but the Compiler can't identify the specific functions called in this manner.
For example, the following code causes unintended code removal:
function Coordinate() { } Coordinate.prototype.initX = function() { this.x = 0; } Coordinate.prototype.initY = function() { this.y = 0; } var coord = new Coordinate(); for (method in Coordinate.prototype) { Coordinate.prototype[method].call(coord); // BAD }
컴파일러는
initX()
및initY()
이for
루프에서 호출된다는 것을 이해하지 못하므로 이러한 메서드를 모두 삭제합니다.함수를 매개변수로 전달하면 컴파일러가 해당 매개변수 호출을 찾을 수 있습니다. 예를 들어 컴파일러는 고급 모드에서 다음 코드를 컴파일할 때
getHello()
함수를 삭제하지 않습니다.function alertF(f) { alert(f()); } function getHello() { return 'hello'; } // The Compiler figures out that this call to alertF also calls getHello(). alertF(getHello); // This is OK.
객체 속성 평면화의 영향
고급 모드에서 컴파일러는 이름 단축을 준비하기 위해 객체 속성을 축소합니다. 예를 들어 컴파일러는 다음을 변환합니다.
var foo = {}; foo.bar = function (a) { alert(a) }; foo.bar("hello");
다음으로 변환합니다.
var foo$bar = function (a) { alert(a) }; foo$bar("hello");
이 속성 평면화를 통해 이후 이름 바꾸기 패스에서 더 효율적으로 이름을 바꿀 수 있습니다. 컴파일러는 foo$bar
를 단일 문자로 바꿀 수 있습니다.
하지만 속성 평면화로 인해 다음과 같은 방법도 위험해집니다.
생성자 및 프로토타입 메서드 외부에서
this
사용:속성 평면화는 함수 내에서
this
키워드의 의미를 변경할 수 있습니다. 예를 들면 다음과 같습니다.var foo = {}; foo.bar = function (a) { this.bad = a; }; // BAD foo.bar("hello");
변경사항:
var foo$bar = function (a) { this.bad = a; }; foo$bar("hello");
변환 전에는
foo.bar
내의this
가foo
를 참조합니다. 변환 후this
는 전역this
를 나타냅니다. 이러한 경우 컴파일러는 다음 경고를 생성합니다."WARNING - dangerous use of this in static method foo.bar"
속성 병합으로 인해
this
참조가 깨지지 않도록 생성자와 프로토타입 메서드 내에서만this
를 사용하세요.new
키워드로 생성자를 호출하거나prototype
의 속성인 함수 내에서this
의 의미는 명확합니다.호출되는 클래스를 알지 못한 채 정적 메서드 사용:
상황별로 확인해야 사항은 다음과 같습니다.
컴파일러는 ES6에서 ES5로 트랜스파일한 후 두class A { static create() { return new A(); }}; class B { static create() { return new B(); }}; let cls = someCondition ? A : B; cls.create();
create
메서드를 모두 축소하므로cls.create()
호출이 실패합니다.@nocollapse
주석을 사용하면 이 문제를 방지할 수 있습니다.class A { /** @nocollapse */ static create() { return new A(); } } class B { /** @nocollapse */ static create() { return new A(); } }
슈퍼클래스를 알지 못하는 정적 메서드에서 super 사용:
컴파일러는
super.sayHi()
가Parent.sayHi()
를 참조한다는 것을 알고 있으므로 다음 코드는 안전합니다.class Parent { static sayHi() { alert('Parent says hi'); } } class Child extends Parent { static sayHi() { super.sayHi(); } } Child.sayHi();
하지만
myMixin(Parent).sayHi
가 컴파일되지 않은Parent.sayHi
와 같더라도 속성 병합으로 인해 다음 코드가 중단됩니다.class Parent { static sayHi() { alert('Parent says hi'); } } class Child extends myMixin(Parent) { static sayHi() { super.sayHi(); } } Child.sayHi();
/** @nocollapse */
주석을 사용하여 이 중단을 방지하세요.Object.defineProperties 또는 ES6 getter/setter 사용:
컴파일러가 이러한 구조를 잘 이해하지 못합니다. ES6 getter 및 setter는 트랜스파일을 통해 Object.defineProperties(...)로 변환됩니다. 현재 컴파일러는 이 구조를 정적으로 분석할 수 없으며 속성 액세스 및 설정에 부작용이 없다고 가정합니다. 이로 인해 위험한 결과가 발생할 수 있습니다. 예를 들면 다음과 같습니다.
class C { static get someProperty() { console.log("hello getters!"); } } var unused = C.someProperty;
다음으로 컴파일됩니다.
C = function() {}; Object.defineProperties(C, {a: // Note someProperty is also renamed to 'a'. {configurable:!0, enumerable:!0, get:function() { console.log("hello world"); return 1; }}});
C.someProperty에 부작용이 없는 것으로 확인되어 삭제되었습니다.