가상 DOM을 패치하여 이 문제를 해결하는 데 무언가 잘못되었거나 어려운 점이 있는 건가요?
PR은 환영합니다. 저희도 시도해 보았지만, 단순히 너무 어려웠습니다.
매우 흥미롭네요! vdom의 포크로 전환하는 것은 이상적이지 않지만, 회귀가 없다는 것을 입증할 수 있고(그리고 해당 코드를 우리 자신의 GitHub 조직으로 옮길 수 있다면) 고려해 볼 수 있을 것 같습니다.
vdom 테스트 스위트를 실행하여 회귀가 없는지 확인해 보셨나요? 이 prepend 동작에 대한 새로운 vdom 테스트를 추가하는 것이 얼마나 쉬울까요?
먼저 두 번째 질문에 답변드리겠습니다. 이 prepend 동작은 본질적으로 동일하며, 단순히 일부 불필요한 작업을 제거할 뿐입니다. vdom이 패치 전후로 각각 어떤 동작을 하는지 보여드리겠습니다.
20개의 게시물이 있고, 스크롤 시 상단에 10개의 게시물이 추가로 로드된다고 가정해 보겠습니다.
원래 vdom은 다음과 같은 변환 순서를 따릅니다:
[20개의 기존] → [20개의 기존, 10개의 새] // 10개의 새 요소가 끝에 추가됨
[20개의 기존, 10개의 새] → // 30개 요소가 모두 제거됨
→ [10개의 새, 20개의 기존] // 30개 요소가 모두 해당 위치에 삽입됨
패치 적용 후 변환 순서는 다음과 같이 변합니다:
[20개의 기존] → [20개의 기존, 10개의 새] // 10개의 새 요소가 끝에 추가됨
[20개의 기존, 10개의 새] → [20개의 기존] // 10개의 새 요소가 끝에서 제거됨
[20개의 기존] → [10개의 새, 20개의 기존] // 10개의 새 요소가 앞에 추가됨
보시는 바와 같이 여전히 불필요한 작업이 남아 있습니다. 원칙적으로는 해당 요소를 즉시 prepend할 수 있지만, 이렇게 하면 한 곳에서만 코드를 추가하면 되며 기존에 있는 것을 다시 작성할 필요가 없습니다.
각 변환은 DOM 조작이 되므로, 원래는 기존 요소가 다시 삽입되어(재렌더링이 트리거됨) 유튜브 영상이 재생되기 시작했지만, 패치 적용 후에는 해당 요소가 제자리에 그대로 유지됩니다.
다음은 virtual-dom의 package.json 발췌본입니다.
"scripts": {
"test": "node ./test/index.js | tap-spec",
"dot": "node ./test/index.js | tap-dot",
"start": "node ./index.js",
"cover": "istanbul cover --report html --print detail ./test/index.js",
"view-cover": "istanbul report html && opn ./coverage/index.html",
"browser": "run-browser test/index.js",
"phantom": "run-browser test/index.js -b | tap-spec",
"dist": "browserify --standalone virtual-dom index.js > dist/virtual-dom.js",
"travis-test": "npm run phantom && npm run cover && istanbul report lcov && ((cat coverage/lcov.info | coveralls) || exit 0)",
"release": "npm run release-patch",
"release-patch": "git checkout master && npm version patch && git push origin master --tags && npm publish",
"release-minor": "git checkout master && npm version minor && git push origin master --tags && npm publish",
"release-major": "git checkout master && npm version major && git push origin master --tags && npm publish"
},
이 중 ‘test’, ‘dot’, ‘cover’, ‘view-cover’, ‘browser’, ‘phantom’, 'travis-test’는 테스트와 관련이 있어 보입니다.
‘browser’, ‘phantom’, 'travis-test’는 제 코드에 포함된 새로운 JavaScript 문법 때문에 파싱 오류가 발생합니다. 나머지는 통과합니다. 이 코드
var prepend = simulate.every(item => item && item.key)
prepend &&= aChildren.every((item, i) => item.key === bChildren[i + shift].key)
을 이 코드로 변경하면
var prepend = true
for (var i = 0; prepend && i < simulate.length; i++) {
prepend = simulate[i] && simulate[i].key
}
for (var i = 0; prepend && i < aChildren.length; i++) {
prepend = aChildren[i].key === bChildren[i + shift].key
}
모두 통과합니다. 이러한 스크립트를 모두 만족시키는 것이 바람직하다면, JavaScript를 일관되게 구식 문법으로 유지하기 위해 이 변경 사항을 push할 수 있습니다.
네, 그렇게 해주세요 - 기존 테스트 스위트가 계속 작동하도록 유지합시다
virtual-dom을 https://github.com/discourse/virtual-dom으로 포크했습니다. 해당 저장소에 대해 PR을 만들어 주시겠습니까?
또한, 제가 virtual-dom에 적용한 변경 사항은 매우 제한적입니다. 구체적으로 prepends를 대상으로 하며, 그 외 모든 경우에서는 원래 알고리즘으로 되돌립니다. 그리고 그 원래 알고리즘은 문제를 일반적으로 살펴보면 여전히 완벽하지 않습니다(불필요하게 기존 요소에 영향을 미치는 예시를 만드는 것은 어렵지 않습니다). 반면, appends, removes, 단일 inserts는 잘 처리합니다. 그리고 이 상태에서 post stream을 상당히 복잡하게 만들어야만 이 문제를 벗어날 수 있습니다. 따라서 실용적인 관점에서 보면 문제를 일반적으로 해결하는 것은 다소 과할 수 있지만, 물론 그렇게 하면 더 안심할 수 있습니다.
새로운 테스트를 몇 개 커밋했습니다. PR 리뷰를 곧 진행할 계획이 있으신가요?
알렉세이, 감사합니다. 다음 주 안에 검토를 진행해 보겠습니다.
virtualdom 포크를 설정했고, 수동 테스트에서 iframe 문제를 해결하는 것을 확인했습니다.
@Aleksey_Bogdanov 한 가지 도움을 요청할 수 있을까요? Discourse 내에서 테스트를 추가하려고 시도하고 있습니다. 구체적으로:
-
<span>ElementOne</span><span>ElementTwo</span>을 렌더링합니다. -
ElementTwo를 제거하고
PrependedElement를 맨 앞에 추가합니다. -
결과가
<span>PrependedElement</span><span>ElementOne</span>인지 확인합니다. -
원래의 ElementOne 스팬이 최종 ElementOne 스팬과 동일한지 확인합니다. (즉, 다시 렌더링되지 않았는지 확인)
불행히도 (4)의 확인이 실패하여 ElementOne이 다시 렌더링된 것으로 나타났습니다. 이 경우 새로운 로직이 작동하지 않는 이유가 무엇일까요? ![]()
새로운 포크 설정과 실패하는 테스트를 기존 PR에 푸시했습니다.
네, 이전에 이 문제에 대해 글을 쓴 적이 있습니다.
이것이 일반적으로 작동할 것이라 기대하지는 않습니다(그리고 당신이 설명한 테스트가 통과할 것이라고도요). 원래는 추가(append), 제거(removal), 단일 삽입(단일 삽입의 조합일 수도 있음)에 대해 작동합니다. 제 변경 사항으로 인해 순수한 삽입(prepend)에도 작동하게 되었는데, 이는 유튜브 버그를 커버해야 합니다. 당신의 테스트에는 '제거 + 삽입’이 포함되어 있습니다. 이는 제 변경 사항으로 커버되지 않습니다.
더 견고한 솔루션은 적어도 virtual-dom의 'reorder’와 'diffChildren’에 상당한 재작성이 필요합니다. 저는 시도해 볼 수 있지만, 이는 장기적으로 virtual-dom의 자체 포크를 유지할 계획이 있는지에 대한 질문을 제기합니다. 견고함을 추구한다면, 유사한 활성 유지 관리 라이브러리로 전환하는 것이 더 나은 아이디어일 수 있으며, 시간과 노력을 덜 들일 수 있습니다. 제 추측으로는 다른 라이브러리들이 이미 이 문제를 해결했을 것입니다.
아, 그렇군요. 이해가 됩니다. 하지만 '순수한 prepend’로 변경해도 동일한 실패가 발생합니다. 관련 reorder() 함수에 브레이크포인트를 설정해 보니 bFree.length === bChildren.length 체크에서 오류가 나고 있습니다:
업데이트된 테스트를 해당 브랜치에 푸시했습니다.
이것은 확실히 단기적인 해결책입니다. 우리는 이미 vdom 사용을 Ember/Glimmer 렌더링으로 대체하기 시작했습니다. 우리의 목표는 향후 12개월 이내에 포스트 스트림 구현을 대체하는 것입니다. Glimmer는 이러한 종류의 'prepend’를 올바르게 처리합니다.
따라서 ‘불완전하지만 이전보다 나은’ 변경 사항을 병합하는 데 전혀 문제없습니다. 하지만, 너무 큰 부담이 아니라면 왜 이 테스트가 작동하지 않는지 이해할 수 있으면 좋겠습니다 ![]()
물론이죠. 아직 정확히 모르겠지만, 최대한 빨리 확인해 보겠습니다.
업데이트 내용입니다.
테스트가 통과하도록 키를 추가했습니다. 키는 virtual-dom이 어떤 요소가 어떤 것에 해당하는지 알려줍니다. 키가 없으면 virtual-dom은 요소들이 이전과 같은 순서를 따르는 것으로 가정하며, 앞에 추가된 요소인지를 파악할 수 없습니다.
그 후, 단일 prepend를 여러 개의 prepend로 변경했습니다. virtual-dom은 이미 단일 삽입(그리고 단일 prepend는 특수한 경우)을 처리하고 있기 때문에, 이로 인해 차이가 발생하지 않아야 합니다.
위에서 설명한 변경 사항이 지금까지 제가 push한 내용입니다.
하지만 지금은 여러 개의 prepend를 사용해도 차이가 발생하지 않아서 놀랍습니다. 제 초기 가설, 즉 무엇이 문제인지 그리고 제 패치가 왜 작동하는지에 대한 추측이 틀렸을 수도 있습니다. 그래서 아직 원인을 파악하고 있습니다.
David, 테스트를 브라우저에서 디버깅하셨다는 점을 참고했습니다. 설정이 어려운가요? 테스트용 디버거를 설정하는 데 어려움을 겪고 있는데, 몇 가지 조언을 주시면 감사하겠습니다.
아, 이제 이해했습니다. 알려주셔서 감사합니다!
작동하는 Discourse 개발 환경이 있나요? 있다면, 환경을 실행한 후 브라우저에서 /tests로 이동하고, 상단의 필터 UI를 사용하여 "avoids rerendering on prepend"를 검색하세요. 그런 다음 브라우저 개발자 도구를 사용해 디버깅할 수 있습니다. (예: 소스(Sources) 탭으로 이동, ctrl+p를 눌러 파일 열기, vdom/diff 검색, 그리고 브레이크포인트 설정)
감사합니다, 시도해 보겠습니다.
그러니까, 이 코드
assert.strictEqual(elementOneBefore, elementOneAfter);
은 elementOneBefore가 어떤 변경 사항이 있든 항상 그 동일성(identity)을 유지하기 때문에 차이를 만들지 않습니다.
직접 확인해 보시려면 아래에 작은 예시를 하나 보여 드리겠습니다:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Youtube Bug Demo</title>
</head>
<body>
<script>
let iframe = document.createElement("iframe");
iframe.width = 690;
iframe.height = 388;
iframe.src = "https://www.youtube.com/embed/Xc5rB-0ZBcI";
iframe.title = "Strange S.T.A.L.K.E.R. car glitch";
document.body.appendChild(iframe);
let button = document.createElement("button");
button.type = "button";
button.innerHTML = "Reinsert";
button.onclick = function(){
let iframeStart = document.querySelector("iframe");
document.body.removeChild(iframeStart);
document.body.insertBefore(iframeStart, button);
let iframeEnd = document.querySelector("iframe");
alert(iframeStart === iframeEnd);
};
document.body.appendChild(button);
</script>
</body>
</html>
실제로 차이를 만드는 것은 'removeChild’와 ‘insertBefore’ 호출입니다. 그래서 DOM 변경 사항(mutation)에 대한 검사를 추가했습니다. 이제 이전 버전에서는 테스트가 실패하고 현재 버전에서는 통과하므로, 이것이 충분한 해결책이 되기를 바랍니다.
정말 감사합니다. @Aleksey_Bogdanov 님의 노고에 깊이 감사드립니다. 방금 PR을 병합했는데, 1시간 이내에 Meta에 반영될 예정입니다.
