클로저 컴파일러에서 적용하는 제한사항 이해하기

클로저 컴파일러에서는 자바스크립트 입력이 몇 가지 제한사항을 준수할 것으로 예상합니다. 컴파일러에 실행하도록 요청하는 최적화 수준이 높을수록 컴파일러가 입력 자바스크립트에 더 많은 제한을 적용합니다.

이 문서에서는 각 최적화 수준의 주요 제한사항을 설명합니다. 컴파일러가 추가로 가정한 경우 이 위키 페이지를 참조하세요.

모든 최적화 수준에 적용되는 제한사항

컴파일러는 모든 최적화 수준에서 처리하는 모든 자바스크립트에 다음과 같은 두 가지 제한사항을 둡니다.

  • 컴파일러는 ECMAScript만 인식합니다.

    ECMAScript 5는 거의 모든 곳에서 지원되는 자바스크립트 버전입니다. 그러나 컴파일러는 ECMAScript 6에서 많은 기능을 지원합니다. 컴파일러는 공식 언어 기능만 지원합니다.

    적절한 ECMAScript 언어 사양을 준수하는 브라우저 관련 기능은 컴파일러에서 잘 작동합니다. 예를 들어 ActiveX 객체는 법적 자바스크립트 구문으로 생성되므로, ActiveX 객체를 생성하는 코드가 컴파일러와 연동됩니다.

    컴파일러 유지관리 담당자는 새로운 언어 버전과 기능을 지원하기 위해 적극적으로 노력하고 있습니다. 프로젝트에서는 --language_in 플래그를 사용하여 원하는 ECMAScript 언어 버전을 지정할 수 있습니다.

  • 컴파일러는 주석을 보존하지 않습니다.

    모든 컴파일러 최적화 수준은 주석을 삭제하므로 특수 형식의 주석을 사용하는 코드는 컴파일러와 호환되지 않습니다.

    예를 들어 컴파일러는 주석을 보존하지 않으므로 JScript의 '조건부 주석'을 직접 사용할 수 없습니다. 그러나 eval() 표현식에서 조건부 주석을 래핑하여 이 제한을 해결할 수 있습니다. 컴파일러는 오류를 생성하지 않고 다음 코드를 처리할 수 있습니다.

     x = eval("/*@cc_on 2+@*/ 0");
    

    참고: @preserve 주석을 사용하여 컴파일러 출력 상단에 오픈소스 라이선스 및 기타 중요한 텍스트를 포함할 수 있습니다.

SIMPLE_OPTIMIZATIONS 제한

단순 최적화 수준은 함수 매개변수, 로컬 변수, 로컬에서 정의된 함수의 이름을 변경하여 코드 크기를 줄입니다. 그러나 일부 자바스크립트 구조에서는 이 이름 변경 프로세스를 중단할 수 있습니다.

SIMPLE_OPTIMIZATIONS를 사용할 때는 다음 구성과 관행을 피하세요.

  • with:

    with를 사용하면 컴파일러는 로컬 변수와 이름이 같은 객체 속성을 구분할 수 없으므로 이름의 모든 인스턴스 이름을 바꿉니다.

    또한 with 문을 사용하면 코드가 읽기 어려워집니다. with 문은 이름 확인의 일반 규칙을 변경하므로 코드를 나타내는 프로그래머도 이름이 무엇을 나타내는지 파악하기가 어려울 수 있습니다.

  • eval():

    컴파일러는 eval()의 문자열 인수를 파싱하지 않으므로 이 인수 내의 기호 이름을 변경하지 않습니다.

  • 함수 또는 매개변수 이름의 문자열 표현:

    컴파일러는 함수 및 함수 매개변수의 이름을 변경하지만 코드에서 함수 또는 매개변수를 참조하는 문자열을 변경하지 않습니다. 따라서 코드에서 함수 또는 매개변수 이름을 문자열로 표현해서는 안 됩니다. 예를 들어 프로토타입 라이브러리 함수 argumentNames()Function.toString()를 사용하여 함수의 매개변수 이름을 검색합니다. argumentNames()가 코드에서 인수 이름을 사용하고자 할 수도 있지만 단순 모드 컴파일에서는 이러한 종류의 참조를 중단합니다.

ADVANCED_OPTIMIZATIONS 제한

ADVANCED_OPTIMIZATIONS 컴파일 수준은 SIMPLE_OPTIMIZATIONS와 동일한 변환을 실행하며 속성, 변수, 함수의 전역 이름 변경, 불량 코드 제거, 속성 평면화도 추가합니다. 이러한 새 패스는 입력 자바스크립트에 추가 제한사항을 적용합니다. 일반적으로 자바스크립트의 동적 기능을 사용하면 코드에서 올바른 정적 분석이 이루어지지 않습니다.

전역 변수, 함수, 속성 이름 변경의 의미:

ADVANCED_OPTIMIZATIONS의 전체 이름 변경은 다음과 같은 방식이 위험합니다.

  • 선언되지 않은 외부 참조:

    전역 변수, 함수, 속성의 이름을 올바르게 바꾸려면 컴파일러가 이러한 전역 변수에 관한 모든 참조를 알아야 합니다. 컴파일 중인 코드 외부에 정의된 기호에 관해 컴파일러에 알려야 합니다. 고급 컴파일 및 익스텐션에서는 외부 기호를 선언하는 방법을 설명합니다.

  • 외부 코드에서 내보내지 않은 내부 이름 사용:

    컴파일된 코드는 컴파일되지 않은 코드가 참조하는 기호를 내보내야 합니다. 고급 컴파일 및 익스텐션에서는 기호를 내보내는 방법을 설명합니다.

  • 문자열 이름을 사용하여 객체 속성 참조:

    컴파일러는 고급 모드에서 속성 이름을 바꾸지만 문자열 이름은 변경하지 않습니다.

      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 내의 thisfoo를 참조합니다. 변환 후 this는 전역 this를 나타냅니다. 이와 같은 경우 컴파일러는 다음 경고를 생성합니다.

    "WARNING - dangerous use of this in static method foo.bar"

    속성 평면화로 인해 this 참조가 중단되지 않도록 하려면 생성자 및 프로토타입 메서드 내에서만 this를 사용하세요. new의 생성자 또는 prototype의 속성인 함수 내에서 생성자를 호출할 때 this의 의미는 명확합니다.

  • 어떤 클래스에서 호출되는지 모르고 정적 메서드 사용:

    예를 들어 다음과 같습니다.

    class A { static create() { return new A(); }};
    class B { static create() { return new B(); }};
    let cls = someCondition ? A : B;
    cls.create();
    
    컴파일러가 두 create 메서드를 모두 축소하여 (ES6에서 ES5로 변환한 후) 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는 부작용이 없는 것으로 확인되어 삭제되었습니다.