Frankenstein 마이그레이션: 프레임워크 불가지론적 접근 방식(2부)

게시 됨: 2022-03-10
빠른 요약 ↬ 우리는 최근에 "Frankenstein 마이그레이션"이 무엇인지 논의하고 이를 기존 유형의 마이그레이션과 비교하고 마이크로서비스웹 구성요소 라는 두 가지 주요 구성 요소를 언급했습니다. 또한 이러한 유형의 마이그레이션이 작동하는 방식에 대한 이론적 기반도 얻었습니다. 해당 토론을 읽지 않았거나 잊어버린 경우 이 기사의 두 번째 부분에서 다룰 모든 내용을 이해하는 데 도움이 되기 때문에 먼저 Part 1로 돌아가고 싶을 것입니다.

이 기사에서는 이전 부분의 권장 사항에 따라 응용 프로그램의 단계별 마이그레이션을 수행하여 모든 이론을 테스트할 것입니다. 일을 간단하게 하고 불확실성, 미지수, 불필요한 추측을 줄이기 위해 마이그레이션의 실제 예를 위해 간단한 할 일 애플리케이션에서 실습을 시연하기로 결정했습니다.

이론을 시험할 시간이다
이론을 시험할 때입니다. (큰 미리보기)

일반적으로 일반 할일 애플리케이션의 작동 방식을 잘 이해하고 있다고 가정합니다. 이러한 유형의 응용 프로그램은 우리의 요구 사항에 매우 잘 맞습니다. 예측 가능하지만 Frankenstein 마이그레이션의 다양한 측면을 보여주기 위해 실행 가능한 최소한의 필수 구성 요소가 있습니다. 그러나 실제 애플리케이션의 크기와 복잡성에 관계 없이 접근 방식은 확장성이 뛰어나고 모든 규모의 프로젝트에 적합해야 합니다.

TodoMVC 애플리케이션의 기본 보기
TodoMVC 애플리케이션의 기본 보기(큰 미리보기)

이 기사의 출발점으로 TodoMVC 프로젝트에서 jQuery 애플리케이션을 선택했습니다. jQuery는 충분히 레거시이며 프로젝트의 실제 상황을 반영할 수 있으며 가장 중요한 것은 최신 동적 애플리케이션을 구동하기 위한 상당한 유지 관리 및 해킹이 필요합니다. (이는 더 유연한 것으로 마이그레이션을 고려하기에 충분해야 합니다.)

그러면 마이그레이션할 "더 유연한" 것은 무엇입니까? 실생활에서 유용한 매우 실용적인 사례를 보여주기 위해 저는 요즘 가장 인기 있는 두 프레임워크인 React와 Vue 중에서 선택해야 했습니다. 그러나 내가 무엇을 선택하든 우리는 다른 방향의 일부 측면을 놓칠 것입니다.

점프 후 더! 아래에서 계속 읽기 ↓

따라서 이 부분에서는 다음 두 가지를 모두 실행합니다.

  • jQuery 애플리케이션을 React 로 마이그레이션
  • jQuery 애플리케이션을 Vue 로 마이그레이션.
우리의 목표: React와 Vue로의 마이그레이션 결과
우리의 목표: React와 Vue로의 마이그레이션 결과. (큰 미리보기)

코드 저장소

여기에 언급된 모든 코드는 공개적으로 사용 가능하며 원할 때마다 액세스할 수 있습니다. 플레이할 수 있는 두 개의 저장소가 있습니다.

  • 프랑켄슈타인 토도MVC
    이 저장소에는 다른 프레임워크/라이브러리에 있는 TodoMVC 애플리케이션 이 포함되어 있습니다. 예를 들어, 이 저장소에서 vue , angularjs , reactjquery 와 같은 분기를 찾을 수 있습니다.
  • 프랑켄슈타인 데모
    여기에는 여러 분기가 포함되어 있으며 각 분기는 첫 번째 저장소에서 사용할 수 있는 응용 프로그램 간의 특정 마이그레이션 방향 을 나타냅니다. 특히 migration/jquery-to-reactmigration/jquery-to-vue 와 같은 분기가 있으며 나중에 다룰 것입니다.

두 리포지토리 모두 작업 진행 중이며 새로운 애플리케이션 및 마이그레이션 지침이 있는 새 분기를 정기적으로 추가해야 합니다. ( 당신도 자유롭게 기여할 수 있습니다! ) 마이그레이션 브랜치의 커밋 기록은 잘 구성되어 있으며 이 기사에서 다룰 수 있는 것보다 훨씬 더 자세한 내용이 포함된 추가 문서 역할을 할 수 있습니다.

이제 손을 더럽히자! 앞으로 갈 길이 멀기 때문에 순조로운 여정을 기대하지 마십시오. 이 문서를 따라가는 방법을 결정하는 것은 귀하에게 달려 있지만 다음을 수행할 수 있습니다.

  • Frankenstein TodoMVC 저장소에서 jquery 분기를 복제하고 아래의 모든 지침을 엄격하게 따르십시오.
  • 또는 Frankenstein Demo 리포지토리에서 React로의 마이그레이션 또는 Vue로의 마이그레이션 전용 분기를 열고 커밋 기록을 추적할 수 있습니다.
  • 또는 여기에서 가장 중요한 코드를 강조 표시할 것이기 때문에 긴장을 풀고 계속 읽을 수 있습니다. 실제 코드보다 프로세스의 메커니즘을 이해하는 것이 훨씬 더 중요합니다.

나는 우리가 이 기사의 이론적 첫 부분에 제시된 단계를 엄격하게 따를 것이라는 점을 한 번 더 언급하고 싶습니다.

바로 뛰어들자!

  1. 마이크로서비스 식별
  2. 호스트-에일리언 액세스 허용
  3. 외계인 마이크로서비스/구성 요소 작성
  4. Alien Service 주변에 웹 구성 요소 래퍼 작성
  5. 호스트 서비스를 웹 구성 요소로 교체
  6. 모든 구성 요소에 대해 헹굼 및 반복
  7. 외계인으로 전환

1. 마이크로서비스 식별

Part 1에서 알 수 있듯이 이 단계에서는 애플리케이션을 하나의 특정 작업 전용의 작고 독립적인 서비스로 구성해야 합니다. 주의 깊은 독자는 할 일 애플리케이션이 이미 작고 독립적이며 자체적으로 하나의 단일 마이크로서비스를 나타낼 수 있음을 알 수 있습니다. 이 응용 프로그램이 좀 더 넓은 맥락에서 작동한다면 이것이 내가 직접 처리하는 방법입니다. 그러나 마이크로서비스를 식별하는 프로세스는 전적으로 주관적이며 정답은 없다는 을 기억하십시오.

따라서 Frankenstein 마이그레이션 프로세스를 더 자세히 보기 위해 한 단계 더 나아가 이 할 일 애플리케이션을 두 개의 독립적인 마이크로서비스로 분할할 수 있습니다.

  1. 새 항목을 추가하기 위한 입력 필드입니다.
    이 서비스는 순전히 이러한 요소의 위치 지정 근접성을 기반으로 하는 애플리케이션의 헤더를 포함할 수도 있습니다.
  2. 이미 추가된 항목의 목록입니다.
    이 서비스는 더 고급이며 목록 자체와 함께 필터링, 목록 항목의 작업 등과 같은 작업도 포함합니다.
두 개의 독립적인 마이크로서비스로 분할된 TodoMVC 애플리케이션
TodoMVC 애플리케이션은 두 개의 독립적인 마이크로서비스로 분할됩니다. (큰 미리보기)

: 선택한 서비스가 진정으로 독립적인지 확인하려면 이러한 각 서비스를 나타내는 HTML 마크업을 제거하십시오. 나머지 기능이 계속 작동하는지 확인하십시오. 우리의 경우 목록 없이 입력 필드에서 localStorage (이 응용 프로그램이 저장소로 사용하는) 에 새 항목을 추가할 수 있어야 하며 , 입력 필드가 누락된 경우에도 목록은 여전히 localStorage 의 항목을 렌더링합니다. 잠재적인 마이크로서비스에 대한 마크업을 제거할 때 애플리케이션에서 오류가 발생하면 1부의 "필요한 경우 리팩터링" 섹션에서 이러한 경우를 처리하는 방법의 예를 살펴보십시오.

물론 우리는 계속해서 두 번째 서비스와 항목 목록을 각 특정 항목에 대한 독립적인 마이크로서비스로 분할할 수 있습니다. 그러나 이 예에서는 너무 세부적일 수 있습니다. 따라서 지금은 애플리케이션에 두 가지 서비스가 있다고 결론을 내립니다. 그들은 독립적이며 각각은 자신의 특정 작업을 위해 일합니다. 따라서 우리는 애플리케이션을 마이크로서비스 로 분할했습니다.

2. 호스트-외계인 액세스 허용

이것들이 무엇인지 간단하게 상기시켜 드리겠습니다.

  • 주인
    이것이 우리의 현재 애플리케이션이 호출되는 것입니다. 그것은 우리가 떠나 려고 하는 프레임워크로 작성되었습니다. 이 특별한 경우에는 jQuery 애플리케이션이 필요합니다.
  • 외계인
    간단히 말해서, 이것은 우리가 이동하려는 새 프레임워크에서 Host를 점진적으로 다시 작성하는 것입니다. 다시 말하지만, 이 특별한 경우에는 React 또는 Vue 애플리케이션입니다.

호스트와 에일리언을 나눌 때 의 경험 법칙은 어느 시점에서든 다른 하나를 중단하지 않고 둘 중 하나를 개발하고 배포할 수 있어야 한다는 것입니다.

호스트와 외계인을 서로 독립적으로 유지하는 것은 Frankenstein 마이그레이션에 매우 중요합니다. 그러나 이것은 둘 사이의 의사 소통을 준비하는 것을 약간 어렵게 만듭니다. 두 가지를 함께 부수지 않고 호스트 액세스 Alien을 어떻게 허용합니까?

호스트의 하위 모듈로 Alien 추가

필요한 설정을 달성하는 데에는 여러 가지 방법이 있지만 이 기준을 충족하도록 프로젝트를 구성하는 가장 간단한 형태는 아마도 git 하위 모듈일 것입니다. 이것이 우리가 이 기사에서 사용할 것입니다. 이 구조의 한계와 문제를 이해하기 위해 git의 하위 모듈이 어떻게 작동하는지 주의 깊게 읽는 것은 여러분에게 맡기겠습니다.

git 하위 모듈이 있는 프로젝트 아키텍처의 일반 원칙 은 다음과 같아야 합니다.

  • Host와 Alien은 모두 독립적이며 별도의 git 저장소에 보관됩니다.
  • 호스트는 Alien을 하위 모듈로 참조합니다. 이 단계에서 호스트는 Alien의 특정 상태(커밋)를 선택하고 호스트의 폴더 구조에서 하위 폴더처럼 보이는 대로 추가합니다.
jQuery TodoMVC 애플리케이션에 git 하위 모듈로 추가된 React TodoMVC
React TodoMVC가 jQuery TodoMVC 애플리케이션에 자식 하위 모듈로 추가되었습니다. (큰 미리보기)

하위 모듈을 추가하는 프로세스는 모든 애플리케이션에서 동일합니다. git submodules 을 가르치는 것은 이 기사의 범위를 벗어나며 Frankenstein Migration 자체와 직접적인 관련이 없습니다. 따라서 가능한 예를 간단히 살펴보겠습니다.

아래 스니펫에서는 React 방향을 예로 사용합니다. 다른 마이그레이션 방향의 경우 react 를 Frankenstein TodoMVC의 분기 이름으로 바꾸거나 필요한 경우 사용자 지정 값으로 조정합니다.

원본 jQuery TodoMVC 응용 프로그램을 사용하여 따라하는 경우:

 $ git submodule add -b react [email protected]:mishunov/frankenstein-todomvc.git react $ git submodule update --remote $ cd react $ npm i

Frankenstein Demo 리포지토리에서 migration/jquery-to-react (또는 다른 마이그레이션 방향) 분기를 따라가면 Alien 애플리케이션이 이미 git submodule 로 거기에 있어야 하며 해당 폴더가 표시되어야 합니다. 단, 폴더는 기본적으로 비어 있으며 등록된 서브모듈을 업데이트 및 초기화해야 합니다.

프로젝트의 루트(호스트)에서:

 $ git submodule update --init $ cd react $ npm i

두 경우 모두 Alien 애플리케이션에 대한 종속성을 설치하지만 하위 폴더에 대한 샌드박스가 되어 호스트를 오염시키지 않습니다.

Alien 애플리케이션을 호스트의 하위 모듈로 추가한 후 독립(마이크로서비스 측면에서) Alien 및 Host 애플리케이션을 얻습니다. 그러나 호스트는 이 경우 Alien의 하위 폴더로 간주하며, 이를 통해 Host는 문제 없이 Alien에 액세스할 수 있습니다.

3. 외계인 마이크로서비스/구성요소 작성

이 단계에서 먼저 마이그레이션할 마이크로 서비스를 결정하고 Alien 측에서 작성/사용해야 합니다. 1단계에서 식별한 동일한 서비스 순서를 따르고 첫 번째 항목부터 시작하겠습니다. 새 항목을 추가하기 위한 입력 필드입니다. 그러나 시작 하기 전에 프론트엔드 프레임워크의 전제 로 이동하고 구성 요소 라는 용어가 거의 모든 현대적인 뼈대.

Frankenstein TodoMVC 저장소의 분기에는 첫 번째 서비스 "새 항목을 추가하기 위한 입력 필드"를 헤더 구성 요소로 나타내는 결과 구성 요소가 포함되어 있습니다.

  • React의 헤더 컴포넌트
  • Vue의 헤더 구성 요소

선택한 프레임워크에서 구성 요소를 작성하는 것은 이 문서의 범위를 벗어나며 Frankenstein 마이그레이션의 일부가 아닙니다. 그러나 Alien 구성 요소를 작성하는 동안 염두에 두어야 할 몇 가지 사항이 있습니다.

독립

우선 Alien의 구성 요소는 이전에 호스트 측에서 설정한 것과 동일한 독립 원칙을 따라야 합니다. 구성 요소는 다른 구성 요소에 어떤 식으로든 종속되어서는 안 됩니다.

상호 운용성

서비스의 독립성 덕분에 호스트의 구성 요소는 상태 관리 시스템, 일부 공유 저장소를 통한 통신 또는 DOM 이벤트 시스템을 통한 직접 통신 등 잘 확립된 방식으로 통신합니다. Alien 구성 요소의 "상호 운용성"은 상태 변경에 대한 정보를 전달하고 다른 구성 요소의 변경 사항을 수신하기 위해 호스트가 설정한 동일한 통신 소스에 연결할 수 있어야 함을 의미합니다. 실제로 이것은 호스트의 구성 요소가 DOM 이벤트를 통해 통신하는 경우 상태 관리를 염두에 두고 독점적으로 Alien 구성 요소를 구축하는 것이 불행히도 이러한 유형의 마이그레이션에서 완벽하게 작동하지 않는다는 것을 의미합니다.

예를 들어 jQuery 구성 요소의 기본 통신 채널인 js/storage.js 파일을 살펴보세요.

 ... fetch: function() { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); }, save: function(todos) { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); var event = new CustomEvent("store-update", { detail: { todos } }); document.dispatchEvent(event); }, ...

여기에서는 localStorage (이 예는 보안에 중요하지 않음)를 사용하여 할 일 항목을 저장하고, 저장소에 대한 변경 사항이 기록되면 모든 구성 요소가 수신할 수 있는 document 요소에 사용자 정의 DOM 이벤트를 전달합니다.

동시에 Alien 측(React라고 하자)에서는 원하는 대로 복잡한 상태 관리 통신을 설정할 수 있습니다. 그러나 미래를 위해 유지하는 것이 현명할 것입니다. Alien React 구성 요소를 Host에 성공적으로 통합하려면 Host에서 사용하는 것과 동일한 통신 채널에 연결해야 합니다. 이 경우 localStorage 입니다. 간단하게 하기 위해 호스트의 저장 파일을 Alien에 복사하고 구성 요소를 연결했습니다.

 import todoStorage from "../storage"; class Header extends Component { constructor(props) { this.state = { todos: todoStorage.fetch() }; } componentDidMount() { document.addEventListener("store-update", this.updateTodos); } componentWillUnmount() { document.removeEventListener("store-update", this.updateTodos); } componentDidUpdate(prevProps, prevState) { if (prevState.todos !== this.state.todos) { todoStorage.save(this.state.todos); } } ... }

이제 Alien 구성 요소는 호스트 구성 요소와 동일한 언어로 대화할 수 있으며 그 반대의 경우도 마찬가지입니다.

4. Alien 서비스 주변에 웹 구성 요소 래퍼 작성

이제 겨우 네 번째 단계에 불과하지만 다음과 같이 많은 것을 달성했습니다.

  • 호스트 애플리케이션을 Alien 서비스로 대체할 준비가 된 독립 서비스로 분할했습니다.
  • Host와 Alien은 서로 완전히 독립적이지만 git submodules 를 통해 매우 잘 연결되도록 설정했습니다.
  • 새 프레임워크를 사용하여 첫 번째 Alien 구성 요소를 작성했습니다.

이제 새로운 Alien 구성 요소가 호스트에서 작동할 수 있도록 호스트와 Alien 사이에 브리지를 설정할 차례입니다.

1부의 알림 : 호스트에 사용 가능한 패키지 번들러가 있는지 확인하십시오. 이 기사에서 우리는 Webpack에 의존하지만 이것이 롤업이나 선택한 다른 번들러에서 이 기술이 작동하지 않는다는 것을 의미하지는 않습니다. 그러나 Webpack에서 귀하의 실험으로의 매핑을 남겨둡니다.

명명 규칙

이전 기사에서 언급했듯이 Web Components를 사용하여 Alien을 Host에 통합할 것입니다. 호스트 측에서 js/frankenstein-wrappers/Header-wrapper.js 라는 새 파일을 만듭니다. (이것은 우리의 첫 Frankenstein 래퍼가 될 것입니다.) Alien 응용 프로그램의 구성 요소와 동일한 이름으로 래퍼 이름을 지정하는 것이 좋습니다. 예를 들어 " -wrapper " 접미사를 추가하는 것입니다. 이것이 왜 좋은 생각인지는 나중에 알게 될 것이지만 지금은 이것이 Alien 구성 요소가 Header.js (React에서) 또는 Header.vue (Vue에서)라고 하는 경우 해당 래퍼가 호스트 측의 이름은 Header-wrapper.js 여야 합니다.

첫 번째 래퍼에서 사용자 지정 요소를 등록하기 위한 기본 상용구로 시작합니다.

 class FrankensteinWrapper extends HTMLElement {} customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);

다음으로 이 요소에 대한 Shadow DOM 을 초기화해야 합니다.

Shadow DOM을 사용하는 이유를 알아보려면 Part 1을 참조하세요.

 class FrankensteinWrapper extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }); } }

이것으로 웹 구성 요소의 모든 필수 요소가 설정되었으며 Alien 구성 요소를 혼합에 추가할 시간입니다. 우선 Frankenstein 래퍼를 시작할 때 Alien 구성 요소의 렌더링을 담당하는 모든 비트를 가져와야 합니다.

 import React from "../../react/node_modules/react"; import ReactDOM from "../../react/node_modules/react-dom"; import HeaderApp from "../../react/src/components/Header"; ...

여기서 우리는 잠시 멈춰야 합니다. 호스트의 node_modules 에서 Alien의 종속성을 가져오지 않습니다. 모든 것은 react/ 하위 폴더에 있는 Alien 자체에서 나옵니다. 이것이 2단계가 중요한 이유이며 호스트가 Alien 자산에 대한 전체 액세스 권한을 갖도록 하는 것이 중요합니다.

이제 웹 구성 요소의 Shadow DOM 내에서 Alien 구성 요소를 렌더링할 수 있습니다.

 ... connectedCallback() { ... ReactDOM.render(<HeaderApp />, this.shadowRoot); } ...

참고 : 이 경우 React는 다른 것이 필요하지 않습니다. 그러나 Vue 구성 요소를 렌더링하려면 다음과 같이 Vue 구성 요소를 포함할 래핑 노드를 추가해야 합니다.

 ... connectedCallback() { const mountPoint = document.createElement("div"); this.attachShadow({ mode: "open" }).appendChild(mountPoint); new Vue({ render: h => h(VueHeader) }).$mount(mountPoint); } ...

그 이유는 React와 Vue가 구성 요소를 렌더링하는 방식이 다르기 때문입니다. React는 참조된 DOM 노드에 구성 요소를 추가하는 반면 Vue는 참조된 DOM 노드를 구성 요소로 대체합니다. 따라서 Vue에 대해 .$mount(this.shadowRoot) 를 수행하면 본질적으로 Shadow DOM을 대체합니다.

그것이 우리가 지금 래퍼에 해야 할 전부입니다. jQuery-to-React 및 jQuery-to-Vue 마이그레이션 방향 모두에서 Frankenstein 래퍼에 대한 현재 결과는 여기에서 찾을 수 있습니다.

  • React 컴포넌트용 Frankenstein 래퍼
  • Vue 구성 요소용 Frankenstein 래퍼

Frankenstein 래퍼의 역학을 요약하면 다음과 같습니다.

  1. 사용자 정의 요소를 생성하고,
  2. Shadow DOM 시작,
  3. Alien 구성 요소를 렌더링하는 데 필요한 모든 것을 가져옵니다.
  4. 사용자 정의 요소의 Shadow DOM 내에서 Alien 구성 요소를 렌더링합니다.

그러나 이것은 Alien in Host를 자동으로 렌더링하지 않습니다. 기존 호스트 마크업을 새로운 Frankenstein 래퍼로 교체해야 합니다.

안전벨트를 매세요. 생각만큼 간단하지 않을 수 있습니다!

5. 호스트 서비스를 웹 구성 요소로 교체

계속해서 새로운 Header-wrapper.js 파일을 index.html 에 추가하고 기존 헤더 마크업을 새로 생성된 <frankenstein-header-wrapper> 사용자 정의 요소로 교체해 보겠습니다.

 ... <!-- <header class="header">--> <!-- <h1>todos</h1>--> <!-- <input class="new-todo" placeholder="What needs to be done?" autofocus>--> <!-- </header>--> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script type="module" src="js/frankenstein-wrappers/Header-wrapper.js"></script>

불행히도 이것은 그렇게 간단하게 작동하지 않습니다. 브라우저를 열고 콘솔을 확인하면 Uncaught SyntaxError 가 기다리고 있습니다. 브라우저 및 ES6 모듈 지원에 따라 ES6 가져오기 또는 Alien 구성 요소가 렌더링되는 방식과 관련이 있습니다. 어느 쪽이든, 우리는 그것에 대해 뭔가를 해야 하지만 문제와 솔루션은 대부분의 독자에게 친숙하고 명확해야 합니다.

5.1. 필요한 경우 Webpack 및 Babel 업데이트

Frankenstein 래퍼를 통합하기 전에 Webpack과 Babel 마법을 사용해야 합니다. 이러한 도구를 랭글링하는 것은 이 기사의 범위를 벗어나지만 Frankenstein Demo 리포지토리에서 해당 커밋을 볼 수 있습니다.

  • React로 마이그레이션하기 위한 구성
  • Vue로 마이그레이션하기 위한 구성

본질적으로, 우리 는 Webpack의 구성 에서 Frankenstein 래퍼와 관련된 모든 것을 한 곳에 포함하도록 새로운 진입점 frankenstein 과 파일 처리를 설정했습니다.

호스트의 Webpack이 Alien 구성 요소와 웹 구성 요소를 처리하는 방법을 알게 되면 호스트의 마크업을 새로운 Frankenstein 래퍼로 교체할 준비가 된 것입니다.

5.2. 실제 구성 요소의 교체

이제 구성 요소의 교체가 간단해야 합니다. 호스트의 index.html 에서 다음을 수행합니다.

  1. <header class="header"> DOM 요소를 <frankenstein-header-wrapper> 교체합니다.
  2. 새 스크립트 frankenstein.js 를 추가합니다. 이것은 Frankenstein 래퍼와 관련된 모든 것을 포함하는 Webpack의 새로운 진입점입니다.
 ... <!-- We replace <header class="header"> --> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script src="./frankenstein.js"></script>

그게 다야! 필요한 경우 서버를 다시 시작하고 호스트에 통합된 Alien 구성 요소의 마법을 목격하십시오.

그러나 여전히 뭔가 부족한 것 같습니다. 호스트 컨텍스트의 Alien 구성 요소는 독립 실행형 Alien 응용 프로그램의 컨텍스트에서와 같은 방식으로 보이지 않습니다. 그것은 단순히 스타일이 없습니다.

호스트에 통합된 후 스타일이 지정되지 않은 Alien React 구성 요소
호스트에 통합된 후 스타일이 지정되지 않은 Alien React 구성 요소(큰 미리 보기)

왜 그래야만하지? 구성 요소의 스타일이 Alien 구성 요소와 함께 Host에 자동으로 통합되어야 하지 않습니까? 그랬으면 좋겠지만 너무 많은 상황에서 그러하듯이 상황에 따라 다릅니다. Frankenstein 마이그레이션의 도전적인 부분에 도달하고 있습니다.

5.3. 외계인 구성 요소의 스타일링에 대한 일반 정보

우선, 아이러니는 작동 방식에 버그가 없다는 것입니다. 모든 것이 작동하도록 설계된 그대로입니다. 이를 설명하기 위해 구성 요소를 스타일링하는 다양한 방법을 간략하게 언급하겠습니다.

전역 스타일

우리 모두는 이것에 익숙합니다. 전역 스타일은 특정 구성 요소 없이 배포될 수 있으며 전체 페이지에 적용될 수 있습니다. 전역 스타일은 선택기가 일치하는 모든 DOM 노드에 영향을 줍니다.

전역 스타일의 몇 가지 예는 index.html 에 있는 <style><link rel="stylesheet"> 태그입니다. 또는 전역 스타일시트를 일부 루트 JS 모듈로 가져와 모든 구성 요소가 액세스할 수 있도록 할 수도 있습니다.

이러한 방식으로 응용 프로그램을 스타일링하는 문제는 명백합니다. 대규모 응용 프로그램에 대해 모놀리식 스타일시트를 유지 관리하는 것이 매우 어려워집니다. 또한 이전 기사에서 보았듯이 전역 스타일은 React 또는 Vue에서와 같이 기본 DOM 트리에서 직접 렌더링되는 구성 요소를 쉽게 깨뜨릴 수 있습니다.

번들 스타일

이러한 스타일은 일반적으로 구성 요소 자체와 밀접하게 결합되어 있으며 구성 요소 없이 배포되는 경우는 드뭅니다. 스타일은 일반적으로 구성요소와 동일한 파일에 있습니다. 이러한 유형의 스타일 지정에 대한 좋은 예는 React 또는 CSS Modules의 styled-components와 Vue의 단일 파일 구성 요소에 있는 Scoped CSS입니다. 그러나 번들 스타일을 작성하기 위한 다양한 도구와 상관없이 대부분의 기본 원칙은 동일합니다. 도구는 구성 요소에 정의된 스타일을 잠그는 범위 지정 메커니즘을 제공하여 스타일이 다른 구성 요소나 전역 요소를 손상시키지 않도록 합니다. 스타일.

범위가 지정된 스타일이 취약한 이유는 무엇입니까?

1부에서는 Frankenstein 마이그레이션에서 Shadow DOM의 사용을 정당화할 때 범위 지정 대 캡슐화) 및 Shadow DOM의 캡슐화가 범위 지정 스타일 도구와 어떻게 다른지에 대해 간략하게 다루었습니다. 그러나 범위 지정 도구가 구성 요소에 대해 취약한 스타일을 제공하는 이유를 설명하지 않았으며 이제 스타일이 지정되지 않은 Alien 구성 요소를 만났을 때 이해에 필수적이 되었습니다.

최신 프레임워크를 위한 모든 범위 지정 도구는 유사하게 작동합니다.

  • 범위나 캡슐화에 대해 많이 생각하지 않고 어떤 식으로든 구성 요소에 대한 스타일을 작성합니다.
  • Webpack 또는 Rollup과 같은 번들링 시스템을 통해 가져온/포함된 스타일시트로 구성 요소를 실행합니다.
  • 번들러는 고유한 CSS 클래스 또는 기타 속성을 생성하여 HTML 및 해당 스타일시트에 대한 개별 선택기를 생성하고 주입합니다.
  • 번들러는 문서의 <head><style> 항목을 만들고 고유한 혼합 선택기와 함께 구성 요소의 스타일을 넣습니다.

그 정도입니다. 그것은 작동하고 많은 경우에 잘 작동합니다. 그렇지 않은 경우를 제외하고: 모든 구성 요소의 스타일이 전역 스타일 범위에 있는 경우 예를 들어 더 높은 특이성을 사용하여 이러한 스타일을 쉽게 깨뜨릴 수 있습니다. 이것은 범위 지정 도구의 잠재적인 취약성을 설명하지만 Alien 구성 요소가 완전히 스타일이 지정되지 않은 이유는 무엇입니까?

DevTools를 사용하여 현재 호스트를 살펴보겠습니다. 예를 들어 Alien React 구성 요소로 새로 추가된 Frankenstein 래퍼를 검사하면 다음과 같은 내용을 볼 수 있습니다.

내부에 Alien 구성 요소가 있는 Frankenstein 래퍼. Alien의 노드에 있는 고유한 CSS 클래스에 주목하십시오.
내부에 Alien 구성 요소가 있는 Frankenstein 래퍼. Alien의 노드에 있는 고유한 CSS 클래스에 유의하십시오. (큰 미리보기)

따라서 Webpack은 구성 요소에 대해 고유한 CSS 클래스를 생성합니다. 엄청난! 그렇다면 스타일은 어디에 있습니까? 글쎄요, 스타일은 문서의 <head> 에 정확히 디자인된 위치에 있습니다.

Alien 구성 요소는 Frankenstein 래퍼 내에 있지만 스타일은 문서의 머리 부분에 있습니다.
Alien 구성 요소는 Frankenstein 래퍼 내에 있지만 스타일은 문서의 <head> 에 있습니다. (큰 미리보기)

따라서 모든 것이 제대로 작동하며 이것이 주요 문제입니다. Alien 구성 요소는 Shadow DOM에 있고 Part #1에서 설명했듯이 Shadow DOM은 Shadow 테두리를 넘을 수 없는 구성 요소에 대해 새로 생성된 스타일시트를 포함하여 페이지의 나머지 부분과 전역 스타일에서 구성 요소의 전체 캡슐화를 제공합니다. Alien 구성 요소로 이동하십시오. 따라서 Alien 구성 요소는 스타일이 지정되지 않은 상태로 유지됩니다. 그러나 이제 문제를 해결하는 전술은 분명해야 합니다. 어떻게든 구성 요소의 스타일을 문서의 <head> 대신 구성 요소가 있는 동일한 Shadow DOM에 배치해야 합니다.

5.4. Alien 구성 요소의 스타일 고정

지금까지 프레임워크로의 마이그레이션 프로세스는 동일했습니다. 그러나 여기서 상황이 달라지기 시작합니다. 모든 프레임워크에는 구성 요소의 스타일을 지정하는 방법에 대한 권장 사항이 있으므로 문제를 해결하는 방법이 다릅니다. 여기에서는 가장 일반적인 경우에 대해 논의하지만 작업하는 프레임워크가 구성 요소의 스타일을 지정하는 고유한 방법을 사용하는 경우 구성 요소의 스타일을 <head> 대신 Shadow DOM에 넣는 것과 같은 기본 전술을 염두에 두어야 합니다.

이 장에서는 다음에 대한 수정 사항을 다룹니다.

  • Vue의 CSS 모듈이 포함된 번들 스타일(범위 지정 CSS에 대한 전술은 동일함);
  • React의 styled-components와 묶음 스타일;
  • 일반 CSS 모듈 및 전역 스타일. CSS 모듈은 일반적으로 전역 스타일시트와 매우 유사하고 특정 구성 요소에서 스타일을 분리하는 모든 구성 요소에서 가져올 수 있기 때문에 이들을 결합합니다.

먼저 제약 조건: 스타일을 수정하기 위해 수행하는 모든 작업은 Alien 구성 요소 자체를 손상시키지 않아야 합니다 . 그렇지 않으면 Alien 및 Host 시스템의 독립성을 잃게 됩니다. 따라서 스타일링 문제를 해결하기 위해 번들러의 구성이나 Frankenstein 래퍼에 의존할 것입니다.

Vue 및 Shadow DOM의 번들 스타일

Vue 응용 프로그램을 작성 중이라면 아마도 단일 파일 구성 요소를 사용하고 있을 것입니다. Webpack도 사용하고 있다면 vue-loadervue-style-loader 두 로더에 익숙해야 합니다. 전자를 사용하면 단일 파일 구성 요소를 작성할 수 있고 후자는 구성 요소의 CSS를 문서에 <style> 태그로 동적으로 삽입합니다. 기본적으로 vue-style-loader 는 구성 요소의 스타일을 문서의 <head> 에 삽입합니다. 그러나 두 패키지 모두 기본 동작을 쉽게 변경하고 스타일(옵션 이름에서 알 수 있듯이)을 Shadow DOM에 삽입할 수 있도록 하는 구성에서 shadowMode 옵션을 허용합니다. 행동으로 봅시다.

웹팩 구성

최소한 Webpack 구성 파일에는 다음이 포함되어야 합니다.

 const VueLoaderPlugin = require('vue-loader/lib/plugin'); ... module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { shadowMode: true } }, { test: /\.css$/, include: path.resolve(__dirname, '../vue'), use: [ { loader:'vue-style-loader', options: { shadowMode: true } }, 'css-loader' ] } ], plugins: [ new VueLoaderPlugin() ] }

실제 응용 프로그램에서 test: /\.css$/ 블록은 Host 및 Alien 구성을 모두 설명하기 위해 더 정교합니다(아마도 oneOf 규칙 포함). 그러나 이 경우 jQuery는 index.html 에서 간단한 <link rel="stylesheet"> 로 스타일이 지정되므로 Webpack을 통해 호스트에 대한 스타일을 빌드하지 않으며 Alien에만 제공하는 것이 안전합니다.

래퍼 구성

Webpack 구성 외에도 Vue가 올바른 Shadow DOM을 가리키도록 Frankenstein 래퍼를 업데이트해야 합니다. Header-wrapper.js 에서 Vue 구성 요소의 렌더링에는 Frankenstein 래퍼의 shadowRoot 로 이어지는 shadowRoot 속성이 포함되어야 합니다.

 ... new Vue({ shadowRoot: this.shadowRoot, render: h => h(VueHeader) }).$mount(mountPoint); ...

파일을 업데이트하고 서버를 다시 시작한 후 DevTools에 다음과 같은 내용이 표시되어야 합니다.

모든 고유 CSS 클래스가 보존된 Frankenstein 래퍼 내에 배치된 Alien Vue 구성 요소와 함께 번들로 제공되는 스타일입니다.
모든 고유 CSS 클래스가 보존된 Frankenstein 래퍼 내에 배치된 Alien Vue 구성 요소와 함께 번들로 제공되는 스타일입니다. (큰 미리보기)

마지막으로 Vue 구성 요소의 스타일은 Shadow DOM 내에 있습니다. 동시에 애플리케이션은 다음과 같아야 합니다.

헤더 구성 요소가 원래대로 보이기 시작합니다. 그러나 아직 부족한 것이 있습니다.
헤더 구성 요소가 원래대로 보이기 시작합니다. 그러나 아직 부족한 것이 있습니다. (큰 미리보기)

Vue 애플리케이션과 유사한 것을 얻기 시작합니다. 구성 요소와 함께 번들로 제공되는 스타일이 래퍼의 Shadow DOM에 주입되지만 구성 요소는 여전히 예상대로 보이지 않습니다. 그 이유는 원래 Vue 응용 프로그램에서 구성 요소가 번들 스타일뿐만 아니라 부분적으로 전역 스타일로 스타일 지정되기 때문입니다. 그러나 전역 스타일을 수정하기 전에 Vue와 동일한 상태로 React 통합을 가져와야 합니다.

React 및 Shadow DOM의 번들 스타일

React 구성 요소의 스타일을 지정할 수 있는 방법이 많기 때문에 Frankenstein Migration에서 Alien 구성 요소를 수정하는 특정 솔루션은 처음에 구성 요소의 스타일을 지정하는 방법에 따라 다릅니다. 가장 일반적으로 사용되는 대안을 간략하게 살펴보겠습니다.

스타일 구성 요소

styled-components는 React 컴포넌트를 스타일링하는 가장 인기 있는 방법 중 하나입니다. Header React 컴포넌트의 경우 styled-components는 정확히 우리가 스타일을 지정하는 방식입니다. 이것은 고전적인 CSS-in-JS 접근 방식이기 때문에 예를 들어 .css 또는 .js 파일과 같이 번들러를 연결할 수 있는 전용 확장자를 가진 파일이 없습니다. 운 좋게도 스타일 구성 요소를 사용하면 구성 요소를 돕는 StyleSheetManager 의 도움으로 문서의 head 대신 구성 요소의 스타일을 사용자 정의 노드(이 경우 Shadow DOM)에 삽입할 수 있습니다. 이는 미리 정의된 구성 요소로, "스타일 정보를 주입하기 위한 대체 DOM 노드"를 정의하는 target 속성을 허용하는 styled-components 패키지와 함께 설치됩니다. 바로 우리에게 필요한 것! 또한 Webpack 구성을 변경할 필요도 없습니다. 모든 것은 Frankenstein 래퍼에 달려 있습니다.

React Alien 구성 요소가 포함된 Header-wrapper.js 를 다음 줄로 업데이트해야 합니다.

 ... import { StyleSheetManager } from "../../react/node_modules/styled-components"; ... const target = this.shadowRoot; ReactDOM.render( <StyleSheetManager target={target}> <HeaderApp /> </StyleSheetManager>, appWrapper ); ...

여기서 StyleSheetManager 구성 요소(호스트가 아닌 Alien에서)를 가져와서 React 구성 요소를 래핑합니다. 동시에 shadowRoot 를 가리키는 target 속성을 보냅니다. 그게 다야 서버를 다시 시작하면 DevTools에서 다음과 같이 표시되어야 합니다.

모든 고유한 CSS 클래스가 보존된 Frankenstein 래퍼 내에 배치된 React Alien 구성 요소와 함께 번들로 제공되는 스타일입니다.
모든 고유한 CSS 클래스가 보존된 Frankenstein 래퍼 내에 배치된 React Alien 구성 요소와 함께 번들로 제공되는 스타일입니다. (큰 미리보기)

이제 구성 요소의 스타일은 <head> 대신 Shadow DOM에 있습니다. 이렇게 하면 앱의 렌더링이 이전에 Vue 앱에서 본 것과 유사합니다.

번들 스타일을 Frankenstein 래퍼로 이동한 후 Alien React 구성 요소가 더 좋아 보이기 시작합니다. 그러나 우리는 아직 거기에 있지 않습니다.
번들 스타일을 Frankenstein 래퍼로 이동한 후 Alien React 구성 요소가 더 좋아 보이기 시작합니다. 그러나 우리는 아직 거기에 있지 않습니다. (큰 미리보기)

같은 이야기: styled-components는 React 구성 요소의 묶음 부분만 담당 하고 전역 스타일은 나머지 부분을 관리합니다. 한 가지 유형의 스타일 구성 요소를 더 검토한 후 전역 스타일로 다시 돌아갑니다.

CSS 모듈

앞서 수정한 Vue 구성 요소를 자세히 살펴보면 CSS 모듈이 해당 구성 요소의 스타일을 정확히 지정하는 방식임을 알 수 있습니다. However, even if we style it with Scoped CSS (another recommended way of styling Vue components) the way we fix our unstyled component doesn't change: it is still up to vue-loader and vue-style-loader to handle it through shadowMode: true option.

When it comes to CSS Modules in React (or any other system using CSS Modules without any dedicated tools), things get a bit more complicated and less flexible, unfortunately.

Let's take a look at the same React component which we've just integrated, but this time styled with CSS Modules instead of styled-components. The main thing to note in this component is a separate import for stylesheet:

 import styles from './Header.module.css'

The .module.css extension is a standard way to tell React applications built with the create-react-app utility that the imported stylesheet is a CSS Module. The stylesheet itself is very basic and does precisely the same our styled-components do.

Integrating CSS modules into a Frankenstein wrapper consists of two parts:

  • Enabling CSS Modules in bundler,
  • Pushing resulting stylesheet into Shadow DOM.

I believe the first point is trivial: all you need to do is set { modules: true } for css-loader in your Webpack configuration. Since, in this particular case, we have a dedicated extension for our CSS Modules ( .module.css ), we can have a dedicated configuration block for it under the general .css configuration:

 { test: /\.css$/, oneOf: [ { test: /\.module\.css$/, use: [ ... { loader: 'css-loader', options: { modules: true, } } ] } ] }

Note : A modules option for css-loader is all we have to know about CSS Modules no matter whether it's React or any other system. When it comes to pushing resulting stylesheet into Shadow DOM, however, CSS Modules are no different from any other global stylesheet.

By now, we went through the ways of integrating bundled styles into Shadow DOM for the following conventional scenarios:

  • Vue components, styled with CSS Modules. Dealing with Scoped CSS in Vue components won't be any different;
  • React components, styled with styled-components;
  • Components styled with raw CSS Modules (without dedicated tools like those in Vue). For these, we have enabled support for CSS modules in Webpack configuration.

However, our components still don't look as they are supposed to because their styles partially come from global styles . Those global styles do not come to our Frankenstein wrappers automatically. Moreover, you might get into a situation in which your Alien components are styled exclusively with global styles without any bundled styles whatsoever. So let's finally fix this side of the story.

Global Styles And Shadow DOM

Having your components styled with global styles is neither wrong nor bad per se: every project has its requirements and limitations. However, the best you can do for your components if they rely on some global styles is to pull those styles into the component itself. This way, you have proper easy-to-maintain self-contained components with bundled styles.

Nevertheless, it's not always possible or reasonable to do so: several components might share some styling, or your whole styling architecture could be built using global stylesheets that are split into the modular structure, and so on.

So having an opportunity to pull in global styles into our Frankenstein wrappers wherever it's required is essential for the success of this type of migration. Before we get to an example, keep in mind that this part is the same for pretty much any framework of your choice — be it React, Vue or anything else using global stylesheets!

Let's get back to our Header component from the Vue application. Take a look at this import:

 import "todomvc-app-css/index.css";

This import is where we pull in the global stylesheet. In this case, we do it from the component itself. It's only one way of using global stylesheet to style your component, but it's not necessarily like this in your application.

Some parent module might add a global stylesheet like in our React application where we import index.css only in index.js , and then our components expect it to be available in the global scope. Your component's styling might even rely on a stylesheet, added with <style> or <link> to your index.html . 그것은 중요하지 않습니다. What matters, however, is that you should expect to either import global stylesheets in your Alien component (if it doesn't harm the Alien application) or explicitly in the Frankenstein wrapper. Otherwise, the wrapper would not know that the Alien component needs any stylesheet other than the ones already bundled with it.

Caution . If there are many global stylesheets to be shared between Alien components and you have a lot of such components, this might harm the performance of your Host application under the migration period.

Here is how import of a global stylesheet, required for the Header component, is done in Frankenstein wrapper for React component:

 // we import directly from react/, not from Host import '../../react/node_modules/todomvc-app-css/index.css'

Nevertheless, by importing a stylesheet this way, we still bring the styles to the global scope of our Host, while what we need is to pull in the styles into our Shadow DOM. 어떻게 합니까?

Webpack configuration for global stylesheets & Shadow DOM

First of all, you might want to add an explicit test to make sure that we process only the stylesheets coming from our Alien. In case of our React migration, it will look similar to this:

 test: /\.css$/, oneOf: [ // this matches stylesheets coming from /react/ subfolder { test: /\/react\//, use: [] }, ... ]

In case of Vue application, obviously, you change test: /\/react\// with something like test: /\/vue\// . Apart from that, the configuration will be the same for any framework. Next, let's specify the required loaders for this block.

 ... use: [ { loader: 'style-loader', options: { ... } }, 'css-loader' ]

Two things to note. First, you have to specify modules: true in css-loader 's configuration if you're processing CSS Modules of your Alien application.

Second, we should convert styles into <style> tag before injecting those into Shadow DOM. In the case of Webpack, for that, we use style-loader . The default behavior for this loader is to insert styles into the document's head. Typically. And this is precisely what we don't want: our goal is to get stylesheets into Shadow DOM. However, in the same way we used target property for styled-components in React or shadowMode option for Vue components that allowed us to specify custom insertion point for our <style> tags, regular style-loader provides us with nearly same functionality for any stylesheet: the insert configuration option is exactly what helps us achieve our primary goal. 좋은 소식! Let's add it to our configuration.

 ... { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }

However, not everything is so smooth here with a couple of things to keep in mind.

전역 스타일시트 및 style-loader insert 옵션

이 옵션에 대한 설명서를 확인하면 이 옵션이 구성당 하나의 선택기를 사용한다는 것을 알 수 있습니다. 즉, 전역 스타일을 Frankenstein 래퍼로 가져와야 하는 Alien 구성 요소가 여러 개 있는 경우 각 Frankenstein 래퍼에 대해 style-loader 를 지정해야 합니다. 실제로 이것은 모든 래퍼에 제공하기 위해 구성 블록의 oneOf 규칙에 의존해야 함을 의미합니다.

 { test: /\/react\//, oneOf: [ { test: /1-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '1-frankenstein-wrapper' } }, `css-loader` ] }, { test: /2-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '2-frankenstein-wrapper' } }, `css-loader` ] }, // etc. ], }

매우 유연하지 않습니다. 동의합니다. 그럼에도 불구하고 마이그레이션할 구성 요소가 수백 가지가 아닌 한 큰 문제는 아닙니다. 그렇지 않으면 Webpack 구성을 유지 관리하기 어렵게 만들 수 있습니다. 그러나 실제 문제는 Shadow DOM에 대한 CSS 선택기를 작성할 수 없다는 것입니다.

이 문제를 해결하기 위해 insert 옵션이 일반 선택자 대신 삽입을 위한 고급 논리를 지정하는 기능을 사용할 수도 있음을 알 수 있습니다. 이를 통해 이 옵션을 사용하여 스타일시트를 Shadow DOM에 바로 삽입할 수 있습니다! 단순화된 형태로 다음과 유사할 수 있습니다.

 insert: function(element) { var parent = document.querySelector('frankenstein-header-wrapper').shadowRoot; parent.insertBefore(element, parent.firstChild); }

유혹하지 않습니까? 그러나 이것은 우리의 시나리오에서 작동하지 않거나 최적에서 멀리 작동합니다. <frankenstein-header-wrapper> 는 실제로 index.html 에서 사용할 수 있습니다(5.2단계에서 추가했기 때문에). 그러나 Webpack이 Alien 구성 요소 또는 Frankenstein 래퍼에 대한 모든 종속성(스타일시트 포함)을 처리할 때 Shadow DOM은 Frankenstein 래퍼에서 아직 초기화되지 않습니다. 가져오기는 그 전에 처리됩니다. 따라서 shadowRoot에 직접 insert 을 가리키면 오류가 발생합니다.

Webpack이 스타일시트 종속성을 처리하기 전에 Shadow DOM이 초기화되도록 보장할 수 있는 경우는 단 한 번뿐입니다. Alien 구성 요소가 스타일시트 자체를 가져오지 않고 Frankenstein 래퍼가 가져오기를 수행하는 경우 Shadow DOM을 설정한 후 동적 가져오기를 사용하고 필요한 스타일시트를 가져올 수 있습니다.

 this.attachShadow({ mode: "open" }); import('../vue/node_modules/todomvc-app-css/index.css');

이것은 작동할 것입니다: 위의 insert 구성과 결합된 이러한 가져오기는 실제로 올바른 Shadow DOM을 찾고 여기에 <style> 태그를 삽입합니다. 그럼에도 불구하고 스타일시트를 가져오고 처리하는 데 시간이 걸리므로 연결 속도가 느리거나 장치가 느린 사용자는 스타일시트가 래퍼의 Shadow DOM 내에서 제자리에 놓이기 전에 스타일이 지정되지 않은 구성 요소의 순간에 직면할 수 있습니다.

Unstyled Alien 구성 요소는 전역 스타일시트를 가져와서 Shadow DOM에 추가하기 전에 렌더링됩니다.
Unstyled Alien 구성 요소는 전역 스타일시트를 가져와서 Shadow DOM에 추가하기 전에 렌더링됩니다. (큰 미리보기)

따라서 대체로 insert 가 기능을 허용하더라도 불행히도 그것만으로는 충분하지 않으며 frankenstein-header-wrapper 와 같은 일반 CSS 선택기로 돌아가야 합니다. 그러나 스타일시트는 자동으로 Shadow DOM에 배치되지 않으며 스타일시트는 Shadow DOM 외부의 <frankenstein-header-wrapper> 에 있습니다.

style-loader는 가져온 스타일시트를 Frankenstein 래퍼에 넣지만 Shadow DOM 외부에 넣습니다.
style-loader 는 가져온 스타일시트를 Frankenstein 래퍼에 넣지만 Shadow DOM 외부에 넣습니다. (큰 미리보기)

퍼즐 조각이 하나 더 필요합니다.

전역 스타일시트 및 Shadow DOM에 대한 래퍼 구성

다행히도 래퍼 측에서는 수정이 매우 간단합니다. Shadow DOM이 초기화되면 현재 래퍼에서 보류 중인 스타일시트를 확인하고 이를 Shadow DOM으로 가져와야 합니다.

현재 글로벌 스타일시트 가져오기 상태는 다음과 같습니다.

  • Shadow DOM에 추가해야 하는 스타일시트를 가져옵니다. 스타일시트는 Alien 구성 요소 자체에서 또는 Frankenstein 래퍼에서 명시적으로 가져올 수 있습니다. 예를 들어 React로 마이그레이션하는 경우 가져오기는 래퍼에서 초기화됩니다. 그러나 Vue로 마이그레이션할 때 유사한 구성 요소 자체가 필요한 스타일시트를 가져오므로 래퍼에서 아무 것도 가져올 필요가 없습니다.
  • 위에서 지적했듯이 Webpack이 Alien 구성 요소에 대한 .css 가져오기를 처리할 때 style-loaderinsert 옵션 덕분에 스타일시트가 Frankenstein 래퍼에 주입되지만 Shadow DOM 외부에 있습니다.

Frankenstein 래퍼에서 Shadow DOM의 단순화된 초기화는 현재(스타일시트를 가져오기 전) 다음과 유사해야 합니다.

 this.attachShadow({ mode: "open" }); ReactDOM.render(); // or `new Vue()`

이제 스타일이 지정되지 않은 구성 요소의 깜박임을 방지하려면 Shadow DOM을 초기화 한 후 Alien 구성 요소의 렌더링 전에 필요한 모든 스타일시트를 가져와야 합니다.

 this.attachShadow({ mode: "open" }); Array.prototype.slice .call(this.querySelectorAll("style")) .forEach(style => { this.shadowRoot.prepend(style); }); ReactDOM.render(); // or new Vue({})

많은 세부 사항이 포함된 긴 설명이었지만 주로 전역 스타일시트를 Shadow DOM으로 가져오는 데 필요한 모든 것입니다.

  • Webpack 구성에서 필요한 Frankenstein 래퍼를 가리키는 insert 옵션이 있는 style-loader 를 추가합니다.
  • 래퍼 자체에서 Shadow DOM 초기화 후 Alien 구성 요소의 렌더링 전에 "보류 중인" 스타일시트를 가져옵니다.

이러한 변경 사항을 구현한 후에는 구성 요소에 필요한 모든 것이 있어야 합니다. 추가할 수 있는 유일한 것은(이것은 요구 사항이 아님) 호스트 환경에서 Alien 구성 요소를 미세 조정하기 위한 몇 가지 사용자 정의 CSS입니다. 호스트에서 사용할 때 Alien 구성 요소의 스타일을 완전히 다르게 지정할 수도 있습니다. 기사의 요점을 넘어서지만 래퍼의 최종 코드를 보면 래퍼 수준에서 간단한 스타일을 재정의하는 방법에 대한 예제를 찾을 수 있습니다.

  • React 컴포넌트용 Frankenstein 래퍼
  • Vue 구성 요소에 대한 Frankenstein 래퍼

이 마이그레이션 단계에서 Webpack 구성을 살펴볼 수도 있습니다.

  • 스타일이 지정된 구성 요소를 사용하여 React로 마이그레이션
  • CSS 모듈을 사용하여 React로 마이그레이션
  • 뷰로 마이그레이션

마지막으로 구성 요소는 의도한 대로 정확하게 보입니다.

Vue와 React로 작성한 Header 컴포넌트를 마이그레이션한 결과입니다. 할 일 목록은 여전히 ​​jQuery 애플리케이션입니다.
Vue와 React로 작성한 Header 컴포넌트를 마이그레이션한 결과입니다. 할 일 목록은 여전히 ​​jQuery 애플리케이션입니다. (큰 미리보기)

5.5. Alien 구성 요소의 고정 스타일 요약

지금까지 이 장에서 배운 내용을 요약할 수 있는 좋은 시간입니다. Alien 구성 요소의 스타일을 수정하기 위해 엄청난 작업을 수행해야 하는 것처럼 보일 수 있습니다. 그러나 모든 것이 요약됩니다.

  • React 또는 CSS 모듈의 styled-components와 Vue의 Scoped CSS로 구현된 번들 스타일을 수정하는 것은 Frankenstein 래퍼 또는 Webpack 구성에서 몇 줄로 간단합니다.
  • CSS 모듈로 구현된 스타일 수정은 css-loader 구성에서 한 줄로 시작합니다. 그 후 CSS 모듈은 전역 스타일시트로 처리됩니다.
  • 전역 스타일시트를 수정하려면 Webpack에서 insert 옵션을 사용하여 style-loader 패키지를 구성하고 래퍼 수명 주기의 적절한 순간에 스타일시트를 Shadow DOM으로 가져오도록 Frankenstein 래퍼를 업데이트해야 합니다.

결국, 우리는 호스트로 마이그레이션된 적절한 스타일의 Alien 구성 요소를 얻었습니다. 그러나 마이그레이션하는 프레임워크에 따라 귀찮게 할 수도 있고 그렇지 않을 수도 있는 한 가지가 있습니다.

좋은 소식이 먼저입니다. Vue로 마이그레이션하는 경우 데모가 제대로 작동하고 마이그레이션된 Vue 구성 요소에서 새 할 일 항목을 추가할 수 있어야 합니다. 그러나 React로 마이그레이션 하고 새 할 일 항목을 추가하려고 하면 성공하지 못할 것입니다. 새 항목을 추가해도 작동하지 않으며 목록에 항목이 추가되지 않습니다. 하지만 왜? 뭐가 문제 야? 편견은 없지만 React는 몇 가지에 대해 나름의 의견을 가지고 있습니다.

5.6. Shadow DOM의 React 및 JS 이벤트

React 문서가 무엇을 말하든 React는 웹 구성 요소에 그다지 친숙하지 않습니다. 문서에 있는 예제의 단순성은 비판을 받지 않으며 Web Component에서 링크를 렌더링하는 것보다 더 복잡한 것은 약간의 연구와 조사가 필요합니다.

Alien 구성 요소의 스타일을 수정하는 동안 보았듯이 웹 구성 요소에 거의 기본적으로 들어맞는 Vue와 달리 React는 웹 구성 요소가 준비되어 있지 않습니다. 지금은 웹 구성 요소 내에서 최소한 React 구성 요소를 보기 좋게 만드는 방법을 이해하고 있지만 수정해야 할 기능과 JavaScript 이벤트도 있습니다.

간단히 말해서 Shadow DOM은 이벤트를 캡슐화하고 대상을 변경하는 반면 React는 기본적으로 Shadow DOM의 이러한 동작을 지원하지 않으므로 Shadow DOM 내에서 발생하는 이벤트를 포착하지 않습니다. 이 동작에 대한 더 깊은 이유가 있으며 더 자세한 내용과 토론에 대해 자세히 알아보려면 React의 버그 추적기에서 미해결 문제가 있습니다.

운 좋게도 똑똑한 사람들이 우리를 위해 솔루션을 준비했습니다. @josephnvu는 솔루션의 기반을 제공했으며 Lukas Bombach는 이를 react-shadow-dom-retarget-events npm 모듈로 변환했습니다. 따라서 패키지를 설치하고 패키지 페이지의 지침을 따르고 래퍼의 코드를 업데이트하면 Alien 구성 요소가 마술처럼 작동하기 시작할 수 있습니다.

 import retargetEvents from 'react-shadow-dom-retarget-events'; ... ReactDOM.render( ... ); retargetEvents(this.shadowRoot);

더 나은 성능을 원하면 패키지의 로컬 복사본을 만들고(MIT 라이선스에서 허용) Frankenstein Demo 저장소에서 수행되는 것처럼 수신할 이벤트 수를 제한할 수 있습니다. 이 예의 경우 대상을 변경하고 해당 이벤트만 지정해야 하는 이벤트를 알고 있습니다.

이것으로 우리는 마침내 (긴 과정이었다는 것을 압니다) 첫 번째 스타일의 완전한 기능의 Alien 구성 요소의 적절한 마이그레이션이 완료되었습니다. 자신에게 좋은 음료를 가져옵니다. 당신은 그럴 자격이 있습니다!

6. 모든 구성 요소를 헹구고 반복하십시오.

첫 번째 구성 요소를 마이그레이션한 후 모든 구성 요소에 대해 프로세스를 반복해야 합니다. 그러나 Frankenstein Demo의 경우 할 일 목록을 렌더링하는 역할을 하는 하나만 남았습니다.

새 구성 요소를 위한 새 래퍼

새 래퍼를 추가하는 것으로 시작하겠습니다. 위에서 논의한 명명 규칙에 따라(React 구성 요소는 MainSection.js 라고 하므로) React로 마이그레이션할 때 해당 래퍼는 MainSection-wrapper.js 라고 해야 합니다. 동시에 Vue의 유사한 구성 요소를 Listing.vue 라고 하므로 Vue로 마이그레이션할 때 해당 래퍼를 Listing-wrapper.js 라고 해야 합니다. 그러나 명명 규칙에 관계없이 래퍼 자체는 이미 가지고 있는 것과 거의 동일합니다.

  • React 목록용 래퍼
  • Vue 목록을 위한 래퍼

React 애플리케이션의 이 두 번째 구성 요소에서 소개하는 흥미로운 것이 한 가지 있습니다. 때로는 그 또는 다른 이유로 구성 요소에서 일부 jQuery 플러그인을 사용하고 싶을 수 있습니다. React 구성 요소의 경우 두 가지를 도입했습니다.

  • jQuery를 사용하는 Bootstrap의 툴팁 플러그인,
  • .addClass().removeClass() ) 와 같은 CSS 클래스에 대한 토글.

    참고 : 이 jQuery를 사용하여 클래스를 추가/제거하는 것은 순전히 예시입니다. 실제 프로젝트에서 이 시나리오에 jQuery를 사용하지 마십시오. 대신 일반 JavaScript에 의존하십시오.

물론 jQuery에서 마이그레이션할 때 Alien 구성 요소에 jQuery를 도입하는 것이 이상하게 보일 수 있지만 이 예제의 호스트는 호스트와 다를 수 있습니다. AngularJS 또는 다른 곳에서 마이그레이션할 수 있습니다. 또한 구성 요소의 jQuery 기능과 전역 jQuery가 반드시 동일한 것은 아닙니다.

그러나 문제는 구성 요소가 Alien 애플리케이션의 컨텍스트에서 제대로 작동하는지 확인하더라도 Shadow DOM에 넣을 때 jQuery 플러그인 및 jQuery에 의존하는 기타 코드가 작동하지 않는다는 것입니다.

섀도우 DOM의 jQuery

임의의 jQuery 플러그인의 일반적인 초기화를 살펴보겠습니다.

 $('.my-selector').fancyPlugin();

이런 식으로 .my-selector 가 있는 모든 요소는 fancyPlugin 에 의해 처리됩니다. 이 초기화 형식은 .my-selector 가 전역 DOM에 있다고 가정합니다. 그러나 스타일과 마찬가지로 이러한 요소가 Shadow DOM에 삽입되면 그림자 경계로 인해 jQuery가 해당 요소에 몰래 들어가는 것을 방지할 수 있습니다. 결과적으로 jQuery는 Shadow DOM 내에서 요소를 찾을 수 없습니다.

해결책은 jQuery가 검색할 루트 요소를 정의하는 선택기에 두 번째 매개변수를 선택적으로 제공하는 것입니다. 그리고 이것은 우리의 shadowRoot 를 제공할 수 있는 곳입니다.

 $('.my-selector', this.shadowRoot).fancyPlugin();

이렇게 하면 jQuery 선택기와 결과적으로 플러그인이 제대로 작동합니다.

Alien 구성 요소는 Shadow DOM이 없는 Alien과 Shadow DOM 내의 Host 모두에서 사용하도록 되어 있음을 명심하십시오. 따라서 기본적으로 Shadow DOM의 존재를 가정하지 않는 보다 통합된 솔루션이 필요합니다.

React 애플리케이션에서 MainSection 구성 요소를 분석하면 documentRoot 속성을 설정한다는 것을 알 수 있습니다.

 ... this.documentRoot = this.props.root? this.props.root: document; ...

그래서 우리는 전달된 root 속성을 확인하고 존재한다면 이것을 documentRoot 로 사용합니다. 그렇지 않으면 document 로 돌아갑니다.

다음은 이 속성을 사용하는 툴팁 플러그인의 초기화입니다.

 $('[data-toggle="tooltip"]', this.documentRoot).tooltip({ container: this.props.root || 'body' });

보너스로 동일한 root 속성을 사용하여 이 경우 도구 설명을 삽입하기 위한 컨테이너를 정의합니다.

이제 Alien 구성 요소가 root 속성을 수락할 준비가 되면 해당 Frankenstein 래퍼에서 구성 요소의 렌더링을 업데이트합니다.

 // `appWrapper` is the root element within wrapper's Shadow DOM. ReactDOM.render(<MainApp root={ appWrapper } />, appWrapper);

그리고 그게 다야! 구성 요소는 전역 DOM에서와 마찬가지로 Shadow DOM에서도 잘 작동합니다.

다중 래퍼 시나리오에 대한 Webpack 구성

흥미로운 부분은 여러 래퍼를 사용할 때 Webpack의 구성에서 발생합니다. Vue 구성 요소의 CSS 모듈 또는 React의 styled-components와 같은 번들 스타일에 대한 변경 사항은 없습니다. 그러나 이제 글로벌 스타일에 약간의 비틀림이 있어야 합니다.

style-loader (전역 스타일시트를 올바른 Shadow DOM에 주입하는 역할을 담당)는 insert 옵션에 대해 한 번에 하나의 선택기만 사용하기 때문에 유연하지 않다고 말했습니다. 이것은 Webpack이 아닌 다른 번들러에 있는 경우 oneOf 규칙 또는 이와 유사한 것을 사용하여 래퍼당 하나의 하위 규칙을 갖도록 Webpack의 .css 규칙을 분할해야 함을 의미합니다.

항상 예를 ​​들어 설명하는 것이 더 쉽기 때문에 이번에는 마이그레이션에서 Vue로의 마이그레이션에 대해 이야기해 보겠습니다(하지만 React로 마이그레이션하는 것은 거의 동일합니다).

 ... oneOf: [ { issuer: /Header/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }, ... ] }, { issuer: /Listing/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-listing-wrapper' } }, ... ] }, ] ...

구성이 모든 경우에 동일하므로 css-loader 를 제외했습니다. 대신 style-loader 에 대해 이야기합시다. 이 구성에서 우리는 해당 스타일시트를 요청하는 파일 이름에 따라 * *-header-* 또는 *-listing-*<style> 태그를 삽입합니다(Webpack의 issuer 규칙). 그러나 Alien 구성 요소를 렌더링하는 데 필요한 전역 스타일시트는 다음 두 위치에서 가져올 수 있음을 기억해야 합니다.

  • Alien 구성 요소 자체,
  • 프랑켄슈타인 래퍼.

그리고 여기서 Alien 구성 요소의 이름과 해당 래퍼가 일치할 때 위에서 설명한 래퍼의 명명 규칙을 이해해야 합니다. 예를 들어 Header.vue 라는 Vue 구성 요소에서 가져온 스타일시트가 있는 경우 *-header-* 래퍼를 수정합니다. 동시에 래퍼에서 스타일시트를 가져오는 경우 구성 변경 없이 래퍼가 Header-wrapper.js 라고 하는 경우 해당 스타일시트는 정확히 동일한 규칙을 따릅니다. Listing.vue 구성 요소와 해당 래퍼 Listing-wrapper.js 도 마찬가지입니다. 이 명명 규칙을 사용하여 번들러의 구성을 줄입니다.

모든 구성 요소가 마이그레이션되면 마이그레이션의 마지막 단계를 수행할 차례입니다.

7. 외계인으로 전환

어느 시점에서 마이그레이션의 맨 처음 단계에서 식별한 구성 요소가 모두 Frankenstein 래퍼로 대체되었음을 알게 됩니다. jQuery 응용 프로그램은 실제로 남아 있지 않으며 본질적으로 호스트 수단을 사용하여 함께 붙어 있는 Alien 응용 프로그램이 있습니다.

예를 들어 jQuery 애플리케이션에서 index.html 의 콘텐츠 부분은 두 마이크로서비스를 모두 마이그레이션한 후 다음과 같이 보입니다.

 <section class="todoapp"> <frankenstein-header-wrapper></frankenstein-header-wrapper> <frankenstein-listing-wrapper></frankenstein-listing-wrapper> </section>

이 순간에 jQuery 애플리케이션을 계속 유지하는 것은 의미가 없습니다. 대신 Vue 애플리케이션으로 전환하고 모든 래퍼, Shadow DOM 및 멋진 Webpack 구성을 잊어버려야 합니다. 이를 위해 우아한 솔루션이 있습니다.

HTTP 요청에 대해 이야기합시다. 여기서 Apache 구성에 대해 언급하겠지만 이것은 구현 세부사항일 뿐입니다. Nginx에서 전환을 수행하거나 다른 모든 작업은 Apache에서처럼 간단해야 합니다.

서버의 /var/www/html 폴더에서 사이트를 제공한다고 상상해 보십시오. 이 경우 httpd.conf 또는 httpd-vhost.conf 에 다음과 같이 해당 폴더를 가리키는 항목이 있어야 합니다.

 DocumentRoot "/var/www/html"

Frankenstein을 jQuery에서 React로 마이그레이션한 후 애플리케이션을 전환하려면 DocumentRoot 항목을 다음과 같이 업데이트하기만 하면 됩니다.

 DocumentRoot "/var/www/html/react/build"

Alien 애플리케이션을 빌드하고 서버를 다시 시작하면 애플리케이션이 Alien의 폴더에서 직접 제공됩니다. React 애플리케이션은 react/ 폴더에서 제공됩니다. 그러나 Vue나 마이그레이션한 다른 프레임워크에서도 마찬가지입니다. 이것이 이 단계에서 Alien이 호스트가 되기 때문에 Host와 Alien을 어느 시점에서든 완전히 독립적이고 기능적으로 유지하는 것이 매우 중요한 이유입니다.

이제 모든 Shadow DOM, Frankenstein 래퍼 및 기타 마이그레이션 관련 아티팩트를 포함하여 Alien 폴더 주변의 모든 것을 안전하게 제거할 수 있습니다. 순간적으로는 험난한 여정이었지만 사이트를 마이그레이션했습니다. 축하합니다!

결론

우리는 확실히 이 기사에서 다소 거친 지형을 거쳤습니다. 그러나 jQuery 애플리케이션으로 시작한 후 Vue와 React로 마이그레이션할 수 있었습니다. 그 과정에서 예상치 못한 몇 가지 사소한 문제를 발견했습니다. 스타일 지정, JavaScript 기능 수정, 번들러 구성 도입 등의 문제를 수정해야 했습니다. 그러나 실제 프로젝트에서 기대할 수 있는 것에 대한 더 나은 개요를 제공했습니다. 결국 우리는 마이그레이션이 진행되는 동안 최종 결과에 대해 회의적일 수 있는 모든 권리가 있었음에도 jQuery 애플리케이션에서 남아 있는 비트 없이 현대적인 애플리케이션을 갖게 되었습니다.

Alien으로 전환한 후 Frankenstein은 은퇴할 수 있습니다.
Alien으로 전환한 후 Frankenstein은 은퇴할 수 있습니다. (큰 미리보기)

Frankenstein Migration은 만병통치약이 아니며 무서운 과정도 아닙니다. 예측 가능한 방식으로 프로젝트를 새롭고 강력한 것으로 변환하는 데 도움이 되는 많은 프로젝트에 적용할 수 있는 정의된 알고리즘일 뿐입니다.