웹 구성요소에서 Lit 요소로

1. 소개

최종 업데이트: 2021년 8월 10일

웹 구성요소

웹 구성요소는 웹페이지 및 웹 앱에서 사용할 재사용 가능한 캡슐화된 맞춤 HTML 태그를 새로 생성할 수 있게 해 주는 일련의 웹 플랫폼 API입니다. 웹 구성요소 표준에 따라 빌드된 맞춤 구성요소 및 위젯은 최신 브라우저에서 작동하며 HTML과 호환되는 모든 JavaScript 라이브러리 또는 프레임워크와 함께 사용할 수 있습니다.

Lit란 무엇인가요?

Lit는 모든 프레임워크에서 또는 프레임워크가 전혀 없는 빠르고 가벼운 웹 구성요소를 빌드하기 위한 간단한 라이브러리입니다. Lit를 사용하면 공유 가능한 구성요소, 애플리케이션, 디자인 시스템 등을 빌드할 수 있습니다.

Lit는 속성, 속성, 렌더링 관리와 같은 일반적인 웹 구성요소 작업을 단순화하는 API를 제공합니다.

학습할 내용

  • 웹 구성 요소란 무엇인가요?
  • 웹 구성요소의 개념
  • 웹 구성 요소를 빌드하는 방법
  • lit-html 및 LitElement란 무엇인가요?
  • Lit가 웹 구성요소를 기반으로 하는 작업

빌드할 항목

  • 기본적인 좋아요 / 싫어요 웹 구성요소
  • 좋아요 / 싫어요 Lit 기반 웹 구성요소

필요한 항목

  • 업데이트된 모든 최신 브라우저 (Chrome, Safari, Firefox, Chromium Edge) 웹 구성요소는 모든 최신 브라우저에서 작동하며 폴리필은 Microsoft Internet Explorer 11 및 Chrome이 아닌 Microsoft Edge에서 사용할 수 있습니다.
  • HTML, CSS, JavaScript, Chrome DevTools에 관한 지식

2. 설정 및 플레이그라운드 둘러보기

코드 액세스

Codelab 전체에 다음과 같은 Lit 플레이그라운드 링크가 있습니다.

플레이그라운드는 브라우저에서 완전히 실행되는 코드 샌드박스입니다. TypeScript 파일과 JavaScript 파일을 컴파일하고 실행할 수 있으며 노드 모듈에 대한 가져오기를 자동으로 해결할 수도 있습니다. 예:

// before
import './my-file.js';
import 'lit';

// after
import './my-file.js';
import 'https://unpkg.com/lit?module';

이 체크포인트를 시작점으로 사용하여 Lit 플레이그라운드에서 전체 튜토리얼을 실행할 수 있습니다. VS Code를 사용하는 경우 이 체크포인트를 사용하여 모든 단계의 시작 코드를 다운로드하고 작업을 확인하는 데 사용할 수 있습니다.

Lit 플레이그라운드 UI 살펴보기

파일 선택기 탭 표시줄에 섹션 1, 코드 수정 섹션은 섹션 2, 출력 미리보기는 섹션 3, 미리보기 새로고침 버튼은 섹션 4로 라벨이 지정됩니다.

Lit 플레이그라운드 UI 스크린샷은 이 Codelab에서 사용할 섹션을 강조 표시합니다.

  1. 파일 선택기 더하기 버튼
  2. 파일 편집기
  3. 코드 미리보기
  4. 새로고침 버튼
  5. 다운로드 버튼

VS Code 설정 (고급)

이 VS Code 설정을 사용하면 다음과 같은 이점이 있습니다.

  • 템플릿 유형 확인
  • 템플릿 intellisense 및 자동 완성

NPM, VS Code (lit-plugin 플러그인 포함)가 이미 설치되어 있고 해당 환경을 사용하는 방법을 알고 있는 경우 다음을 수행하여 프로젝트를 다운로드하고 시작할 수 있습니다.

  • 다운로드 버튼 누르기
  • tar 파일의 콘텐츠를 디렉터리로 추출하기
  • 베어 모듈 지정자를 해결할 수 있는 개발 서버를 설치합니다 (Lit팀은 @web/dev-server를 권장함).
  • 개발 서버를 실행하고 브라우저를 엽니다 (@web/dev-server를 사용하는 경우 npx web-dev-server --node-resolve --watch --open를 사용할 수 있음).
    • package.json 예시를 사용하는 경우 npm run serve를 사용합니다.

3. 맞춤 요소 정의

맞춤 요소

웹 구성요소는 4가지 네이티브 웹 API의 모음입니다. 각 필터는 다음과 같습니다.

  • ES 모듈
  • 맞춤 요소
  • 그림자 DOM
  • HTML 템플릿

이미 ES 모듈 사양을 사용해 보았습니다. 이를 사용하면 가져오기 및 내보내기가 포함된 JavaScript 모듈을 생성할 수 있고 <script type="module">로 페이지에 로드되는 가져오기 기능을 만들 수 있습니다.

커스텀 요소 정의

맞춤 요소 사양을 통해 사용자는 JavaScript를 사용하여 자신의 HTML 요소를 정의할 수 있습니다. 기본 브라우저 요소와 구분하기 위해 이름에는 하이픈 (-)을 포함해야 합니다. index.js 파일을 지우고 맞춤 요소 클래스를 정의합니다.

index.js

class RatingElement extends HTMLElement {}

customElements.define('rating-element', RatingElement);

맞춤 요소는 HTMLElement를 확장하는 클래스를 하이픈으로 연결된 태그 이름으로 연결하여 정의됩니다. customElements.define 호출은 RatingElement 클래스를 tagName ‘rating-element'과 연결하도록 브라우저에 지시합니다. 즉, 문서에서 이름이 <rating-element>인 모든 요소가 이 클래스와 연결됩니다.

문서 본문에 <rating-element>를 배치하고 어떻게 렌더링되는지 확인합니다.

index.html

<body>
 <rating-element></rating-element>
</body>

이제 출력을 보면 렌더링된 것이 없음을 알 수 있습니다. 이는 브라우저에 <rating-element>를 렌더링하는 방법을 알려주지 않았기 때문입니다. Chrome 개발자 도구에서 <rating-element>를 선택하여 맞춤 요소 정의가 성공했는지 확인할 수 있습니다. 요소 선택기를 추가하고 콘솔에서 다음을 호출합니다.

$0.constructor

다음과 같은 결과가 출력됩니다.

class RatingElement extends HTMLElement {}

맞춤 요소 수명 주기

맞춤 요소에는 일련의 수명 주기 후크가 함께 제공됩니다. 각 필터는 다음과 같습니다.

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

constructor는 요소가 처음 생성될 때 호출됩니다(예: document.createElement(‘rating-element') 또는 new RatingElement() 호출). 생성자는 요소를 설정하기에 좋은 위치이지만 일반적으로 요소 'boot-up'을 위해 생성자에서 DOM 조작을 실행하는 것은 좋지 않은 사례로 간주됩니다. 발생할 수 있습니다

connectedCallback는 맞춤 요소가 DOM에 연결될 때 호출됩니다. 일반적으로 여기서 초기 DOM 조작이 발생합니다.

disconnectedCallback는 맞춤 요소가 DOM에서 삭제된 후 호출됩니다.

attributeChangedCallback(attrName, oldValue, newValue)는 사용자 지정 속성이 변경되면 호출됩니다.

adoptedCallback는 맞춤 요소가 HTMLTemplateElement에서와 같이 adoptNode를 통해 다른 documentFragment에서 기본 문서로 채택될 때 호출됩니다.

DOM 렌더링

이제 맞춤 요소로 돌아가서 일부 DOM을 이 요소와 연결합니다. 요소가 DOM에 연결될 때 요소의 콘텐츠를 설정합니다.

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   this.innerHTML = `
     <style>
       rating-element {
         display: inline-flex;
         align-items: center;
       }
       rating-element button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

constructor에서 요소에 rating라는 인스턴스 속성을 저장합니다. connectedCallback에서 <rating-element>에 DOM 하위 요소를 추가하여 좋아요 및 싫어요 버튼과 함께 현재 평점을 표시합니다.

4. 그림자 DOM

Shadow DOM을 사용해야 하는 이유

이전 단계에서 삽입한 스타일 태그의 선택기가 페이지의 평가 요소 및 모든 버튼을 선택하는 것을 확인할 수 있습니다. 이로 인해 스타일이 요소 밖으로 누출되고 스타일을 지정하지 않으려는 다른 노드가 선택될 수 있습니다. 또한 이 맞춤 요소 이외의 다른 스타일로 인해 의도치 않게 맞춤 요소 내부의 노드 스타일이 지정될 수 있습니다. 예를 들어, 기본 문서의 헤드에 스타일 태그를 추가해 보세요.

index.html

<!DOCTYPE html>
<html>
 <head>
   <script src="./index.js" type="module"></script>
   <style>
     span {
       border: 1px solid red;
     }
   </style>
 </head>
 <body>
   <rating-element></rating-element>
 </body>
</html>

출력에는 평점 범위 주위에 빨간색 테두리 상자가 표시됩니다. 이는 사소한 경우이지만 DOM 캡슐화의 부족으로 인해 더 복잡한 애플리케이션에서는 더 큰 문제가 발생할 수 있습니다. 바로 이 부분에서 Shadow DOM이 사용됩니다.

섀도우 루트 연결

요소에 그림자 루트를 연결하고 루트 내부의 DOM을 렌더링합니다.

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});

   shadowRoot.innerHTML = `
     <style>
       :host {
         display: inline-flex;
         align-items: center;
       }
       button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

페이지를 새로고침하면 기본 문서의 스타일에서 더 이상 섀도우 루트 내의 노드를 선택할 수 없습니다.

동기화를 어떻게 시작했나요? connectedCallback에서 섀도우 루트를 요소에 연결하는 this.attachShadow를 호출했습니다. open 모드는 섀도우 콘텐츠를 검사할 수 있으며 this.shadowRoot를 통해서도 섀도우 루트에 액세스할 수 있도록 합니다. Chrome Inspector에서 웹 구성요소도 살펴보세요.

Chrome 검사기의 DOM 트리 여기에는 <rating-element> a#shadow-root (open)가 하위 요소로, 이전 DOM이 해당 shadowroot 내부의 DOM으로 포함되어 있습니다.

이제 콘텐츠가 포함된 확장 가능한 섀도우 루트가 표시됩니다. 이 섀도우 루트 안의 모든 항목을 Shadow DOM이라고 합니다. Chrome 개발자 도구에서 평점 요소를 선택하고 $0.children를 호출하면 하위 요소를 반환하지 않는 것을 확인할 수 있습니다. 이는 Shadow DOM이 직접 하위 요소와 동일한 DOM 트리의 일부가 아니라 섀도우 트리로 간주되기 때문입니다.

Light DOM

실험: 노드를 <rating-element>의 직계 하위 요소로 추가합니다.

index.html

<rating-element>
 <div>
   This is the light DOM!
 </div>
</rating-element>

페이지를 새로고침하면 이 맞춤 요소의 Light DOM에 있는 새 DOM 노드가 페이지에 표시되지 않습니다. 이는 Shadow DOM에 Light DOM 노드가 <slot> 요소를 통해 Shadow DOM에 프로젝션되는 방식을 제어하는 기능이 있기 때문입니다.

5. HTML 템플릿

템플릿을 사용해야 하는 이유

제거 없이 innerHTML 및 템플릿 리터럴 문자열을 사용하면 스크립트 삽입과 관련된 보안 문제가 발생할 수 있습니다. 이전에는 DocumentFragment를 사용하는 메서드가 포함되었지만, 템플릿이 정의될 때 이미지 로드 및 스크립트 실행과 같은 다른 문제가 수반될 뿐만 아니라 재사용성에 장애물이 되기도 합니다. 이때 <template> 요소가 사용됩니다. 템플릿은 비활성 DOM, 노드를 복제하는 고성능 메서드, 재사용 가능한 템플릿을 제공합니다.

템플릿 사용

그런 다음 HTML 템플릿을 사용하도록 구성요소를 전환합니다.

index.html

<body>
 <template id="rating-element-template">
   <style>
     :host {
       display: inline-flex;
       align-items: center;
     }
     button {
       background: transparent;
       border: none;
       cursor: pointer;
     }
   </style>
   <button class="thumb_down" >
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
   </button>
   <span class="rating"></span>
   <button class="thumb_up">
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
   </button>
 </template>

 <rating-element>
   <div>
     This is the light DOM!
   </div>
 </rating-element>
</body>

여기서는 DOM 콘텐츠를 기본 문서의 DOM에 있는 템플릿 태그로 이동했습니다. 이제 맞춤 요소 정의를 리팩터링합니다.

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});
   const templateContent = document.getElementById('rating-element-template').content;
   const clonedContent = templateContent.cloneNode(true);
   shadowRoot.appendChild(clonedContent);

   this.shadowRoot.querySelector('.rating').innerText = this.rating;
 }
}

customElements.define('rating-element', RatingElement);

이 템플릿 요소를 사용하려면 템플릿을 쿼리하고 콘텐츠를 가져온 후 templateContent.cloneNode로 노드를 클론합니다. 여기서 true 인수가 딥 클론을 수행합니다. 그런 다음 데이터를 사용하여 DOM을 초기화합니다.

축하합니다. 이제 웹 구성 요소가 준비되었습니다. 안타깝게도 아직 아무 작업도 하지 않으므로 다음으로 몇 가지 기능을 추가합니다.

6. 기능 추가

속성 결합

현재 평점 요소에서 평점을 설정하는 유일한 방법은 요소를 구성하고 객체에서 rating 속성을 설정한 다음 페이지에 배치하는 것입니다. 안타깝게도 네이티브 HTML 요소가 작동하는 방식이 아닙니다. 기본 HTML 요소는 속성 및 속성 변경으로 업데이트되는 경향이 있습니다.

다음 줄을 추가하여 rating 속성이 변경될 때 맞춤 요소가 뷰를 업데이트하도록 합니다.

index.js

constructor() {
  super();
  this._rating = 0;
}

set rating(value) {
  this._rating = value;
  if (!this.shadowRoot) {
    return;
  }

  const ratingEl = this.shadowRoot.querySelector('.rating');
  if (ratingEl) {
    ratingEl.innerText = this._rating;
  }
}

get rating() {
  return this._rating;
}

평점 속성에 setter 및 getter를 추가한 다음 평점 요소의 텍스트가 있는 경우 이를 업데이트합니다. 즉, 요소에 평점 속성을 설정하면 뷰가 업데이트됩니다. 개발자 도구 콘솔에서 간단히 테스트해 보세요.

속성 결합

이제 속성이 변경되면 뷰를 업데이트합니다. 이는 <input value="newValue">를 설정할 때 뷰를 업데이트하는 입력과 유사합니다. 다행히 웹 구성요소 수명 주기에는 attributeChangedCallback가 포함되어 있습니다. 다음 줄을 추가하여 평점을 업데이트합니다.

index.js

static get observedAttributes() {
 return ['rating'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
 if (attributeName === 'rating') {
   const newRating = Number(newValue);
   this.rating = newRating;
 }
}

attributeChangedCallback를 트리거하려면 RatingElement.observedAttributes which defines the attributes to be observed for changes에 정적 getter를 설정해야 합니다. 그런 다음 DOM에서 선언적으로 평점을 설정합니다. 한번 사용해 보세요.

index.html

<rating-element rating="5"></rating-element>

이제 평점이 선언적으로 업데이트됩니다.

버튼 기능

이제 버튼 기능만 남았습니다. 이 구성요소의 동작을 통해 사용자는 하나의 찬성 또는 반대 투표 평가를 제공하고 사용자에게 시각적 피드백을 제공할 수 있어야 합니다. 일부 이벤트 리스너 및 반영 속성을 사용하여 이를 구현할 수 있지만, 먼저 다음 줄을 추가하여 스타일을 업데이트하여 시각적 피드백을 제공합니다.

index.html

<style>
...

 :host([vote=up]) .thumb_up {
   fill: green;
 }
  :host([vote=down]) .thumb_down {
   fill: red;
 }
</style>

Shadow DOM에서 :host 선택기는 섀도우 루트가 연결된 노드 또는 커스텀 요소를 참조합니다. 이 경우 vote 속성이 "up"이면 좋아요 버튼이 녹색으로 바뀌지만 vote"down", then it will turn the thumb-down button red이면 좋아요. 이제 rating를 구현한 방법과 유사하게 vote의 반영 속성 / 속성을 만들어 이를 위한 로직을 구현합니다. 속성 setter와 getter로 시작합니다.

index.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }
  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }
  this._vote = newValue;
  this.setAttribute('vote', newValue);
}

get vote() {
  return this._vote;
}

constructor에서 null를 사용하여 _vote 인스턴스 속성을 초기화하고 setter에서 새 값이 다른지 확인합니다. 이 경우 평점을 적절하게 조정하고 중요한 것은 this.setAttribute를 사용하여 vote 속성을 호스트에 다시 반영하는 것입니다.

다음으로 속성 결합을 설정합니다.

index.js

static get observedAttributes() {
  return ['rating', 'vote'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
  if (attributeName === 'rating') {
    const newRating = Number(newValue);

    this.rating = newRating;
  } else if (attributeName === 'vote') {
    this.vote = newValue;
  }
}

다시 말하지만, 이는 rating 속성 결합에서 사용한 프로세스와 동일합니다. observedAttributesvote를 추가하고 attributeChangedCallback에서 vote 속성을 설정합니다. 이제 마지막으로 클릭 이벤트 리스너를 추가하여 버튼 기능을 제공합니다.

index.js

constructor() {
 super();
 this._rating = 0;
 this._vote = null;
 this._boundOnUpClick = this._onUpClick.bind(this);
 this._boundOnDownClick = this._onDownClick.bind(this);
}

connectedCallback() {
  ...
  this.shadowRoot.querySelector('.thumb_up')
    .addEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .addEventListener('click', this._boundOnDownClick);
}

disconnectedCallback() {
  this.shadowRoot.querySelector('.thumb_up')
    .removeEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .removeEventListener('click', this._boundOnDownClick);
}

_onUpClick() {
  this.vote = 'up';
}

_onDownClick() {
  this.vote = 'down';
}

constructor에서는 일부 클릭 리스너를 요소에 바인딩하고 참조를 유지합니다. connectedCallback에서 버튼의 클릭 이벤트를 수신 대기합니다. disconnectedCallback에서 이러한 리스너를 정리하고 클릭 리스너 자체에서 vote를 적절하게 설정합니다.

축하합니다. 이제 모든 기능을 갖춘 웹 구성 요소가 완성되었습니다. 버튼을 클릭해 보세요. 이제 문제는 JS 파일이 96줄, HTML 파일은 43줄에 도달한다는 것입니다. 간단한 구성요소의 경우 코드가 매우 장황하고 필수적입니다. Google의 Lit 프로젝트가 필요한 이유가 바로 여기에 있습니다.

7. Lit-HTML

코드 체크포인트

lit-html을 사용해야 하는 이유

무엇보다도 <template> 태그는 유용하고 성능이 우수하지만 구성요소의 로직으로 패키징되지 않으므로 나머지 로직과 함께 템플릿을 배포하기 어렵습니다. 또한 템플릿 요소가 기본적으로 사용되는 방식은 명령형 코드에도 적용됩니다. 따라서 대부분의 경우 선언적 코딩 패턴에 비해 코드 가독성이 떨어집니다.

여기에서 lit-html을 사용할 수 있습니다. Lit html은 JavaScript로 HTML 템플릿을 작성한 다음 이러한 템플릿을 데이터와 함께 효율적으로 렌더링하고 다시 렌더링하여 DOM을 만들고 업데이트할 수 있는 Lit의 렌더링 시스템입니다. 널리 사용되는 JSX 및 VDOM 라이브러리와 유사하지만 기본적으로 브라우저에서 실행되며 대부분의 경우 훨씬 더 효율적으로 실행됩니다.

Lit HTML 사용

다음으로, 네이티브 웹 구성요소 rating-element를 이전하여 특수 문법이 있는 템플릿 문자열을 인수로 사용하는 함수인 태그된 템플릿 리터럴을 사용하는 Lit 템플릿을 사용합니다. 그런 다음 Lit는 내부적으로 템플릿 요소를 사용하여 빠른 렌더링과 보안을 위한 일부 정리 기능을 제공합니다. 먼저 웹 구성요소에 render() 메서드를 추가하여 index.html<template>를 Lit 템플릿으로 이전합니다.

index.js

// Dont forget to import from Lit!
import {render, html} from 'lit';

class RatingElement extends HTMLElement {
  ...
  render() {
    if (!this.shadowRoot) {
      return;
    }

    const template = html`
      <style>
        :host {
          display: inline-flex;
          align-items: center;
        }
        button {
          background: transparent;
          border: none;
          cursor: pointer;
        }

       :host([vote=up]) .thumb_up {
         fill: green;
       }

       :host([vote=down]) .thumb_down {
         fill: red;
       }
      </style>
      <button class="thumb_down">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
      </button>
      <span class="rating">${this.rating}</span>
      <button class="thumb_up">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
      </button>`;

    render(template, this.shadowRoot);
  }
}

index.html에서 템플릿을 삭제할 수도 있습니다. 이 렌더링 메서드에서 template라는 변수를 정의하고 html 태그가 지정된 템플릿 리터럴 함수를 호출합니다. 또한 ${...}의 템플릿 리터럴 보간 구문을 사용하여 span.rating 요소 내에서 간단한 데이터 결합을 실행했습니다. 즉, 결국에는 더 이상 노드를 명령적으로 업데이트할 필요가 없게 됩니다. 또한 템플릿을 섀도 루트에 동기식으로 렌더링하는 lit render 메서드를 호출합니다.

선언적 구문으로 이전

이제 <template> 요소를 제거했으므로 새로 정의된 render 메서드를 대신 호출하도록 코드를 리팩터링합니다. 먼저 lit의 이벤트 리스너 바인딩을 사용하여 리스너 코드를 삭제할 수 있습니다.

index.js

<button
    class="thumb_down"
    @click=${() => {this.vote = 'down'}}>
...
<button
    class="thumb_up"
    @click=${() => {this.vote = 'up'}}>

Lit 템플릿은 @EVENT_NAME 결합 문법을 사용하여 노드에 이벤트 리스너를 추가할 수 있습니다. 이 경우 버튼을 클릭할 때마다 vote 속성을 업데이트합니다.

그런 다음 constructorconnectedCallback, disconnectedCallback에서 이벤트 리스너 초기화 코드를 삭제합니다.

index.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

connectedCallback() {
  this.attachShadow({mode: 'open'});
  this.render();
}

// remove disonnectedCallback and _onUpClick and _onDownClick

세 가지 콜백에서 모두 클릭 리스너 로직을 삭제할 수 있었으며 disconnectedCallback도 완전히 삭제할 수도 있었습니다. connectedCallback에서 모든 DOM 초기화 코드를 삭제하여 훨씬 더 세련되게 보일 수도 있었습니다. 즉, _onUpClick_onDownClick 리스너 메서드를 제거할 수도 있습니다.

마지막으로, 속성 또는 속성이 변경될 때 DOM이 업데이트될 수 있도록 render 메서드를 활용하도록 속성 setter를 업데이트합니다.

index.js

set rating(value) {
  this._rating = value;
  this.render();
}

...

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }

  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }

  this._vote = newValue;
  this.setAttribute('vote', newValue);
  // add render method
  this.render();
}

여기서는 rating setter에서 dom 업데이트 로직을 삭제할 수 있었으며 vote setter에서 render 호출을 추가했습니다. 이제 결합과 이벤트 리스너가 적용되는 위치를 확인할 수 있기 때문에 템플릿을 훨씬 더 쉽게 읽을 수 있습니다.

페이지를 새로고침하면 찬성 투표를 눌렀을 때 다음과 같이 작동하는 평가 버튼이 표시됩니다.

값이 6이고 엄지 손가락 색상이 녹색인 좋아요 및 싫어요 평가 슬라이더

8. LitElement

LitElement를 사용해야 하는 이유

코드에 여전히 문제가 있습니다. 첫째, vote 속성이나 속성을 변경하면 rating 속성이 변경되어 render가 두 번 호출될 수 있습니다. 렌더링의 반복 호출은 본질적으로 노옵스(no-ops)이고 효율적이지만 JavaScript VM은 여전히 해당 함수를 동기식으로 두 번 호출하는 데 시간을 소비하고 있습니다. 둘째, 많은 상용구 코드가 필요하므로 새 속성과 속성을 추가하는 것이 지루합니다. LitElement가 필요한 때입니다.

LitElement는 프레임워크와 환경에서 사용할 수 있는 빠르고 가벼운 웹 구성요소를 만들기 위한 Lit의 기본 클래스입니다. 이제 LitElement를 사용하도록 구현을 변경하여 rating-element에서 LitElement가 할 수 있는 작업을 살펴보겠습니다.

LitElement 사용

먼저 lit 패키지에서 LitElement 기본 클래스를 가져와서 서브클래스로 만듭니다.

index.js

import {LitElement, html, css} from 'lit';

class RatingElement extends LitElement {
// remove connectedCallback()
...

rating-element의 새로운 기본 클래스인 LitElement를 가져옵니다. 그런 다음 html 가져오기를 유지하고 마지막으로 css를 만듭니다. 이를 통해 CSS 수학, 템플릿, 기타 기능을 내부적으로 정의할 수 있도록 CSS 태그된 템플릿 리터럴을 정의할 수 있습니다.

그런 다음 렌더링 메서드에서 Lit의 정적 스타일시트로 스타일을 이동합니다.

index.js

class RatingElement extends LitElement {
  static get styles() {
    return css`
      :host {
        display: inline-flex;
        align-items: center;
      }
      button {
        background: transparent;
        border: none;
        cursor: pointer;
      }

      :host([vote=up]) .thumb_up {
        fill: green;
      }

      :host([vote=down]) .thumb_down {
        fill: red;
      }
    `;
  }
 ...

대부분의 스타일이 Lit에 있는 위치입니다. Lit는 이러한 스타일을 가져와 구성 가능한 스타일시트와 같은 브라우저 기능을 사용하여 렌더링 시간을 단축할 뿐만 아니라 필요한 경우 이전 브라우저의 웹 구성요소 폴리필을 통해 전달합니다.

수명 주기

Lit는 네이티브 웹 구성요소 콜백 외에 일련의 렌더링 수명 주기 콜백 메서드를 도입합니다. 이러한 콜백은 선언된 Lit 속성이 변경될 때 트리거됩니다.

이 기능을 사용하려면 렌더링 수명 주기를 트리거할 속성을 정적으로 선언해야 합니다.

index.js

static get properties() {
  return {
    rating: {
      type: Number,
    },
    vote: {
      type: String,
      reflect: true,
    }
  };
}

// remove observedAttributes() and attributeChangedCallback()
// remove set rating() get rating()

여기서는 ratingvote가 LitElement 렌더링 수명 주기를 트리거하도록 정의하고 문자열 속성을 속성으로 변환하는 데 사용할 유형을 정의합니다.

<user-profile .name=${this.user.name} .age=${this.user.age}>
  ${this.user.family.map(member => html`
        <family-member
             .name=${member.name}
             .relation=${member.relation}>
        </family-member>`)}
</user-profile>

또한 vote 속성의 reflect 플래그는 vote setter에서 수동으로 트리거한 호스트 요소의 vote 속성을 자동으로 업데이트합니다.

이제 정적 속성 블록이 있으므로 모든 속성 및 속성 렌더링 업데이트 로직을 삭제할 수 있습니다. 즉, 다음 메서드를 삭제할 수 있습니다.

  • connectedCallback
  • observedAttributes
  • attributeChangedCallback
  • rating (setter 및 getter)
  • vote (setter와 getter이지만 setter의 변경 로직을 유지함)

constructor를 유지하고 새 willUpdate 수명 주기 메서드를 추가합니다.

index.js

constructor() {
  super();
  this.rating = 0;
  this.vote = null;
}

willUpdate(changedProps) {
  if (changedProps.has('vote')) {
    const newValue = this.vote;
    const oldValue = changedProps.get('vote');

    if (newValue === 'up') {
      if (oldValue === 'down') {
        this.rating += 2;
      } else {
        this.rating += 1;
      }
    } else if (newValue === 'down') {
      if (oldValue === 'up') {
        this.rating -= 2;
      } else {
        this.rating -= 1;
      }
    }
  }
}

// remove set vote() and get vote()

여기서는 ratingvote를 초기화하고 vote setter 로직을 willUpdate 수명 주기 메서드로 이동합니다. LitElement는 속성 변경사항을 일괄 처리하고 렌더링을 비동기식으로 만들기 때문에 업데이트 속성이 변경될 때마다 render 전에 willUpdate 메서드가 호출됩니다. willUpdate에서 반응형 속성 (예: this.rating)을 변경해도 불필요한 render 수명 주기 호출이 트리거되지 않습니다.

마지막으로 render는 Lit 템플릿을 반환해야 하는 LitElement 수명 주기 메서드입니다.

index.js

render() {
  return html`
    <button
        class="thumb_down"
        @click=${() => {this.vote = 'down'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
    </button>
    <span class="rating">${this.rating}</span>
    <button
        class="thumb_up"
        @click=${() => {this.vote = 'up'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
    </button>`;
}

더 이상 섀도 루트를 확인할 필요가 없으며 이전에 'lit' 패키지에서 가져온 render 함수를 더 이상 호출하지 않아도 됩니다.

이제 요소가 미리보기에서 렌더링됩니다. 클릭해 보세요.

9. 축하합니다

축하합니다. 웹 구성요소를 처음부터 성공적으로 빌드하고 LitElement로 발전시켰습니다.

Lit는 매우 작고(5kb 미만의 축소 및 gzip으로 처리됨), 매우 빠르고 코딩하기가 매우 재미있습니다. 다른 프레임워크에서 사용할 구성 요소를 만들거나 이를 사용하여 완전한 앱을 빌드할 수 있습니다.

지금까지 웹 구성요소가 무엇인지, 어떻게 빌드하는지, Lit를 통해 어떻게 웹 구성요소를 더 쉽게 빌드할 수 있는지 알아보았습니다.

코드 체크포인트

최종 코드와 Google의 코드를 비교해 보시겠어요? 여기에서 비교해 보세요.

다음 단계

다른 Codelab도 확인해 보세요.

추가 자료

커뮤니티