Quantcast GDPR 솔루션 'choice'와 ember.js 충돌

안녕하세요,
이것은 상당히 구체적인 버그이지만, 그 영향은 이 특정 사례를 훨씬 넘어설 수 있을 것 같아 관련해서 질문이 있습니다.

(또한, 제 영어가 어색한 점 양해 부탁드립니다. 저는 프랑스인이라 영어를 모국어로 사용하지 않습니다…)

하지만 먼저, 맥락을 설명드리겠습니다.
저는 라즈베리 파이(Raspberry Pi)에 관한 프랑스어 포럼(forum.raspberry-pi.fr)을 위해 꽤 오랫동안 Discourse를 사용해 왔습니다. 이 포럼은 광고 관리(Themoneytizer)를 사용합니다. 아마 아시다시피, 유럽에서는 사용자 개인정보 보호를 위해 RGPD(일반 데이터 보호 규정)를 이행하도록 의무화하고 있습니다. RGPD 동의 관리의 주요 플레이어(최소 프랑스에서는)는 Quantcast와 그들의 “Choice” 솔루션입니다.

그래서 저는 꽤 오랫동안 Quantcast 솔루션을 사용해 왔는데, 최근에 “모두 허용” 버튼이 더 이상 제대로 작동하지 않는 것을 발견했습니다. 클릭해도 아무 반응이 없으며, 개발자 콘솔을 확인해 보니 "Uncaught TypeError: can’t define property “status”: Function is not extensible"라는 오류가 발생했습니다.

무슨 일이 일어나고 있는지(적어도 제 이해 범위 내에서):
이 문제의 원인을 찾는 데 정말로 아주 오랜 시간이 걸렸다는 점을 믿어 주십시오. apparently Ember.js(저는 이 기술에 매우 익숙하지 않습니다)는 Array, String, Function과 같은 자바스크립트 네이티브 객체를 확장합니다. 그리고 어떤 이유에서인지, Ember.js는 이러한 객체의 확장을 어떤 방식으로든 방지하는 것처럼 보입니다(이 부분은 아직 완전히 이해하지 못했습니다).

Quantcast의 솔루션은 그 자체로, 아마도 FunctionAcceptAll 함수에서(이것이 버그가 ‘모두 허용’ 버튼과 ‘모두 거부’ 버튼을 클릭할 때만 발생하는 이유를 설명합니다), Ember.js에 의해 정상 동작이 수정된 객체(아마도 배열)를 확장하려고 시도하는 것 같습니다.

이 버그를 이해하기 위해 많은 연구를 한 끝에, Ember.js의 동작을 수정하여 자바스크립트 프로토타입 확장을 비활성화할 수 있다는 것도 발견했습니다. 자세한 내용은 이 페이지 https://guides.emberjs.com/release/configuring-ember/disabling-prototype-extensions/ 에서 설명되어 있습니다.

몇 가지 테스트를 해 보았는데, window.EmberENV.FORCE_JQUERY = true; 줄 뒤에 window.EmberENV.EXTEND_PROTOTYPES = {String: true, Array: false}; 줄을 _ember_jquery-189e46ebcb33594b835e782fd1ce916ec750bc0cf980ebc4fb7796649161a18d.js 파일에 추가하면 버그가 사라지는 것을 확인했습니다.

시도해 보고자 하시는 분들을 위해, 포럼의 /tst/index.html 페이지를 확인해 보실 수 있습니다(스크립트가 시작되려면 유럽 IP가 필요할 수 있으며, 정확히는 알 수 없습니다).
이제 제가 드릴 수 있는 모든 정보를 드린 것 같습니다.

자, 이제 제 질문입니다.
이것이 상당히 구체적인 버그이긴 하지만, 현재 유럽에서는 RGPD가 점점 더 중요해지고 있으며, 이 추세는 더 쉬워지지 않을 것입니다.
Quantcast는 RGPD를 구현하는 데 수백 달러를 지불할 여유가 없는 플레이어들에게는 상당한 독점적 지위에 있습니다. 이 버그는 Quantcast의 사용을, 그리고 유럽에서 Discourse의 광고 사용을 완전히 불가능하게 만듭니다. 이것은 저에게 큰 문제로 보입니다.
또한, 제가 Quantcast를 통해만 버그를 발견했지만, 광고나 기타 목적으로 임베드해야 하지만 우리가 전혀 제어할 수 없으며, Array, String, Function 객체의 자바스크립트 “정상” 동작에 의존하는 많은 서드파티 스크립트에서 이러한 유형의 버그가 실제로 발생할 수 있습니다.

저는 Discourse 코드를 충분히 알지 못하므로 질문드립니다. Ember.js가 Array, String, Function 객체에 추가하는 속성들(앞서 링크한 페이지 참고)이 Discourse에서 사용되고 있지 않나요? 만약 그렇지 않다면, 이러한 부작용을 방지하기 위해 Ember.js의 프로토타입 확장을 비활성화하는 것을 고려해야 하지 않을까요?

누군가 이 문제에 대해 정보를 주실 수 있기를 바랍니다.
감사합니다

현재 이 기능은 지원되지 않으며, 확장(extension)에 의존하고 있습니다. 향후 버전에서는 이 의존성이 제거될 수도 있습니다. @eviltrout 님이 더 자세한 맥락을 제공해 드릴 수 있습니다.

여기서 어떻게 해야 할지 명확하지 않지만, 어떤 종류의 우회 방법(workaround)이 필요하다고 생각합니다. RGPD가 깨지는 것이 놀랍습니다. quantcast에 티켓을 열어 논의하고 그 링크를 여기에 공유하는 것이 좋겠습니다.

Quantcast에 필요한 스크립트를 어떻게 추가하고 계신가요? 아래처럼 src(외부)가 포함된 스크립트 태그를 통해 추가하고 계신가요?

<script src="foo"></script>

아니면 아래처럼 인라인으로 추가하고 계신가요?

<script>
  alert("Hello World!");
</script>

안녕하세요,
답변 주셔서 감사합니다.

@sam 이 주제를 만든 날 Quantcast에 티켓을 제출했고, 오늘 답변을 받았습니다. 현재 문제를 확인 중이라고 하며, hopefully 수정 방법을 찾을 수 있기를 바랍니다. “선택 수락” 버튼이 정상적으로 작동하므로, 이들에게는 비교적 쉬운 수정이 될 것이라 생각합니다. Discourse와 ember.js가 충분히 많이 사용되어 전용 수정이 필요한 솔루션으로 인정받기를 바랍니다.

@Johani 저는 실제로 Quantcast 스크립트를 직접 추가하지 않습니다. 머니타이저(Monetizer)가 스크립트를 제공하며, 스크립트 추가와 tcf2 이벤트 탐색 등을 대신 처리해 줍니다. 원하시면 이 스크립트를 여기에서 확인해 보실 수 있습니다.

이 스크립트는 페이지의 첫 번째 스크립트 앞에 새로운 요소를 추가하는 것으로 보입니다. 확실하지는 않지만, Quantcast 문서 예제에서 정확히 이 스크립트를 본 기억이 강하게 남아 있어, Quantcast Choice 통합을 위한 상당히 일반적으로 사용되는 스크립트라고 추정합니다.

다시 한번 시간 내어 주셔서 감사합니다.

우리는 가까운 시일 내에 배열 프로토타입을 안전하게 제거할 수 없을 것 같습니다. 그동안 이 프로토타입들은 무해했으며 많은 편의성을 제공해 왔습니다.

Quantcast에 연락하여 배열 프로토타입에 의존하지 않는 스크립트 버전을 받아보시는 것은 어떨까요?

여러분 안녕하세요,
새로운 정보를 가지고 왔고, 이 문제에 대한 해결책도 찾았습니다.

오늘 이 버그를 다시 파고들었고, 더 많은 정보를 찾은 것 같습니다.

Quantcast의 cmp2ui-fr.js 파일을 살펴보니 버그가 발생하는 위치를 찾을 수 있었습니다. 해당 함수는 다음과 같습니다(현재는 minified 버전만 존재함):

function(t){for(var n in t){t[n].status=e;}}

보시는 바와 같이, 이 함수는 for..in을 사용하며, t 변수는 배열입니다. 이전에 설명드린 바와 같이, ember.js는 JavaScript의 기본 Array를 확장합니다. 그런데 수정 사항 중 하나가 _super 항목을 추가한 것입니다.

_super 항목은 ROOT() 함수를 가리키는데, 이는 ember.js의 _utils.ROOT를 참조하는 것으로 보입니다. (여러분 중 일부에게는 익숙하게 들릴 수 있지만, 저에게는 그렇지 않네요 ^^). 이 ROOT() 함수는 확장할 수 없습니다.

명백히 이 _super 속성은 enumerable(열거 가능)로 취급되는 것 같습니다. 이는 배열에 대해 for..in 루프를 사용할 때, _super 항목이 “일반적인” 항목으로 취급된다는 뜻입니다. (예를 들어 values, bind, valueOf 등 배열 객체의 모든 함수들과는 달리).

저는 이것이 ember.js의 버그이며, 의도된 동작이 아니라고 생각합니다.

이로써 버그를 매우 재현하기 쉽게 만들었습니다. 방법은 다음과 같습니다. 먼저 객체 배열을 생성합니다:

var objs = [{'key':'val'},{'key':'val'}];

그 다음, strict 모드를 사용하고 배열을 반복하면서 각 항목에 새로운 속성을 설정하는 함수를 생성합니다:

var tst_func = function (objs){'use strict';for(var i in objs){objs[i].newproperty = true }};

마지막으로, 이 함수를 객체 배열에 대해 호출합니다:

tst_func(objs);

Uncaught TypeError: can't define property "newproperty": Function is not extensible라는 오류가 발생해야 합니다.

이것을 보니 버그가 이미 널리 퍼져 있을 가능성이 매우 높고, Quantcast에 국한된 문제가 아니라는 생각이 듭니다. 기본적으로 for..in을 사용하려는 누구든 일관되지 않은 동작이나 치명적인 버그에 노출됩니다.

저에게는 이것이 Discourse나 Quantcast의 문제가 아니라 명확히 ember.js의 문제입니다. 그러나 ember.js에서 수정될 때까지 우리는 해결책을 찾아야 한다는 사실은 변하지 않습니다. ^^.

좋은 소식은, 이 문제를 수정할 방법을 찾은 것 같다는 점입니다.

한 가지 방법은 모든 for..in 루프(아마도 each, for of 등도 포함)에 if (!objs.hasOwnProperty(i)) {continue};라는 줄을 사용하는 것입니다. 이는 Array의 _super라는 이상한 동작을 수정하지는 않지만, 로컬에서 해당 항목에 접근하는 것을 방지합니다. 이는 당연히 우리가 제어할 수 없는 외부 스크립트, 예를 들어 제 특정 Quantcast 사용 사례에서는 작동하지 않습니다.

또 다른 방법, 즉 제가 취해야 할 길이라고 생각하는 방법은 JavaScript의 Array 프로토타입을 수정하여(기본적으로 ember.js 자체의 오버라이드를 오버라이드하는 셈이죠 ^^) _super를 열거 불가능(not enumerable)하게 만드는 것입니다. 이를要做到 하려면, ember.js가 호출되고 평가된 후에 이 JS 줄을 실행해야 합니다:

//Make _super not enumerable to prevent bug between emberjs and for..in
Object.defineProperty(Array.prototype, '_super', {'enumerable': false});

두 번째 방법은 _super의 직접적인 사용을 의도대로 허용하면서 루프에서 나타나는 것을 방지합니다. 다만, 이 이상한 동작이 ember.js 내부 함수나 외부 플러그인에서 사용되고 있는지 보장할 수는 없습니다.

_ember_jquery-189e46ebcb33594b835e782fd1ce916ec750bc0cf980ebc4fb7796649161a18d.js 파일을 살펴보니, Discourse에 특화된 몇몇 줄을 발견했습니다. 예를 들어:

var ALIASES = {
    "ember-addons/ember-computed-decorators":
      "discourse-common/utils/decorators",
    "discourse/lib/raw-templates": "discourse-common/lib/raw-templates",
    "preload-store": "discourse/lib/preload-store",
    "fixtures/user_fixtures": "discourse/tests/fixtures/user-fixtures",
  };
  var ALIAS_PREPEND = {
    fixtures: "discourse/tests/",
    helpers: "discourse/tests/",
  };

여기에 우리의 Object.defineProperty(Array.prototype, '_super', {'enumerable': false}); 줄을 추가할 수 있을까요?

저는 현재 Quantcast choice.js 스크립트를 호출하기 전에 다음 줄을 추가하여 버그를 수정했습니다:
//Make _super not enumerable to prevent bug between emberjs and for..in
Object.defineProperty(Array.prototype, ‘_super’, {‘enumerable’: false});

버그를 로컬에서 수정하려면, 이 두 줄을 가진 ‘fix_ember.js’ 파일을 생성하고, 리버스 프록시(예: Nginx)에서 정적으로 서빙하며, 커스터마이징을 사용하여 테마 푸터에 <script src="/fix_emmber.js"></script> 줄을 추가할 수 있다고 생각합니다. 링크를 생성하는 대신 스크립트를 직접 작성하는 것은 Discourse의 스크립트 추출 기능(Custom javascript in <head> disappear 참조) 때문에 작동하지 않습니다.

이 토픽이 다른 사람들에게 도움이 되기를 바랍니다. 내일 ember.js에 티켓을 열어 이것이 버그인지, 아니면 매우 이상하지만 의도된 동작인지 확인해 볼 것입니다.

Discourse에 수정 사항을 포함해야 하는지 확인하기 위해 정보를 계속 공유하겠습니다.

PS: Extending JavaScript Natives – JavaScript, JavaScript… 의 Angus Croll에게 많은 감사를 드립니다. 그의 포스트가 큰 도움이 되었습니다!

이 문제를 알아봐 주셔서 감사합니다, @OsaAjani. 저희도 Discourse 포럼에서 Quantcast choice와 정확히 동일한 문제를 겪고 있으며, 현재 오류가 발생하지 않는 마지막 버전인 12에 머물러 있습니다.

최신 버전은 23이며, 최신 버전을 사용하길 매우 희망합니다.

해결 방법을 조속히 알려주시면 감사하겠습니다.

안녕하세요 @Terrapop. 이 주제가 도움이 되길 바랍니다. 현재 제가 ember.js 저장소([Bug] Property "_super" is enumerable on Array, creating conflicts with external libs (ex : Quantcast Choice RGPD) · Issue #19289 · emberjs/ember.js · GitHub)에 등록한 이슈에 대해 전혀 피드백을 받지 못하고 있습니다. Quantcast 측에서도 역시 아무런 회신이 없습니다.

다만 좋은 소식은, 이전 메시지에서 설명드린 수정 방법을 적용해 fix_ember.js라는 파일에 아래 코드를 작성한 것입니다:

Object.defineProperty(Array.prototype, '_super', {'enumerable': false});

그리고 다음 줄을 추가했습니다:

<script defer src="/fix_emmber.js"></script>

관리 패널의 커스텀 기능을 이용해 테마 푸터에 추가했습니다.

이 해결책으로 문제가 해결되었으며, 지금까지 부수적인 효과(side effects)는 발견되지 않았습니다. 따라서 이 수정 사항을 자신 있게 적용하시고 Quantcast v23으로 마이그레이션하셔도 될 것 같습니다.

choice.js를 로드하기 전에 해당 줄을 추가했습니다. 이 방법도 잘 작동하는 것 같습니다.

다른 해결책에 대해서는: 저희 Discourse 앞에 리버스 프록시가 없기 때문에 fix_ember.js 파일을 적절한 방식으로 생성하는 방법을 잘 모르겠습니다. Docker 내에서 이것이 가능한가요? 조언이 있으신가요?

글쎄요, choice.js보다 먼저 로드할 수 있다면 더 좋습니다!

네, 작동합니다. 정말 감사드립니다. 문제를 찾지 못해 며칠 동안 머리를 쥐어짰거든요. Quantcast와 20통의 이메일을 주고받았는데, 그쪽에서도 문제를 찾지 못했습니다.

글쎄, 만약 Quantcast와 원활한 소통 채널이 있다면, 이 게시글을 그들에게 알려서 문제를 해결하도록 유도할 수 있을 겁니다. 저는 이미 문의를 했지만, 실제로 그들의 고객이 아니어서 지원팀으로부터 별다른 답변을 받지 못했습니다.

알겠습니다. 좋은 지적이네요.