웹 구성요소에서 Lit 요소로

1. 소개

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

Web Components

웹 구성요소는 웹페이지와 웹 앱에서 사용할 수 있는 새로운 맞춤형 재사용 가능 캡슐화된 HTML 태그를 만들 수 있는 웹 플랫폼 API 집합입니다. 웹 구성요소 표준을 기반으로 빌드된 맞춤 구성요소와 위젯은 최신 브라우저에서 작동하며 HTML과 호환되는 JavaScript 라이브러리 또는 프레임워크와 함께 사용할 수 있습니다.

Lit란 무엇인가요?

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

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

학습할 내용

  • 웹 구성요소란 무엇인가요?
  • 웹 구성요소의 개념
  • 웹 구성요소 빌드 방법
  • lit-html 및 LitElement란 무엇인가요?
  • 웹 구성요소 위에 Lit가 하는 일

빌드할 항목

  • 바닐라 좋아요 / 싫어요 웹 구성요소
  • 좋아요 / 싫어요 Lit 기반 웹 구성요소

필요한 항목

  • 업데이트된 최신 브라우저 (Chrome, Safari, Firefox, Chromium Edge) 웹 구성요소는 모든 최신 브라우저에서 작동하며 Microsoft Internet Explorer 11 및 비크로미움 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 살펴보기

파일 선택기 탭바는 Section 1, 코드 수정 섹션은 Section 2, 출력 미리보기는 Section 3, 미리보기 새로고침 버튼은 Section 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. 맞춤 요소 정의

맞춤 요소

Web Components는 4개의 네이티브 웹 API 모음입니다. 이동했습니다.

  • ES 모듈
  • 맞춤 요소
  • Shadow DOM
  • HTML 템플릿

이미 ES 모듈 사양을 사용해 보셨을 것입니다. 이 사양을 사용하면 <script type="module">로 페이지에 로드되는 가져오기 및 내보내기가 있는 JavaScript 모듈을 만들 수 있습니다.

맞춤 요소 정의

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

index.js

class RatingElement extends HTMLElement {}

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

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

문서 본문에 <rating-element>를 배치하고 렌더링되는 내용을 확인합니다.

index.html

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

이제 출력을 보면 렌더링된 항목이 없습니다. 브라우저에 <rating-element>를 렌더링하는 방법을 알려주지 않았으므로 정상적인 동작입니다. Chrome DevTools의 요소 선택기에서 <rating-element>를 선택하고 콘솔에서 다음을 호출하여 맞춤 요소 정의가 성공했는지 확인할 수 있습니다.

$0.constructor

다음과 같이 출력됩니다.

class RatingElement extends HTMLElement {}

맞춤 요소 수명 주기

커스텀 요소에는 일련의 수명 주기 후크가 제공됩니다. 이동했습니다.

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

constructor는 요소가 처음 생성될 때 호출됩니다(예: document.createElement(‘rating-element') 또는 new RatingElement() 호출). 생성자는 요소를 설정하기에 적합한 위치이지만, 요소 '부팅' 성능상의 이유로 생성자에서 DOM 조작을 실행하는 것은 일반적으로 좋지 않은 방법으로 간주됩니다.

맞춤 요소가 DOM에 연결되면 connectedCallback가 호출됩니다. 일반적으로 초기 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에서 DOM 하위 요소를 <rating-element>에 추가하여 좋아요 및 싫어요 버튼과 함께 현재 평점을 표시합니다.

4. Shadow 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이 사용됩니다.

섀도우 루트 연결

요소에 Shadow Root를 연결하고 해당 루트 내에 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);

페이지를 새로고침하면 기본 문서의 스타일이 더 이상 Shadow Root 내부의 노드를 선택할 수 없습니다.

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

Chrome 검사기의 DOM 트리 #shadow-root (open)이 하위 요소인 <rating-element>가 있고, 그 shadowroot 내부에 이전의 DOM이 있습니다.

이제 콘텐츠를 보유하는 확장 가능한 섀도우 루트가 표시됩니다. 이 섀도 루트 내의 모든 것을 섀도 DOM이라고 합니다. Chrome Dev Tools에서 평가 요소를 선택하고 $0.children를 호출하면 자식이 반환되지 않습니다. 이는 Shadow DOM이 직접 하위 요소와 동일한 DOM 트리의 일부가 아니라 Shadow Tree로 간주되기 때문입니다.

Light DOM

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

index.html

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

페이지를 새로고침하면 이 맞춤 요소의 Light DOM에 있는 이 새 DOM 노드가 페이지에 표시되지 않습니다. 이는 Shadow DOM에 <slot> 요소를 통해 Light DOM 노드가 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 선택기는 Shadow Root가 연결된 노드 또는 맞춤 요소를 참조합니다. 이 경우 vote 속성이 "up"이면 좋아요 버튼이 녹색으로 바뀌지만 vote"down", then it will turn the thumb-down button red이면 이제 rating를 구현한 방식과 유사하게 vote의 반영 속성 / 속성을 만들어 이 로직을 구현합니다. 속성 설정자와 가져오기로 시작합니다.

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 인스턴스 속성을 초기화하고 설정자에서 새 값이 다른지 확인합니다. 이 경우 평가를 적절하게 조정하고 vote 속성을 this.setAttribute로 호스트에 다시 반영합니다.

다음으로 속성 바인딩을 설정합니다.

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 속성이 업데이트됩니다.

다음으로 constructor, connectedCallback, 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 메서드를 사용하도록 속성 설정자를 업데이트합니다.

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가 두 번 호출될 수 있습니다. 렌더링의 반복 호출은 기본적으로 아무 작업도 하지 않고 효율적이지만 JavaScript VM은 여전히 동기식으로 해당 함수를 두 번 호출하는 데 시간을 소비합니다. 둘째, 많은 상용구 코드가 필요하므로 새 속성과 속성을 추가하는 것이 번거롭습니다. 이때 LitElement가 유용합니다.

LitElement는 프레임워크와 환경 전반에서 사용할 수 있는 빠르고 가벼운 웹 구성요소를 만들기 위한 Lit의 기본 클래스입니다. 다음으로, 구현을 변경하여 LitElement를 사용하면 rating-element에서 어떤 작업을 할 수 있는지 살펴보세요.

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는 이러한 스타일을 가져와 생성 가능한 스타일시트와 같은 브라우저 기능을 사용하여 렌더링 시간을 단축하고 필요한 경우 이전 브라우저에서 Web Components 폴리필을 통해 전달합니다.

수명 주기

Lit는 네이티브 Web Component 콜백 위에 렌더링 수명 주기 콜백 메서드 집합을 도입합니다. 이러한 콜백은 선언된 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는 매우 작고(축소 + gzip으로 압축 시 5KB 미만) 매우 빠르며 코딩하는 재미가 있습니다. 다른 프레임워크에서 사용할 구성요소를 만들거나 완전한 앱을 빌드할 수 있습니다.

이제 웹 구성요소가 무엇인지, 웹 구성요소를 빌드하는 방법, Lit를 사용하면 웹 구성요소를 더 쉽게 빌드할 수 있는 방법을 알게 되었습니다.

코드 체크포인트

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

다음 단계

다른 Codelab도 확인해 보세요.

추가 자료

커뮤니티