React 개발자를 위한 Lit 안내

Lit란?

Lit는 개발자가 모든 프레임워크에서 신속하게 작동하는 경량 구성요소를 빌드하도록 돕는 Google의 오픈소스 라이브러리 세트입니다. Lit를 사용하면 공유 가능한 구성요소, 애플리케이션, 설계 시스템 등을 빌드할 수 있습니다.

과정 내용

다음과 같은 React 개념을 Lit로 변환하는 방법:

  • JSX 및 템플릿 작성
  • 구성요소 및 props
  • 상태 및 수명 주기
  • 후크
  • 하위 요소
  • Refs
  • 상태 조정하기

빌드할 프로그램

이 Codelab을 완료하면 React 구성요소 개념을 Lit 아날로그로 변환할 수 있습니다.

필요한 사항

  • 최신 버전의 Chrome, Safari, Firefox, Edge
  • HTML, CSS, 자바스크립트, Chrome DevTools 관련 지식
  • React 관련 지식
  • (고급) 최상의 환경에서 개발하려면 VS Code를 다운로드합니다. VS Code 및 NPMlit-plugin도 필요합니다.

Lit의 핵심 개념과 기능은 여러 면에서 React와 비슷하지만, Lit에는 몇 가지 주요 차이점과 차별화 요소가 있습니다.

작은 크기

Lit는 작습니다. 축소되고 gzip으로 압축되어 크기가 약 5KB입니다. 이에 비해 React + ReactDOM은 40KB 이상입니다.

축소 및 압축된 번들 크기(KB)를 보여주는 막대 그래프. Lit는 5KB이고 React + React DOM은 42.2KB입니다.

빠른 속도

공개 벤치마크에서 Lit의 템플릿 작성 시스템인 lit-html과 React의 VDOM을 비교한 결과를 보면 lit-html의 속도는 최악의 경우에도 React보다 8~10% 더 높고 일반적인 사용 사례에서는 50% 더 높습니다.

LitElement(Lit의 구성요소 기본 클래스)는 lit-html에 최소한의 오버헤드를 추가하지만 메모리 사용량, 상호작용, 시작 시간과 같은 구성요소 기능을 비교할 때 React의 성능보다 16~30% 더 높습니다.

Lit와 React의 성능(밀리초 단위)을 비교하는 그룹화된 막대 그래프(낮을수록 좋음)

빌드가 필요하지 않음

Lit에서는 ES 모듈과 같은 새로운 브라우저 기능과 태그된 템플릿 리터럴을 사용하므로 컴파일을 실행할 필요가 없습니다. 즉, 스크립트 태그 + 브라우저 + 서버를 사용하여 개발 환경을 설정하면 바로 사용할 수 있습니다.

ES 모듈과 Skypack 또는 UNPKG 같은 최신 CDN을 사용하면 NPM 없이도 시작할 수 있습니다.

그러나 원한다면 Lit 코드를 빌드하고 최적화할 수도 있습니다. 기본 ES 모듈에 관한 최근의 개발자 통합이 Lit에 도움이 되었습니다. Lit는 일반적인 자바스크립트일 뿐이므로 프레임워크별 CLI나 빌드 처리가 필요하지 않습니다.

프레임워크 제약 없음

Lit의 구성요소는 Web Components라는 일련의 웹 표준을 기반으로 빌드됩니다. 즉, 구성요소를 Lit에서 빌드하면 현재 및 향후 프레임워크에서 작동합니다. HTML 요소를 지원한다면 Web Components를 지원합니다.

프레임워크 상호 운용성과 관련한 유일한 문제는 프레임워크에서 DOM을 제한적으로 지원하는 경우입니다. React는 이러한 프레임워크 중 하나이지만 Refs를 통해 문제를 해결할 수 있습니다. 단, React의 Refs는 개발자 경험이 좋지 않습니다.

Lit팀은 Refs를 사용할 필요가 없도록 Lit 구성요소를 자동으로 파싱하고 React 래퍼를 생성하는 @lit-labs/react라는 실험용 프로젝트를 진행했습니다.

또한 Custom Elements Everywhere에서는 맞춤 요소와 원활하게 작동하는 프레임워크와 라이브러리를 보여줍니다.

최고 수준의 TypeScript 지원

모든 Lit 코드를 자바스크립트로 작성할 수도 있지만 Lit는 TypeScript로 작성되며 Lit팀에서도 개발자에게 TypeScript를 사용하도록 권장합니다.

Lit팀은 lit-analyzerlit-plugin을 사용하여 개발 시간과 빌드 시간에 TypeScript 유형 확인 및 intellisense를 Lit 템플릿에 도입하는 프로젝트를 유지관리하기 위해 Lit 커뮤니티와 협력하고 있습니다.

부울 설정을 위한 부적절한 유형 확인(숫자 윤곽선 표시)을 보여주는 IDE 스크린샷

intellisense 추천을 보여주는 IDE 스크린샷

브라우저에 내장된 개발자 도구

Lit 구성요소는 DOM의 HTML 요소입니다. 즉, 구성요소를 검사하기 위해 브라우저에 맞는 도구나 확장 프로그램을 설치할 필요가 없습니다.

개발자 도구를 열어 요소를 선택하고 속성 또는 상태를 살펴볼 수 있습니다.

Chrome 개발자 도구에서 $0이 <mwc-textfield>를 반환하고 $0.value가 hello world를 반환하고 $0.outlined가 true를 반환하고 {$0}에 의해 속성 확장이 표시된 이미지

서버 측 렌더링(SSR)을 고려한 빌드

Lit 2는 SSR 지원을 염두에 두고 빌드되었습니다. 이 Codelab을 작성하는 시점에 Lit팀이 아직 안정적인 형태의 SSR 도구를 출시하지는 않았지만 Lit팀은 이미 Google 제품에서 서버 측 렌더링 구성요소를 배포해 왔습니다. Lit팀은 곧 GitHub에서 이러한 도구를 외부에 공개할 것으로 예상합니다.

그 전까지 여기에서 Lit팀의 진행 상황을 확인할 수 있습니다.

손쉬운 도입

Lit를 사용하는 데 큰 노력이 필요하지 않습니다. Lit에서 구성요소를 빌드해 기존 프로젝트에 추가할 수 있습니다. 추가한 구성요소가 마음에 들지 않는 경우 전체 앱을 한꺼번에 변환할 필요가 없습니다. 웹 구성요소가 다른 프레임워크에서 작동하기 때문입니다.

Lit에서 전체 앱을 빌드했으며 이 앱을 다른 무언가로 변경하고 싶으세요? 현재 Lit 애플리케이션을 새 프레임워크 내부에 배치하고 원하는 항목을 새 프레임워크의 구성요소로 이전할 수 있습니다.

또한 많은 최신 프레임워크가 웹 구성요소 내 출력을 지원하므로 일반적으로 Lit 요소 자체 내부에 들어갈 수 있습니다.

다음 두 가지 방식으로 이 Codelab을 진행할 수 있습니다.

  • 브라우저를 통해 완전히 온라인에서 진행
  • (고급) VS Code를 사용하여 로컬 머신에서 진행

코드에 액세스하기

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

코드 체크포인트

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

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

// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';

이 체크포인트를 시작점으로 사용하여 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 파일의 콘텐츠를 디렉터리로 추출하기
  • (TS의 경우) 모듈 및 es2015+를 출력하는 quick tsconfig 설정하기
  • 베어 모듈 지정자를 해결할 수 있는 개발 서버 설치하기(Lit팀은 @web/dev-server를 권장함)
  • 개발 서버를 실행하고 브라우저 열기(@web/dev-server를 사용하는 경우 web-dev-server --node-resolve --watch --open을 사용할 수 있음)

이 섹션에서는 Lit에서 템플릿 작성의 기본사항을 알아봅니다.

JSX 및 Lit 템플릿

JSX는 자바스크립트의 구문 확장으로, 이를 통해 React 사용자는 자바스크립트 코드로 쉽게 템플릿을 작성할 수 있습니다. Lit 템플릿은 유사한 용도로, 구성요소의 UI를 상태의 함수로 표현합니다.

기본 구문

코드 체크포인트

React에서 다음과 같이 JSX Hello World를 렌더링합니다.

import 'react';
import ReactDOM from 'react-dom';

const name = 'Josh Perez';
const element = (
  <>
    <h1>Hello, {name}</h1>
    <div>How are you?</div>
  </>
);

ReactDOM.render(
  element,
  mountNode
);

위의 예에는 요소 두 개와 포함된 'name' 변수 하나가 있습니다. Lit에서는 다음과 같습니다.

import {html, render} from 'lit';

const name = 'Josh Perez';
const element = html`
  <h1>Hello, ${name}</h1>
  <div>How are you?</div>`;

render(
  element,
  mountNode
);

Lit 템플릿에서는 여러 요소를 템플릿으로 그룹화하는 데 React Fragment가 필요하지 않습니다.

Lit에서는 html 태그된 템플릿 리터럴(LITeral)로 템플릿이 래핑되어, 여기서 Lit라는 이름이 나왔습니다.

템플릿 값

Lit 템플릿은 TemplateResult라는 다른 Lit 템플릿을 수락할 수 있습니다. 예를 들어 name을 기울임꼴 태그(<i>)로 래핑하고 태그된 템플릿 리터럴 N.B로 래핑합니다. 작은따옴표 문자(')가 아닌 백틱 문자(`)를 사용해야 합니다.

import {html, render} from 'lit';

const name = html`<i>Josh Perez</i>`;
const element = html`
  <h1>Hello, ${name}</h1>
  <div>How are you?</div>`;

render(
  element,
  mountNode
);

Lit TemplateResult는 배열, 문자열, 기타 TemplateResult, 지시문을 수락할 수 있습니다.

코드 체크포인트

연습용으로 다음 React 코드를 Lit로 변환해 보세요.

const itemsToBuy = [
  <li>Bananas</li>,
  <li>oranges</li>,
  <li>apples</li>,
  <li>grapes</li>
];
const element = (
  <>
    <h1>Things to buy:</h1>
    <ol>
      {itemsToBuy}
    </ol>
  </>);

ReactDOM.render(
  element,
  mountNode
);

답변:

import {html, render} from 'lit';

const itemsToBuy = [
  html`<li>Bananas</li>`,
  html`<li>oranges</li>`,
  html`<li>apples</li>`,
  html`<li>grapes</li>`
];
const element = html`
  <h1>Things to buy:</h1>
  <ol>
    ${itemsToBuy}
  </ol>`;

render(
  element,
  mountNode
);

props 전달 및 설정하기

코드 체크포인트

JSX와 Lit 구문의 가장 큰 차이점 중 하나는 데이터 결합 구문입니다. 결합이 포함된 다음 React 입력을 예로 들겠습니다.

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
  <input
      disabled={disabled}
      className={`static-class ${myClass}`}
      defaultValue={value}/>;

ReactDOM.render(
  element,
  mountNode
);

위의 예에는 다음을 처리하는 입력이 정의되어 있습니다.

  • disabled를 정의된 변수(이 경우 false)로 설정합니다.
  • 클래스를 static-class와 변수(이 경우 "static-class my-class")로 설정합니다.
  • 기본값을 설정합니다.

Lit에서는 다음과 같습니다.

import {html, render} from 'lit';

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
  <input
      ?disabled=${disabled}
      class="static-class ${myClass}"
      .value=${value}>`;

render(
  element,
  mountNode
);

Lit 예에서는 disabled 속성을 전환하는 부울 결합이 추가되었습니다.

다음으로, className이 아닌 class 속성에 관한 직접 결합이 있습니다. 클래스 전환을 위한 선언적 도우미인 classMap 지시문을 사용하지 않는 한, 여러 결합을 class 속성에 추가할 수 있습니다.

마지막으로, 입력에 value 속성이 설정됩니다. React와 달리, 기본 구현 및 입력 동작을 따르므로 입력 요소를 읽기 전용으로 설정하지 않습니다.

Lit prop 결합 구문

html`<my-element ?attribute-name=${booleanVar}>`;
  • ? 접두사는 요소의 속성을 전환하는 결합 구문입니다.
  • inputRef.toggleAttribute('attribute-name', booleanVar)와 같습니다.
  • disabled를 사용하는 요소에 유용합니다. 다음으로 인해 DOM에서 disabled="false"를 여전히 true로 읽습니다.inputElement.hasAttribute('disabled') === true
html`<my-element .property-name=${anyVar}>`;
  • . 접두사는 요소의 속성을 설정하는 결합 구문입니다.
  • inputRef.propertyName = anyVar과 같습니다.
  • 객체, 배열, 클래스와 같은 복잡한 데이터를 전달하는 데 적합합니다.
html`<my-element attribute-name=${stringVar}>`;
  • 요소의 속성에 결합됩니다.
  • inputRef.setAttribute('attribute-name', stringVar)와 같습니다.
  • 기본값, 스타일 규칙 선택기, querySelector에 적합합니다.

핸들러 전달하기

코드 체크포인트

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
  <input
      onClick={() => console.log('click')}
      onChange={e => console.log(e.target.value)} />;

ReactDOM.render(
  element,
  mountNode
);

위의 예에는 다음을 처리하는 입력이 정의되어 있습니다.

  • 입력이 클릭되면 'click'이라는 단어를 기록합니다.
  • 사용자가 문자를 입력하면 입력 값을 기록합니다.

Lit에서는 다음과 같습니다.

import {html, render} from 'lit';

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
  <input
      @click=${() => console.log('click')}
      @input=${e => console.log(e.target.value)}>`;

render(
  element,
  mountNode
);

Lit 예에서는 @click을 사용하는 리스너가 click 이벤트에 추가되었습니다.

다음으로, onChange를 사용하는 대신 <input>의 기본 input 이벤트에 관한 결합이 사용됩니다. 기본 change 이벤트blur에서만 실행되기 때문입니다(React에서는 이러한 이벤트에 관해 추상화함).

Lit 이벤트 핸들러 구문

html`<my-element @event-name=${() => {...}}></my-element>`;
  • @ 접두사는 이벤트 리스너의 결합 구문입니다.
  • inputRef.addEventListener('event-name', ...)와 같습니다.
  • 기본 DOM 이벤트 이름을 사용합니다.

이 섹션에서는 Lit 클래스 구성요소 및 함수에 관해 알아봅니다. 상태 및 후크는 이후 섹션에서 자세히 다룹니다.

클래스 구성요소 및 LitElement

코드 체크포인트(TS)코드 체크포인트(JS)

React 클래스 구성요소에 상응하는 Lit 항목은 LitElement입니다. Lit의 '반응형 속성' 개념은 React의 props와 상태의 조합입니다. 예를 들면 다음과 같습니다.

import React from 'react';
import ReactDOM from 'react-dom';

class Welcome extends React.Component {
  constructor(props) {
    super(props);
    this.state = {name: ''};
  }

  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
  element,
  mountNode
);

위의 예에는 다음을 처리하는 React 구성요소가 있습니다.

  • name을 렌더링합니다.
  • name의 기본값을 빈 문자열("")로 설정합니다.
  • name"Elliott"에 재할당합니다.

LitElement에서 다음과 같은 방법으로 이를 처리합니다.

TypeScript:

import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators';

@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  @property({type: String})
  name = '';

  render() {
    return html`<h1>Hello, ${this.name}</h1>`
  }
}

자바스크립트:

import {LitElement, html} from 'lit';

class WelcomeBanner extends LitElement {
  static get properties() {
    return {
      name: {type: String}
    }
  }

  constructor() {
    super();
    this.name = '';
  }

  render() {
    return html`<h1>Hello, ${this.name}</h1>`
  }
}

customElements.define('welcome-banner', WelcomeBanner);

HTML 파일:

<!-- index.html -->
<head>
  <script type="module" src="./index.js"></script>
</head>
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>

위의 예에서 진행되는 작업 검토:

@property({type: String})
name = '';
  • 구성요소의 공개 API의 일부인 공개 반응형 속성을 정의합니다.
  • 구성요소의 속성(기본)과 속성을 노출합니다.
  • 구성요소의 속성(문자열)을 값으로 변환하는 방법을 정의합니다.
static get properties() {
  return {
    name: {type: String}
  }
}
  • @property TS 데코레이터와 동일한 기능을 하지만 기본적으로 자바스크립트로 실행됩니다.
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • 반응형 속성이 변경될 때마다 호출됩니다.
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • 이렇게 하면 HTML 요소 태그 이름과 클래스 정의가 연결됩니다.
  • Custom Elements 표준으로 인해 태그 이름에 하이픈(-)을 반드시 포함해야 합니다.
  • LitElement의 this는 맞춤 요소의 인스턴스(이 경우 <welcome-banner>)를 참조합니다.
customElements.define('welcome-banner', WelcomeBanner);
  • @customElement TS 데코레이터에 상응하는 자바스크립트 항목입니다.
<head>
  <script type="module" src="./index.js"></script>
</head>
  • 맞춤 요소 정의를 가져옵니다.
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>
  • 페이지에 맞춤 요소를 추가합니다.
  • name 속성을 'Elliott'로 설정합니다.

함수 구성요소

코드 체크포인트

Lit는 JSX 또는 전처리기를 사용하지 않으므로 함수 구성요소를 1:1로 해석하지 않습니다. 하지만 속성을 가져와 이러한 속성을 기반으로 DOM을 렌더링하는 함수를 작성하는 것은 아주 간단합니다. 예를 들면 다음과 같습니다.

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
  element,
  mountNode
);

Lit에서는 다음과 같습니다.

import {html, render} from 'lit';

function Welcome(props) {
  return html`<h1>Hello, ${props.name}</h1>`;
}

render(
  Welcome({name: 'Elliott'}),
  document.body.querySelector('#root')
);

이 섹션에서는 Lit의 상태와 수명 주기에 관해 알아봅니다.

상태

Lit의 '반응형 속성' 개념은 React의 상태와 props의 조합입니다. 반응형 속성이 변경되면 구성요소 수명 주기가 트리거될 수 있습니다. 반응형 속성은 다음 두 유형으로 제공됩니다.

공개 반응형 속성

// React
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props)
    this.state = {name: 'there'}
  }

  componentWillReceiveProps(nextProps) {
    if (this.props.name !== nextProps.name) {
      this.setState({name: nextProps.name})
    }
  }
}

// Lit (TS)
import {LitElement} from 'lit';
import {property} from 'lit/decorators';

class MyEl extends LitElement {
  @property() name = 'there';
}
  • @property에 의해 정의됩니다.
  • React의 props 및 상태와 비슷하지만 변경 가능합니다.
  • 구성요소의 소비자가 액세스하고 설정하는 공개 API입니다.

내부 반응형 상태

// React
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props)
    this.state = {name: 'there'}
  }
}

// Lit (TS)
import {LitElement} from 'lit';
import {state} from 'lit/decorators';

class MyEl extends LitElement {
  @state() name = 'there';
}
  • @state에 의해 정의됩니다.
  • React의 상태와 비슷하지만 변경 가능합니다.
  • 일반적으로 구성요소 또는 서브클래스 내에서 액세스하는 비공개 내부 상태입니다.

수명 주기

Lit 수명 주기는 React의 수명 주기와 상당히 비슷하지만, 몇 가지 중요한 차이점이 있습니다.

constructor

// React (js)
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
    this._privateProp = 'private';
  }
}

// Lit (ts)
class MyEl extends LitElement {
  @property({type: Number}) counter = 0;
  private _privateProp = 'private';
}

// Lit (js)
class MyEl extends LitElement {
  static get properties() {
    return { counter: {type: Number} }
  }
  constructor() {
    this.counter = 0;
    this._privateProp = 'private';
  }
}
  • 상응하는 Lit 항목도 constructor입니다.
  • 슈퍼 호출에 아무것도 전달할 필요가 없습니다.
  • 다음을 통해 호출됩니다(모두 포함되지는 않음).
    • document.createElement
    • document.innerHTML
    • new ComponentClass()
    • 업그레이드되지 않은 태그 이름이 페이지에 있고 정의가 @customElement 또는 customElements.define으로 로드되고 등록된 경우
  • React의 constructor와 기능이 비슷합니다.

render

// React
render() {
  return <div>Hello World</div>
}

// Lit
render() {
  return html`<div>Hello World</div>`;
}
  • 상응하는 Lit 항목도 render입니다.
  • TemplateResult 또는 string 등 모든 렌더링 가능한 결과를 반환할 수 있습니다.
  • React와 마찬가지로 render()는 순수 함수여야 합니다.
  • createRenderRoot()가 반환하는 노드(기본적으로 ShadowRoot)로 렌더링됩니다.

componentDidMount

componentDidMount는 Lit의 firstUpdatedconnectedCallback 수명 주기 콜백의 조합과 비슷합니다.

firstUpdated

import Chart from 'chart.js';

// React
componentDidMount() {
  this._chart = new Chart(this.chartElRef.current, {...});
}

// Lit
firstUpdated() {
  this._chart = new Chart(this.chartEl, {...});
}
  • 구성요소의 템플릿이 처음 구성요소의 루트로 렌더링될 때 호출됩니다.
  • 노드가 DOM 트리에 추가될 때까지 요소가 연결되어 있는 경우에만 호출됩니다(예: document.createElement('my-component')를 통해 호출되지 않음).
  • 구성요소로 DOM을 렌더링하도록 요구하는 구성요소 설정을 실행하기에 적합한 위치입니다.
  • React의 firstUpdated와 달리, componentDidMount에서 반응형 속성이 변경되는 경우 다시 렌더링됩니다. 하지만 브라우저는 일반적으로 변경사항을 동일한 프레임으로 일괄 처리합니다. 이러한 변경사항이 루트 DOM에 액세스할 필요가 없다면 일반적으로 willUpdate로 이동해야 합니다.

connectedCallback

// React
componentDidMount() {
  this.window.addEventListener('resize', this.boundOnResize);
}

// Lit
connectedCallback() {
  super.connectedCallback();
  this.window.addEventListener('resize', this.boundOnResize);
}
  • DOM 트리에 맞춤 요소가 삽입될 때마다 호출됩니다.
  • React 구성요소와 달리, 맞춤 요소가 DOM에서 분리되는 경우 제거되지 않으므로 여러 번 '연결'될 수 있습니다.
  • DOM을 다시 초기화하거나 연결 해제 시 정리된 이벤트 리스너를 다시 연결하는 데 유용합니다.
  • 참고: firstUpdated 이전에 connectedCallback이 호출될 수도 있으므로 첫 번째 호출에서 DOM을 사용하지 못할 수도 있습니다.

componentDidUpdate

// React
componentDidUpdate(prevProps) {
  if (this.props.title !== prevProps.title) {
    this._chart.setTitle(this.props.title);
  }
}

// Lit (ts)
updated(prevProps: PropertyValues<this>) {
  if (prevProps.has('title')) {
    this._chart.setTitle(this.title);
  }
}
  • 상응하는 Lit 항목은 updated(영어 'update'의 과거형)입니다.
  • React와 달리, updated는 초기 렌더링 시에도 호출됩니다.
  • React의 componentDidUpdate와 기능이 비슷합니다.

componentWillUnmount

// React
componentWillUnmount() {
  this.window.removeEventListener('resize', this.boundOnResize);
}

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • 상응하는 Lit 항목은 disconnectedCallback과 비슷합니다.
  • React 구성요소와 달리, 맞춤 요소가 DOM에서 분리되는 경우 제거되지 않습니다.
  • componentWillUnmount와 달리, disconnectedCallback은 요소가 트리에서 삭제된 후에 호출됩니다.
  • 루트 내부의 DOM은 여전히 루트의 하위 트리에 연결됩니다.
  • 브라우저가 구성요소에서 가비지 컬렉션을 처리할 수 있도록 이벤트 리스너 및 유출된 참조를 정리하는 데 유용합니다.

연습

코드 체크포인트(TS)코드 체크포인트(JS)

import React from 'react';
import ReactDOM from 'react-dom';

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

위의 예에는 다음을 실행하는 간단한 시계가 있습니다.

  • "Hello World! It is"를 렌더링한 후 시간을 표시합니다.
  • 매초마다 시계가 업데이트됩니다.
  • 마운트 해제되면 틱을 호출하는 간격이 지워집니다.

먼저 구성요소 클래스 선언으로 시작합니다.

// Lit (TS)
// some imports here are imported in advance
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators';

@customElement('lit-clock')
class LitClock extends LitElement {
}

// Lit (JS)
// `html` is imported in advance
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
}

customElements.define('lit-clock', LitClock);

그런 다음 구성요소 사용자가 date를 직접 설정하지 않으므로 date를 초기화하고 @state를 사용해 내부 반응형 속성으로 선언합니다.

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators';

@customElement('lit-clock')
class LitClock extends LitElement {
  @state() // declares internal reactive prop
  private date = new Date(); // initialization
}

// Lit (JS)
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
  static get properties() {
    return {
      // declares internal reactive prop
      date: {state: true}
    }
  }

  constructor() {
    super();
    // initialization
    this.date = new Date();
  }
}

customElements.define('lit-clock', LitClock);

다음으로, 템플릿을 렌더링합니다.

// Lit (JS & TS)
render() {
  return html`
    <div>
      <h1>Hello, World!</h1>
      <h2>It is ${this.date.toLocaleTimeString()}.</h2>
    </div>
  `;
}

이제 틱 메서드를 구현합니다.

tick() {
  this.date = new Date();
}

다음은 componentDidMount의 구현입니다. Lit 아날로그는 firstUpdatedconnectedCallback이 혼합되어 있습니다. 이 구성요소의 경우 setIntervaltick을 호출할 때 루트 내의 DOM에 액세스할 필요가 없습니다. 또한 요소가 문서 트리에서 삭제되면 간격이 지워지므로, 요소가 다시 연결되는 경우 간격을 다시 시작해야 합니다. 따라서 connectedCallback을 사용하는 것이 더 좋습니다.

// Lit (TS)
@customElement('lit-clock')
class LitClock extends LitElement {
  @state()
  private date = new Date();
  private timerId = -1; // initialize timerId for TS

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  ...
}

// Lit (JS)
constructor() {
  super();
  // initialization
  this.date = new Date();
  this.timerId = -1; // initialize timerId for JS
}

connectedCallback() {
  super.connectedCallback();
  this.timerId = setInterval(
    () => this.tick(),
    1000
  );
}

마지막으로, 요소가 문서 트리에서 연결 해제된 후에는 틱이 실행되지 않도록 간격을 삭제합니다.

// Lit (TS & JS)
disconnectedCallback() {
  super.disconnectedCallback();
  clearInterval(this.timerId);
}

종합하면 다음과 같습니다.

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators';

@customElement('lit-clock')
class LitClock extends LitElement {
  @state()
  private date = new Date();
  private timerId = -1;

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  tick() {
    this.date = new Date();
  }

  render() {
    return html`
      <div>
        <h1>Hello, World!</h1>
        <h2>It is ${this.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    clearInterval(this.timerId);
  }
}

// Lit (JS)
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
  static get properties() {
    return {
      date: {state: true}
    }
  }

  constructor() {
    super();
    this.date = new Date();
  }

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  tick() {
    this.date = new Date();
  }

  render() {
    return html`
      <div>
        <h1>Hello, World!</h1>
        <h2>It is ${this.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    clearInterval(this.timerId);
  }
}

customElements.define('lit-clock', LitClock);

이 섹션에서는 React 후크 개념을 Lit로 변환하는 방법을 알아봅니다.

React 후크의 개념

React 후크를 통해 함수 구성요소를 상태에 연결할 수 있습니다. 이렇게 하면 몇 가지 이점이 있습니다.

  • 스테이트풀(Stateful) 논리의 재사용을 간소화합니다.
  • 더 작은 함수로 구성요소를 분할할 수 있습니다.

또한 함수 기반 구성요소에 중점을 두고 React의 클래스 기반 구문과 관련된 다음과 같은 특정 문제를 해결했습니다.

  • constructor에서 superprops를 전달해야 함
  • constructor에서 속성이 제대로 초기화되지 않음
    • 이것이 React 팀이 언급한 이유였고 ES2019에서 해결함
  • this로 인해 발생한 문제가 더 이상 구성요소를 참조하지 않음

Lit에서 React 후크의 개념

구성요소 및 props 섹션에서 언급한 것처럼 Lit에는 함수에서 맞춤 요소를 만드는 방법이 없습니다. 하지만 LitElement는 React 클래스 구성요소와 관련된 주요 문제 대부분을 해결합니다. 예를 들면 다음과 같습니다.

// React (at the time of making hooks)
import React from 'react';
import ReactDOM from 'react-dom';

class MyEl extends React.Component {
  constructor(props) {
    super(props); // Leaky implementation
    this.state = {count: 0};
    this._chart = null; // Deemed messy
  }

  render() {
    return (
      <>
        <div>Num times clicked {count}</div>
        <button onClick={this.clickCallback}>click me</button>
      </>
    );
  }

  clickCallback() {
    // Errors because `this` no longer refers to the component
    this.setState({count: this.count + 1});
  }
}

// Lit (ts)
class MyEl extends LitElement {
  @property({type: Number}) count = 0; // No need for constructor to set state
  private _chart = null; // Public class fields introduced to JS in 2019

  render() {
    return html`
        <div>Num times clicked ${count}</div>
        <button @click=${this.clickCallback}>click me</button>`;
  }

  private clickCallback() {
    // No error because `this` refers to component
    this.count++;
  }
}

Lit는 이 문제를 어떻게 해결할까요?

  • constructor가 인수를 사용하지 않습니다.
  • 모든 @event 결합이 this에 자동으로 결합됩니다.
  • 대부분의 경우 this는 맞춤 요소의 참조를 나타냅니다.
  • 이제 클래스 속성을 클래스 멤버로 인스턴스화할 수 있습니다. 이렇게 하면 생성자 기반 구현이 삭제됩니다.

반응형 컨트롤러

코드 체크포인트(TS)코드 체크포인트(JS)

후크의 기본 개념은 Lit에서는 반응형 컨트롤러로 존재합니다. 반응형 컨트롤러 패턴을 사용하면 스테이트풀(Stateful) 논리를 공유하고, 구성요소를 더 모듈화된 비트로 분할하고 요소의 업데이트 수명 주기에 연결할 수 있습니다.

반응형 컨트롤러는 LitElement와 같은 컨트롤러 호스트의 업데이트 수명 주기에 연결할 수 있는 객체 인터페이스입니다.

ReactiveControllerreactiveControllerHost의 수명 주기는 다음과 같습니다.

interface ReactiveController {
  hostConnected(): void;
  hostUpdate(): void;
  hostUpdated(): void;
  hostDisconnected(): void;
}
interface ReactiveControllerHost {
  addController(controller: ReactiveController): void;
  removeController(controller: ReactiveController): void;
  requestUpdate(): void;
  readonly updateComplete: Promise<boolean>;
}

반응형 컨트롤러를 구성하고 addController를 사용해 호스트에 연결하면 컨트롤러의 수명 주기가 호스트의 수명 주기와 함께 호출됩니다. 예를 들어 상태 및 수명 주기 섹션의 시계 예시를 다시 생각해 보세요.

import React from 'react';
import ReactDOM from 'react-dom';

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

위의 예에는 다음을 실행하는 간단한 시계가 있습니다.

  • "Hello World! It is"를 렌더링한 후 시간을 표시합니다.
  • 매초마다 시계가 업데이트됩니다.
  • 마운트 해제되면 틱을 호출하는 간격이 지워집니다.

구성요소 스캐폴딩 빌드하기

먼저 구성요소 클래스 선언으로 시작하고 render 함수를 추가합니다.

// Lit (TS) - index.ts
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators';

@customElement('my-element')
class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${'time to get Lit'}.</h2>
      </div>
    `;
  }
}

// Lit (JS) - index.js
import {LitElement, html} from 'lit';

class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${'time to get Lit'}.</h2>
      </div>
    `;
  }
}

customElements.define('my-element', MyElement);

컨트롤러 빌드하기

이제 clock.ts로 전환하고 ClockController의 클래스를 만들고 constructor를 설정합니다.

// Lit (TS) - clock.ts
import {ReactiveController, ReactiveControllerHost} from 'lit';

export class ClockController implements ReactiveController {
  private readonly host: ReactiveControllerHost;

  constructor(host: ReactiveControllerHost) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
  }

  private tick() {
  }

  hostDisconnected() {
  }

  // Will not be used but needed for TS compilation
  hostUpdate() {};
  hostUpdated() {};
}

// Lit (JS) - clock.js
export class ClockController {
  constructor(host) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
  }

  tick() {
  }

  hostDisconnected() {
  }
}

반응형 컨트롤러는 ReactiveController 인터페이스를 공유하는 경우 어쨌든 빌드할 수 있습니다. 하지만 ReactiveControllerHost 인터페이스뿐만 아니라 컨트롤러를 초기화하는 데 필요한 다른 속성을 사용할 수 있는 constructor와 함께 클래스를 사용하는 것은 Lit팀이 대부분의 기본 사례에 우선 사용하는 패턴입니다.

이제 React 수명 주기 콜백을 컨트롤러 콜백으로 변환해야 합니다. 예를 들면 다음과 같습니다.

  • componentDidMount
    • LitElement의 connectedCallback으로
    • 컨트롤러의 hostConnected
  • ComponentWillUnmount
    • LitElement의 disconnectedCallback으로
    • 컨트롤러의 hostDisconnected

React 수명 주기를 Lit 수명 주기로 변환하는 방법에 관한 자세한 내용은 상태 및 수명 주기 섹션을 참고하세요.

그런 다음 hostConnected 콜백과 tick 메서드를 구현하고 상태 및 수명 주기 섹션의 예와 같이 hostDisconnected의 간격을 삭제합니다.

// Lit (TS) - clock.ts
export class ClockController implements ReactiveController {
  private readonly host: ReactiveControllerHost;
  private interval = 0;
  date = new Date();

  constructor(host: ReactiveControllerHost) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  private tick() {
    this.date = new Date();
  }

  hostDisconnected() {
    clearInterval(this.interval);
  }

  hostUpdate() {};
  hostUpdated() {};
}

// Lit (JS) - clock.js
export class ClockController {
  interval = 0;
  host;
  date = new Date();

  constructor(host) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  _ick() {
    this.date = new Date();
  }

  hostDisconnected() {
    clearInterval(this.interval);
  }
}

컨트롤러 사용하기

시계 컨트롤러를 사용하려면 컨트롤러를 가져오고 index.ts 또는 index.js에서 구성요소를 업데이트합니다.

// Lit (TS) - index.ts
import {LitElement, html, ReactiveController, ReactiveControllerHost} from 'lit';
import {customElement} from 'lit/decorators';
import {ClockController} from './clock.js';

@customElement('my-element')
class MyElement extends LitElement {
  private readonly clock = new ClockController(this); // Instantiate

  render() {
    // Use controller
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }
}

// Lit (JS) - index.js
import {LitElement, html} from 'lit';
import {ClockController} from './clock.js';

class MyElement extends LitElement {
  clock = new ClockController(this); // Instantiate

  render() {
    // Use controller
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }
}

customElements.define('my-element', MyElement);

컨트롤러를 사용하려면 컨트롤러 호스트의 참조(<my-element> 구성요소)를 전달하여 컨트롤러를 인스턴스화한 다음 render 메서드에 이 컨트롤러를 사용해야 합니다.

컨트롤러에서 재렌더링 트리거하기

시간이 표시되지만 업데이트되지 않는 것을 볼 수 있습니다. 컨트롤러가 매초마다 날짜를 설정하지만 호스트는 업데이트되지 않기 때문입니다. 그 이유는 더 이상 구성요소가 아닌 ClockController 클래스에서 date가 변경되기 때문입니다. 즉, 컨트롤러에서 date가 설정된 후에는 host.requestUpdate()를 사용하여 업데이트 수명 주기를 실행하도록 호스트에 지시해야 합니다.

// Lit (TS & JS) - clock.ts / clock.js
private tick() {
  this.date = new Date();
  this.host.requestUpdate();
}

이제 시계의 시간이 업데이트됩니다.

후크와 일반적인 사용 사례를 자세히 비교해서 보려면 고급 주제 - 후크 섹션을 참고하세요.

이 섹션에서는 Lit에서 슬롯을 사용하여 하위 요소를 관리하는 방법을 알아봅니다.

슬롯 및 하위 요소

코드 체크포인트

슬롯은 구성을 사용 설정하며, 슬롯을 사용해 구성요소를 중첩할 수 있습니다.

React에서 하위 요소는 props를 통해 상속됩니다. 기본 슬롯은 props.children이며, render 함수는 기본 슬롯의 위치를 정의합니다. 예를 들면 다음과 같습니다.

const MyArticle = (props) => {
 return <article>{props.children}</article>;
};

props.children은 HTML 요소가 아니라 React 구성요소입니다.

Lit에서는 슬롯 요소를 사용하여 하위 요소가 렌더링 함수로 작성됩니다. 하위 요소는 React와 같은 방식으로 상속되지 않는다는 점에 유의하세요. Lit에서 하위 요소는 슬롯에 연결된 HTMLElement입니다. 이러한 연결을 프로젝션이라고 합니다.

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <slot></slot>
      </article>
   `;
  }
}

여러 슬롯

코드 체크포인트

React에서 여러 슬롯을 추가하는 것은 기본적으로 더 많은 props를 상속하는 것과 같습니다.

const MyArticle = (props) => {
  return (
    <article>
      <header>
        {props.headerChildren}
      </header>
      <section>
        {props.sectionChildren}
      </section>
    </article>
  );
};

마찬가지로 <slot> 요소를 추가하면 Lit에 더 많은 슬롯이 생성됩니다. 여러 슬롯이 name 속성 <slot name="slot-name">으로 정의됩니다. 이렇게 하면 하위 요소가 할당받는 슬롯을 선언할 수 있습니다.

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <header>
          <slot name="headerChildren"></slot>
        </header>
        <section>
          <slot name="sectionChildren"></slot>
        </section>
      </article>
   `;
  }
}

기본 슬롯 콘텐츠

슬롯에 프로젝션되는 노드가 없으면 슬롯에 하위 트리가 표시됩니다. 노드가 슬롯에 프로젝션되면 이 슬롯에 하위 트리 대신 프로젝션된 노드가 표시됩니다.

@customElement("my-element")
export class MyElement extends LitElement {
  render() {
    return html`
      <section>
        <div>
          <slot name="slotWithDefault">
            <p>
             This message will not be rendered when children are attached to this slot!
            <p>
          </slot>
        </div>
      </section>
   `;
  }
}

슬롯에 하위 요소 할당하기

코드 체크포인트

React에서는 구성요소의 속성을 통해 하위 요소가 슬롯에 할당됩니다. 아래 예에서는 React 요소가 headerChildrensectionChildren props에 전달됩니다.

const MyNewsArticle = () => {
 return (
   <MyArticle
     headerChildren={<h3>Extry, Extry! Read all about it!</h3>}
     sectionChildren={<p>Children are props in React!</p>}
   />
 );
};

Lit에서는 slot 속성을 사용하여 하위 요소가 슬롯에 할당됩니다.

@customElement("my-news-article")
export class MyNewsArticle extends LitElement {
  render() {
    return html`
      <my-article>
        <h3 slot="headerChildren">
          Extry, Extry! Read all about it!
        </h3>
        <p slot="sectionChildren">
          Children are composed with slots in Lit!
        </p>
      </my-article>
   `;
  }
}

기본 슬롯(예: <slot>)이 없으며 맞춤 요소의 하위 요소의 slot 속성(예: <div slot="foo">)과 일치하는 name 속성(예: <slot name="foo">)을 가진 슬롯이 없는 경우에는 노드가 프로젝션되지 않으며 표시되지 않습니다.

때때로 개발자가 HTMLElement의 API에 액세스해야 하는 경우도 있습니다.

이 섹션에서는 Lit에서 요소 참조를 가져오는 방법을 알아봅니다.

React 참조

코드 체크포인트(TS)코드 체크포인트(JS)

React 구성요소는 호출되면 가상 DOM을 생성하는 일련의 함수 호출로 변환 컴파일됩니다. 이 가상 DOM은 ReactDOM을 통해 해석되고 HTMLElement를 렌더링합니다.

React에서 Refs는 생성된 HTMLElement를 포함하기 위한 메모리 내 공간입니다.

const RefsExample = (props) => {
 const inputRef = React.useRef(null);
 const onButtonClick = React.useCallback(() => {
   inputRef.current?.focus();
 }, [inputRef]);

 return (
   <div>
     <input type={"text"} ref={inputRef} />
     <br />
     <button onClick={onButtonClick}>
       Click to focus on the input above!
     </button>
   </div>
 );
};

위의 예에서 React 구성요소는 다음을 실행합니다.

  • 빈 텍스트 입력 및 텍스트가 있는 버튼을 렌더링합니다.
  • 버튼 클릭 시 입력에 포커스를 놓습니다.

초기 렌더링 후 React는 inputRef.currentref 속성을 통해 생성된 HTMLInputElement로 설정합니다.

@query를 사용한 Lit '참조'

Lit는 브라우저 가까이에 있으며 기본 브라우저 기능 위에 아주 얇은 추상화를 생성합니다.

Lit의 refs에 상응하는 React 항목은 @query@queryAll 데코레이터에 의해 반환된 HTMLElement입니다.

@customElement("my-element")
export class MyElement extends LitElement {
  @query('input') // Define the query
  inputEl!: HTMLInputElement; // Declare the prop

  // Declare the click event listener
  onButtonClick() {
    // Use the query to focus
    this.inputEl.focus();
  }

  render() {
    return html`
      <input type="text"></input>
      <br />
      <!-- Bind the click listener -->
      <button @click=${this.onButtonClick}>
        Click to focus on the input above!
      </button>
   `;
  }
}

위의 예에서 Lit 구성요소는 다음을 실행합니다.

  • @query 데코레이터를 사용하여 MyElement에 속성을 정의합니다(HTMLInputElement의 getter 만들기).
  • onButtonClick이라는 클릭 이벤트 콜백을 선언하고 연결합니다.
  • 버튼 클릭 시 입력에 포커스를 놓습니다.

자바스크립트에서 @query@queryAll 데코레이터는 각각 querySelectorquerySelectorAll을 실행합니다. 다음은 @query('input') inputEl!: HTMLInputElement;에 상응하는 자바스크립트 항목입니다.

get inputEl() {
  return this.renderRoot.querySelector('input');
}

Lit 구성요소가 render 메서드의 템플릿을 my-element의 루트에 커밋한 후에는 이제 @query 데코레이터는 inputEl가 렌더링 루트의 첫 번째 input 요소를 반환할 수 있게 합니다. @query에서 지정된 요소를 찾을 수 없는 경우 null을 반환합니다.

렌더링 루트에 input 요소가 여러 개 있는 경우 @queryAll은 노드 목록을 반환합니다.

이 섹션에서는 Lit에서 구성요소 간의 상태를 중재하는 방법을 알아봅니다.

재사용 가능한 구성요소

코드 체크포인트

React는 하향식 데이터 흐름으로 기능적 렌더링 파이프라인을 모방합니다. 상위 요소는 props를 통해 상태를 하위 요소에 제공합니다. 하위 요소가 props에 있는 콜백을 통해 상위 요소와 통신합니다.

const CounterButton = (props) => {
  const label = props.step < 0
    ? `- ${-1 * props.step}`
    : `+ ${props.step}`;

  return (
    <button
      onClick={() =>
        props.addToCounter(props.step)}>{label}</button>
  );
};

위의 예에서 React 구성요소는 다음을 실행합니다.

  • props.step 값을 기반으로 라벨을 만듭니다.
  • +step 또는 -step을 라벨로 사용하여 버튼을 렌더링합니다.
  • props.step을 클릭 시 인수로 사용해 props.addToCounter를 호출하여 상위 구성요소를 업데이트합니다.

Lit에서 콜백을 전달할 수 있지만 기존 패턴은 다릅니다. 위의 예에서 React 구성요소는 아래 예의 Lit 구성요소로 작성할 수 있습니다.

@customElement('counter-button')
export class CounterButton extends LitElement {
  @property({type: Number}) step: number = 0;

  onClick() {
    const event = new CustomEvent('update-counter', {
      bubbles: true,
      detail: {
        step: this.step,
      }
    });

    this.dispatchEvent(event);
  }

  render() {
    const label = this.step < 0
      ? `- ${-1 * this.step}`  // "- 1"
      : `+ ${this.step}`;      // "+ 1"

    return html`
      <button @click=${this.onClick}>${label}</button>
    `;
  }
}

위의 예에서 Lit 구성요소는 다음을 실행합니다.

  • 반응형 속성 step을 만듭니다.
  • 클릭 시 요소의 step 값을 전달하는 update-counter라는 맞춤 이벤트를 전달합니다.

상위 요소에서 상하 요소로 흐르는 브라우저 이벤트가 상단에 표시됩니다. 이벤트를 통해 하위 요소가 상호작용 이벤트 및 상태 변경을 브로드캐스트할 수 있습니다. React는 기본적으로 반대 방향으로 상태를 전달하므로, React 구성요소가 Lit 구성요소와 동일한 방향으로 이벤트를 전달 및 대기하는 것은 드문 일입니다.

스테이트풀(Stateful) 구성요소

코드 체크포인트

React에서는 일반적으로 후크를 사용하여 상태를 관리합니다. CounterButton 구성요소를 재사용하여 MyCounter 구성요소를 만들 수 있습니다. CounterButton의 두 인스턴스 모두에 addToCounter가 전달되는 방법을 살펴보세요.

const MyCounter = (props) => {
 const [counterSum, setCounterSum] = React.useState(0);
 const addToCounter = useCallback(
   (step) => {
     setCounterSum(counterSum + step);
   },
   [counterSum, setCounterSum]
 );

 return (
   <div>
     <h3>&Sigma;: {counterSum}</h3>
     <CounterButton
       step={-1}
       addToCounter={addToCounter} />
     <CounterButton
       step={1}
       addToCounter={addToCounter} />
   </div>
 );
};

위의 예에서는 다음을 실행합니다.

  • count 상태를 만듭니다.
  • count 상태에 숫자를 추가하는 콜백을 만듭니다.
  • 모든 클릭 시 CounterButtonaddToCounter를 사용하여 countstep만큼 업데이트합니다.

Lit에서 MyCounter와 비슷하게 구현할 수 있습니다. addToCountercounter-button에 전달되지 않습니다. 대신 콜백이 상위 요소의 @update-counter 이벤트에 관한 이벤트 리스너로 결합됩니다.

@customElement("my-counter")
export class MyCounter extends LitElement {
  @property({type: Number}) count = 0;

  addToCounter(e: CustomEvent<{step: number}>) {
    // Get step from detail of event or via @query
    this.count += e.detail.step;
  }

  render() {
    return html`
      <div @update-counter="${this.addToCounter}">
        <h3>&Sigma; ${this.count}</h3>
        <counter-button step="-1"></counter-button>
        <counter-button step="1"></counter-button>
      </div>
    `;
  }
}

위의 예에서는 다음을 실행합니다.

  • 값이 변경되면 구성요소를 업데이트하는 count라는 반응형 속성을 만듭니다.
  • addToCounter 콜백을 @update-counter 이벤트 리스너에 결합합니다.
  • update-counter 이벤트의 detail.step에 있는 값을 추가하여 count를 업데이트합니다.
  • step 속성을 통해 counter-buttonstep 값을 설정합니다.

Lit에서 반응형 속성을 사용하여 상위 요소에서 하위 요소에 변경사항을 브로드캐스트하는 것이 더 일반적입니다. 마찬가지로 브라우저의 이벤트 시스템을 사용하여 하단의 세부정보를 상단에 표시하는 것이 좋습니다.

이 접근 방식은 권장사항을 따르고, 웹 구성요소에 관한 크로스 플랫폼 지원을 제공하는 Lit의 목표를 준수합니다.

이 섹션에서는 Lit의 스타일 지정에 관해 알아봅니다.

스타일 지정

Lit는 여러 방식으로 요소의 스타일을 지정할 수 있는 내장된 솔루션입니다.

인라인 스타일

코드 체크포인트

Lit는 인라인 스타일뿐만 아니라 인라인 스타일에 결합하기도 지원합니다.

import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators';

@customElement('my-element')
class MyElement extends LitElement {
  render() {
    return html`
      <div>
    <h1 style="color:orange;">This text is orange</h1>
        <h1 style="color:rebeccapurple;">This text is rebeccapurple</h1>
      </div>
    `;
  }
}

위 예에는 각각 인라인 스타일이 있는 제목 두 개가 있습니다.

이제 주황색 텍스트에 border: 1px solid black을 결합해봅니다.

<h1 style="color:orange;${'border: 1px solid black;'}">This text is orange</h1>

매번 스타일 문자열을 계산해야 한다면 조금 성가실 수도 있으므로 Lit에서는 여기에 도움이 지시문을 제공합니다.

styleMap

styleMap 지시문을 사용하면 더 쉽게 자바스크립트를 사용해 인라인 스타일을 설정할 수 있습니다. 예를 들면 다음과 같습니다.

코드 체크포인트

import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators';
import {styleMap} from 'lit/directives/style-map';

@customElement('my-element')
class MyElement extends LitElement {
  @property({type: String})
  color = '#000'

  render() {
    // Define the styleMap
    const headerStyle = styleMap({
      'border-color': this.color,
    });

    return html`
      <div>
        <h1
          style="border-style:solid;
          <!-- Use the styleMap -->
          border-width:2px;${headerStyle}">
          This div has a border color of ${this.color}
        </h1>
        <input
          type="color"
          @input=${e => (this.color = e.target.value)}
          value="#000">
      </div>
    `;
  }
}

위의 예에서는 다음을 실행합니다.

  • 테두리와 색상 선택 도구가 있는 h1을 표시합니다.
  • border-color를 색상 선택 도구의 값으로 변경합니다.

또한 h1의 스타일을 설정하는 데 사용되는 styleMap이 있습니다. styleMap은 React의 style 속성 결합 구문과 비슷한 구문을 따릅니다.

CSSResult

코드 체크포인트

구성요소의 스타일을 지정하는 권장 방법은 css 태그된 템플릿 리터럴을 사용하는 것입니다.

import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators';

const ORANGE = css`orange`;

@customElement('my-element')
class MyElement extends LitElement {
  static styles = [
    css`
      #orange {
        color: ${ORANGE};
      }

      #purple {
        color: rebeccapurple;
      }
    `
  ];

  render() {
    return html`
      <div>
    <h1 id="orange">This text is orange</h1>
        <h1 id="purple">This text is rebeccapurple</h1>
      </div>
    `;
  }
}

위의 예에서는 다음을 실행합니다.

  • 결합을 사용하여 CSS 태그된 템플릿 리터럴을 선언합니다.
  • ID를 사용하여 두 h1의 색상을 설정합니다.

css 템플릿 태그 사용 시 이점:

  • 클래스별로 한 번씩 파싱됩니다(인스턴스별 파싱 아님).
  • 모듈 재사용을 염두에 두고 구현되었습니다.
  • 스타일을 자체 파일로 손쉽게 구분할 수 있습니다.
  • CSS 맞춤 속성 폴리필과 호환됩니다.

또한 index.html<style> 태그를 확인합니다.

<!-- index.html -->
<style>
  h1 {
    color: red !important;
  }
</style>

Lit는 구성요소의 스타일 범위를 루트로 지정합니다. 즉, 스타일이 유출되지 않습니다. Lit팀은 구성요소에 스타일을 전달할 때는 Lit 스타일 범위를 관통할 수 있는 CSS 맞춤 속성을 사용하도록 권장합니다.

스타일 태그

템플릿에서 간단히 <style> 태그를 인라인 삽입할 수도 있습니다. 브라우저는 이러한 스타일 태그를 중복 삭제하지만 템플릿에 배치합니다. 따라서 css 태그된 템플릿의 경우처럼 클래스별이 아닌 구성요소 인스턴스별로 파싱됩니다. 또한 브라우저의 CSSResult 중복 삭제는 훨씬 더 빠릅니다.

템플릿에서 <link rel="stylesheet">를 사용하는 것은 스타일의 측면이기는 하지만 스타일이 지정되지 않은 콘텐츠(FOUC)의 초기 플래시가 발생할 수도 있기 때문에 권장되지 않습니다.

JSX 및 템플릿 작성

Lit 및 가상 DOM

Lit-html은 개별 노드를 구분하는 기존 가상 DOM을 포함하지 않습니다. 대신, ES2015의 태그된 템플릿 리터럴 사양에 고유한 성능 기능을 활용합니다. 태그된 템플릿 리터럴은 태그 함수가 연결된 템플릿 리터럴 문자열입니다.

다음은 템플릿 리터럴의 예입니다.

const str = 'string';
console.log(`This is a template literal ${str}`);

다음은 태그된 템플릿 리터럴의 예입니다.

const tag = (strings, ...values) => ({strings, values});
const f = (x) => tag`hello ${x} how are you`;
console.log(f('world')); // {strings: ["hello ", " how are you"], values: ["world"]}
console.log(f('world').strings === f(1 + 2).strings); // true

위의 예에서 태그는 tag 함수이고 f 함수는 태그된 템플릿 리터럴의 호출을 반환합니다.

Lit에서 다수의 뛰어난 성능은 태그 함수에 전달되는 문자열 배열에 동일한 포인터가 있다는 사실에서 비롯됩니다(두 번째 console.log에서 확인). 브라우저가 동일한 템플릿 리터럴(AST의 동일한 위치에 사용)을 사용 중이므로 각 태그 함수 호출에서 새로운 strings 배열이 다시 생성되지 않습니다. 따라서 많은 런타임 diff 오버헤드 없이 Lit의 결합, 파싱, 템플릿 캐싱에 이러한 기능을 활용할 수 있습니다.

태그된 템플릿 리터럴의 내장 브라우저 동작을 사용하면 Lit에서 상당한 성능 이점을 얻을 수 있습니다. 대부분의 기존 가상 DOM은 대다수 작업을 자바스크립트로 처리합니다. 그러나 태그된 템플릿 리터럴은 브라우저의 C++에서 대부분의 diff를 실행합니다.

React 또는 Preact에서 HTML 태그된 템플릿 리터럴을 사용하기 시작하려는 개발자에게 Lit팀은 htm 라이브러리를 권장합니다.

하지만, Google Codelabs 사이트 및 일부 온라인 코드 편집기의 경우와 마찬가지로 태그된 템플릿 리터럴 구문 강조표시가 아주 일반적이지는 않음을 알 수 있습니다. Atom 및 GitHub의 코드 블록 강조표시와 같은 일부 IDE 및 텍스트 편집기는 기본적으로 이러한 구문 강조를 지원합니다. 또한 Lit팀은 커뮤니티와 긴밀하게 협력하여 Lit 프로젝트에 구문 강조표시, 유형 확인, intellisense를 추가하는 VS Code 플러그인인 lit-plugin 같은 프로젝트를 유지관리합니다.

Lit 및 JSX + React DOM

JSX는 브라우저에서 실행되지 않으며 대신 전처리기를 사용하여 JSX를 자바스크립트 함수 호출로 변환합니다(일반적으로 Babel을 통해).

예를 들어 Babel은 다음을

const element = <div className="title">Hello World!</div>;
ReactDOM.render(element, mountNode);

다음으로 변환합니다.

const element = React.createElement('div', {className: 'title'}, 'Hello World!');
ReactDOM.render(element, mountNode);

그런 다음 React DOM은 React 출력을 가져와 실제 DOM으로 변환합니다(속성, 이벤트 리스너 등).

Lit-html은 변환 컴파일이나 전처리기 없이 브라우저에서 실행할 수 있는 태그된 템플릿 리터럴을 사용합니다. 따라서 Lit를 시작하려면 HTML 파일, ES 모듈 스크립트, 서버만 있으면 됩니다. 다음은 완전히 브라우저에서 실행할 수 있는 스크립트입니다.

<!DOCTYPE html>
<html>
  <head>
    <script type="module">
      import {html, render} from 'https://cdn.skypack.dev/lit';

      render(
        html`<div>Hello World!</div>`,
        document.querySelector('.root')
      )
    </script>
  </head>
  <body>
    <div class="root"></div>
  </body>
</html>

또한 Lit의 템플릿 작성 시스템인 lit-html은 일반적인 가상 DOM을 사용하지 않고 DOM API를 직접 사용합니다. Lit 2의 크기는 축소하고 gzip으로 압축하여 5KB 미만이고, 이에 비해 축소하고 gzip으로 압축하여 React(2.8KB) + react-dom(39.4KB)으로 약 40KB입니다.

이벤트

React는 합성 이벤트 시스템을 사용합니다. 즉, react-dom은 모든 구성요소에 사용될 모든 이벤트를 정의하고, 각 노드 유형에 상응하는 camelCase 이벤트 리스너를 제공해야 합니다. 따라서 JSX에는 맞춤 이벤트에 관한 이벤트 리스너를 정의하는 메서드가 없으며, 개발자는 ref를 사용한 후 필수적으로 리스너를 적용해야 합니다. 이렇게 하면 React를 고려하지 않은 라이브러리를 통합할 때 표준 이하의 개발자 환경이 생성되며, 결과적으로 React 특정 래퍼를 작성해야 합니다.

Lit-html은 DOM에 직접 액세스하여 기본 이벤트를 사용하므로, 이벤트 리스너를 추가하는 것은 @event-name=${eventNameListener}만큼 쉽습니다. 즉, 이벤트 리스너 추가 및 이벤트 실행에 필요한 런타임 파싱이 더 적습니다.

구성요소 및 props

React 구성요소 및 맞춤 요소

내부적으로 LitElement는 맞춤 요소를 사용하여 구성요소를 패키징합니다. 맞춤 요소를 사용하면 구성요소화와 관련하여 React 구성요소 사이에 어느 정도의 절충이 발생합니다. (상태와 수명 주기는 상태 및 수명 주기 섹션에서 자세히 설명합니다.)

구성요소 시스템으로서 맞춤 요소의 장점:

  • 브라우저에 기본 제공되며 도구가 필요하지 않음
  • innerHTML, document.createElement, querySelector 등 모든 브라우저 API에 적합함
  • 일반적으로 프레임워크 간에 사용 가능
  • customElements.define 및 'hydrate' DOM을 사용하여 지연 등록 가능

React 구성요소와 비교할 때 맞춤 요소의 단점:

  • 클래스를 정의하지 않으면 맞춤 요소를 생성할 수 없음(JSX와 같은 기능 구성요소가 없음)
  • 닫는 태그를 포함해야 함
    • 참고: 개발자 편의 브라우저 공급업체가 자체적으로 닫는 태그 사양을 후회하는 경향이 있기 때문에 최신 사양에는 자체적으로 닫는 태그가 포함되어 있지 않습니다.
  • DOM 트리에 추가 노드가 도입되어 레이아웃 문제를 일으킬 수도 있음
  • 자바스크립트를 통해 등록해야 함

Lit는 맞춤형 요소 시스템이 아닌 맞춤 요소를 채택했습니다. 맞춤 요소가 브라우저에 내장되기 때문입니다. Lit팀은 프레임워크 간 이점이 구성요소 추상화 계층에서 얻는 이점보다 크다고 생각합니다. 실제로 Lit팀은 lit-ssr 공간과 관련한 작업을 통해 자바스크립트 등록과 관련된 주요 문제를 해결했습니다. 또한 GitHub와 같은 일부 회사에서는 맞춤 요소 지연 등록을 활용하여 선택적 기능을 통해 페이지를 점진적으로 개선하고 있습니다.

맞춤 요소에 데이터 전달하기

맞춤 요소에 관한 일반적인 오해는 데이터를 문자열로만 전달할 수 있다는 생각입니다. 요소 속성은 문자열로만 기록될 수 있기 때문에 이러한 오해가 생길 수 있습니다. Lit가 문자열 속성을 정의된 유형으로 변환하는 것이 사실이라 해도 맞춤 요소가 복잡한 데이터를 속성으로 수락할 수 있습니다.

예를 들어 다음과 같은 LitElement 정의를 가정해보겠습니다.

코드

// data-test.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators';

@customElement('data-test')
class DataTest extends LitElement {
  @property({type: Number})
  num = 0;

  @property({attribute: false})
  data = {a: 0, b: null, c: [html`<div>hello</div>`, html`<div>world</div>`]}

  render() {
    return html`
      <div>num + 1 = ${this.num + 1}</div>
      <div>data.a = ${this.data.a}</div>
      <div>data.b = ${this.data.b}</div>
      <div>data.c = ${this.data.c}</div>`;
  }
}

기본 반응형 속성 num이 정의되어 속성의 문자열 값을 number로 변환한 다음 Lit의 속성 처리를 비활성화하는 attribute:false로 복잡한 데이터 구조를 사용합니다.

이 맞춤 요소에 데이터를 전달하는 방법은 다음과 같습니다.

<head>
  <script type="module">
    import './data-test.js'; // loads element definition
    import {html} from './data-test.js';

    const el = document.querySelector('data-test');
    el.data = {
      a: 5,
      b: null,
      c: [html`<div>foo</div>`,html`<div>bar</div>`]
    };
  </script>
</head>
<body>
  <data-test num="5"></data-test>
</body>

상태 및 수명 주기

기타 React 수명 주기 콜백

static getDerivedStateFromProps

props와 상태는 모두 동일한 클래스 속성이므로, Lit에는 상응하는 것이 없습니다.

shouldComponentUpdate

  • shouldUpdate에 상응하는 Lit 항목입니다.
  • React와 달리, 첫 번째 렌더링 시 호출됩니다.
  • React의 shouldComponentUpdate와 기능이 비슷합니다.

getSnapshotBeforeUpdate

Lit에서 getSnapshotBeforeUpdateupdatewillUpdate와 비슷합니다.

willUpdate

  • update 전에 호출됩니다.
  • getSnapshotBeforeUpdate와 달리, willUpdaterender 전에 호출됩니다.
  • willUpdate에서 반응형 속성이 변경될 때 업데이트 주기가 다시 트리거되지 않습니다.
  • 다른 속성에 종속되고 업데이트 프로세스의 나머지 부분에서 사용되는 속성 값을 계산하는 데 적합한 위치입니다.
  • 이 메서드는 SSR의 서버에서 호출되므로 여기서 DOM에 액세스하지 않는 것이 좋습니다.

update

  • willUpdate 다음에 호출됩니다.
  • getSnapshotBeforeUpdate와 달리, updaterender 전에 호출됩니다.
  • update에서 반응형 속성이 변경될 때 super.update를 호출하기 전에 변경된 경우 업데이트 주기가 다시 트리거되지 않습니다.
  • 렌더링된 출력이 DOM에 커밋되기 전에 구성요소와 관련된 DOM에서 정보를 캡처하기에 적합한 위치입니다.
  • 이 메서드는 SSR의 서버에서 호출되지 않습니다.

기타 Lit 수명 주기 콜백

React에 상응하는 아날로그 버전이 없기 때문에 이전 섹션에서 언급하지 않은 몇 가지 수명 주기 콜백이 있습니다. 다음과 같습니다.

attributeChangedCallback

요소의 observedAttributes 중 하나가 변경되면 호출됩니다. observedAttributesattributeChangedCallback은 모두 맞춤 요소 사양의 일부이며 Lit에서 내부적으로 구현되어 Lit 요소의 속성 API를 제공합니다.

adoptedCallback

구성요소가 새 문서로 이동(예: HTMLTemplateElementdocumentFragment에서 기본 document로)할 때 호출됩니다. 이 콜백은 맞춤 요소 사양의 일부이기도 하며 구성요소가 문서를 변경하는 고급 사용 사례에만 사용해야 합니다.

기타 수명 주기 메서드 및 속성

이러한 메서드와 속성은 수명 주기 프로세스를 조작하기 위해 호출하거나 재정의하거나 대기할 수 있는 클래스 멤버입니다.

updateComplete

업데이트 및 렌더링 수명 주기가 비동기식이므로 요소 업데이트가 완료되는 시점을 확인하는 Promise입니다. 예:

async nextButtonClicked() {
  this.step++;
  // Wait for the next "step" state to render
  await this.updateComplete;
  this.dispatchEvent(new Event('step-rendered'));
}

getUpdateComplete

updateComplete가 확인되면 맞춤설정하기 위해 재정의해야 하는 메서드입니다. 구성요소가 하위 구성요소를 렌더링하고 렌더링 주기가 동기화되어야 하는 경우 일반적입니다. 예:

class MyElement extends LitElement {
  ...
  async getUpdateComplete() {
    await super.getUpdateComplete();
    await this.myChild.updateComplete;
  }
}

performUpdate

이 메서드는 업데이트 수명 주기 콜백을 호출합니다. 동기식으로 업데이트하거나 맞춤 일정을 위해 업데이트해야 하는 흔치 않은 경우가 아니면 일반적으로 필요하지 않습니다.

hasUpdated

구성요소가 한 번 이상 업데이트된 경우 이 속성은 true입니다.

isConnected

맞춤 요소 사양의 일부인 이 속성은 요소가 현재 기본 문서 트리에 연결되어 있으면 true가 됩니다.

Lit 업데이트 수명 주기 시각화

업데이트 수명 주기는 다음 세 부분으로 구성됩니다.

  • 사전 업데이트
  • 업데이트
  • 사후 업데이트

사전 업데이트

콜백 이름이 포함된 노드의 방향성 비순환 그래프입니다. 생성자는 requestUpdate로 지정되고 @property는 Property Setter로, attributeChangedCallback은 Property Setter로 지정됩니다. Property Setter는 hasChanged로 지정됩니다. requestUpdate로 hasChanged 처리됩니다. requestUpdate는 다음 업데이트 수명 주기 그래프를 가리킵니다.

requestUpdate 다음에 예약된 업데이트가 대기 중입니다.

업데이트

콜백 이름이 포함된 노드의 방향성 비순환 그래프입니다. 사전 업데이트 수명 주기의 이전 이미지에서 화살표가 performUpdate를 가리킵니다. performUpdate가 shouldUpdate로 지정됩니다. shouldUpdate가 ‘complete update if false’와 willUpdate를 모두 가리킵니다. willUpdate가 업데이트합니다. 렌더링뿐만 아니라 다음 사후 업데이트 수명 주기 그래프도 업데이트합니다. 렌더링이 다음 사후 업데이트 수명 주기 그래프도 가리킵니다.

사후 업데이트

콜백 이름이 포함된 노드의 방향성 비순환 그래프입니다. 업데이트 수명 주기의 이전 이미지에서 화살표가 firstUpdated를 가리킵니다. firstUpdated가 updated로 지정됩니다. updated가 updateComplete로 지정됩니다.

후크

후크를 사용하는 이유

후크는 상태가 필요한 간단한 함수 구성요소 사용 사례를 위해 React에 도입되었습니다. 많은 간단한 사례에서 후크가 포함된 함수 구성요소는 클래스 구성요소보다 훨씬 더 간단하고 더 읽기 쉽습니다. 하지만 비동기 상태 업데이트를 도입하고 후크나 효과 사이에 데이터를 전달하는 경우 후크 패턴은 충분하지 않은 경향이 있으며 따라서 반응형 컨트롤러와 같은 클래스 기반 솔루션이 유용합니다.

API 요청 후크 및 컨트롤러

일반적으로 API에서 데이터를 요청하는 후크를 작성합니다. 다음을 실행하는 이 React 함수 구성요소를 예로 들겠습니다.

  • index.tsx
    • 텍스트를 렌더링합니다.
    • useAPI의 응답을 렌더링합니다.
      • 사용자 ID + 사용자 이름
      • 오류 메시지
        • 사용자 11에 도달 시 404(설계를 통해)
        • API 가져오기가 취소되는 경우 취소 오류
      • 로드 중 메시지
    • 작업 버튼을 렌더링합니다.
      • 다음 사용자: 다음 사용자를 위한 API를 가져옵니다.
      • 취소: API 가져오기를 취소하고 오류를 표시합니다.
  • useApi.tsx
    • useApi 맞춤 후크를 정의합니다.
    • 비동기식으로 API에서 사용자 객체를 가져옵니다.
    • 다음을 내보냅니다.
      • 사용자 이름
      • 가져오기를 로드하는지 여부
      • 표시된 오류 메시지
      • 가져오기를 취소하는 콜백
    • 마운트 해제되는 경우 진행 중인 가져오기를 취소합니다.

Lit + 반응형 컨트롤러 구현을 참고하세요.

핵심사항:

  • 반응형 컨트롤러는 맞춤 후크와 비슷합니다.
  • 렌더링 불가능한 데이터를 콜백 및 효과 사이에 전달합니다.
    • React는 useRef를 사용하여 useEffectuseCallback 사이에 데이터를 전달합니다.
    • Lit는 비공개 클래스 속성을 사용합니다.
    • React는 기본적으로 비공개 클래스 속성의 동작을 모방하고 있습니다.

하위 요소

기본 슬롯

HTML 요소에 slot 속성이 제공되지 않으면 이 요소는 이름이 지정되지 않은 기본 슬롯에 할당됩니다. 아래 예에서 MyApp은 이름이 지정된 슬롯에 한 단락을 배치합니다. 다른 단락은 이름이 지정되지 않은 슬롯으로 기본 설정됩니다.

플레이그라운드

@customElement("my-element")
export class MyElement extends LitElement {
  render() {
    return html`
      <section>
        <div>
          <slot></slot>
        </div>
        <div>
          <slot name="custom-slot"></slot>
        </div>
      </section>
   `;
  }
}

@customElement("my-app")
export class MyApp extends LitElement {
  render() {
    return html`
      <my-element>
        <p slot="custom-slot">
          This paragraph will be placed in the custom-slot!
        </p>
        <p>
          This paragraph will be placed in the unnamed default slot!
        </p>
      </my-element>
   `;
  }
}

슬롯 업데이트

슬롯 하위 요소의 구조가 변경되면 slotchange 이벤트가 실행됩니다. Lit 구성요소는 이벤트 리스너를 slotchange 이벤트에 결합할 수 있습니다. 아래 예에서 shadowRoot에 있는 첫 번째 슬롯은 slotchange 시 콘솔에 assignedNodes를 기록합니다.

@customElement("my-element")
export class MyElement extends LitElement {
  onSlotChange(e: Event) {
    const slot = this.shadowRoot.querySelector('slot');
    console.log(slot.assignedNodes({flatten: true}));
  }

  render() {
    return html`
      <section>
        <div>
          <slot @slotchange="{this.onSlotChange}"></slot>
        </div>
      </section>
   `;
  }
}

Refs

참조 생성

Lit와 React는 모두 render 함수가 호출된 후 HTMLElement 참조를 노출합니다. 하지만 나중에 Lit @query 데코레이터나 React 참조를 통해 반환되는 DOM이 React와 Lit에서 작성되는 방식을 검토할 필요가 있습니다.

React는 HTMLElement가 아닌 React 구성요소를 생성하는 기능 파이프라인입니다. Ref는 HTMLElement가 렌더링되기 전에 선언되기 때문에 메모리의 공간이 할당됩니다. 이러한 이유로 null이 Ref의 초기 값으로 표시됩니다. 실제 DOM 요소, 즉 useRef(null)가 아직 생성(또는 렌더링)되지 않았기 때문입니다.

ReactDOM이 React 구성요소를 HTMLElement로 변환한 후에는 ReactComponent에서 ref라는 속성을 찾습니다. 가능한 경우 ReactDOM은 HTMLElement의 참조를 ref.current에 배치합니다.

LitElement는 lit-html의 html 템플릿 태그 함수를 사용하여 내부적으로 템플릿 요소를 작성합니다. LitElement는 렌더링 후 맞춤 요소의 shadow DOM에 템플릿 콘텐츠를 기록합니다. shadow DOM은 섀도 루트로 캡슐화되어 범위가 지정된 DOM 트리입니다. 그러면 @query 데코레이터는 범위가 지정된 루트에서 기본적으로 this.shadowRoot.querySelector를 실행하는 속성에 관한 getter를 만듭니다.

여러 요소 쿼리하기

아래 예에서 @queryAll 데코레이터는 섀도 루트의 두 단락을 NodeList로 반환합니다.

@customElement("my-element")
export class MyElement extends LitElement {
  @queryAll('p')
  paragraphs!: NodeList;

  render() {
    return html`
      <p>Hello, world!</p>
      <p>How are you?</p>
   `;
  }
}

기본적으로 @queryAllthis.shadowRoot.querySelectorAll()의 결과를 반환하는 paragraphs에 관한 getter를 만듭니다. 자바스크립트에서 동일한 목적을 위한 getter를 선언할 수 있습니다.

get paragraphs() {
  return this.renderRoot.querySelectorAll('p');
}

변경 요소 쿼리하기

@queryAsync 데코레이터는 다른 요소 속성의 상태에 따라 변경될 수 있는 노드를 처리하는 데 더 적합합니다.

아래 예에서 @queryAsync는 첫 번째 단락 요소를 찾습니다. 하지만 단락 요소는 renderParagraph가 홀수 숫자를 무작위로 생성할 때만 렌더링됩니다. @queryAsync 지시문은 첫 번째 단락을 사용할 수 있을 때 확인되는 Promise를 반환합니다.

@customElement("my-dissappearing-paragraph")
export class MyDisapppearingParagraph extends LitElement {
  @queryAsync('p')
  paragraph!: Promise<HTMLElement>;

  renderParagraph() {
    const randomNumber = Math.floor(Math.random() * 10)
    if (randomNumber % 2 === 0) {
      return "";
    }

    return html`<p>This checkbox is checked!`
  }

  render() {
    return html`
      ${this.renderParagraph()}
   `;
  }
}

상태 조정하기

React에서는 상태가 React 자체에 의해 조정되므로 콜백을 사용하는 것이 규칙입니다. React에서는 요소가 제공한 상태에 의존하지 않기 위해 최선을 다합니다. DOM은 렌더링 프로세스의 효과일 뿐입니다.

외부 상태

Redux, MobX 또는 다른 상태 관리 라이브러리를 Lit와 함께 사용할 수 있습니다.

브라우저 범위에서 Lit 구성요소가 생성됩니다. 따라서 Lit에서는 브라우저 범위에도 존재하는 모든 라이브러리를 사용할 수 있습니다. Lit의 기존 상태 관리 시스템을 활용할 수 있는 뛰어난 라이브러리가 많습니다.

다음은 바뎅 씨가 Lit 구성요소에서 Redux를 활용하는 방법을 설명하는 시리즈입니다.

Adobe의 lit-mobx를 확인하여 대규모 사이트가 Lit에서 MobX를 활용하는 방법을 알아보세요.

또한 Apollo Elements를 확인하여 개발자가 웹 구성요소에 GraphQL을 포함하는 방법을 알아보세요.

Lit는 기본 브라우저 기능과 호환되며, 브라우저 범위에 있는 대부분의 상태 관리 솔루션을 Lit 구성요소에서 사용할 수 있습니다.

스타일 지정

Shadow DOM

Custom Elements 내에 스타일과 DOM을 기본적으로 캡슐화하기 위해 Lit에서는 Shadow DOM을 사용합니다. 섀도 루트는 기본 문서 트리와 별도로 섀도 트리를 생성합니다. 이것은 대부분의 스타일이 이 문서로 범위가 지정됨을 의미합니다. 색상과 같은 특정 스타일 및 다른 글꼴 관련 스타일이 유출됩니다.

또한 Shadow DOM은 CSS 사양에 새로운 개념과 선택기를 제공합니다.

:host, :host(:hover), :host([hover]) {
  /* Styles the element in which the shadow root is attached to */
}

slot[name="title"]::slotted(*), slot::slotted(:hover), slot::slotted([hover]) {
  /*
   * Styles the elements projected into a slot element. NOTE: the spec only allows
   * styling the direcly slotted elements. Children of those elements are not stylable.
   */
}

스타일 공유하기

Lit를 사용하면 css 템플릿 태그를 통해 CSSTemplateResults 형식으로 쉽게 스타일을 구성요소 사이에 공유할 수 있습니다. 예를 들면 다음과 같습니다.

// typography.ts
export const body1 = css`
  .body1 {
    ...
  }
`;

// my-el.ts
import {body1} from './typography.ts';

@customElement('my-el')
class MyEl Extends {
  static get styles = [
    body1,
    css`/* local styles come after so they will override bod1 */`
  ]

  render() {
    return html`<div class="body1">...</div>`
  }
}

테마 설정

섀도 루트를 사용하려면 일반적으로 하향식의 태그 접근 방식인 기존의 테마 설정에 약간의 문제가 있습니다. Web Components에서 Shadow DOM을 사용하는 테마 설정을 처리하는 기존 방법은 CSS 맞춤 속성을 통해 스타일 API를 노출하는 것입니다. 예를 들어 다음은 머티리얼 디자인에서 사용하는 패턴입니다.

.mdc-textfield-outline {
  border-color: var(--mdc-theme-primary, /* default value */ #...);
}
.mdc-textfield--input {
  caret-color: var(--mdc-theme-primary, #...);
}

사용자는 맞춤 속성 값을 적용하여 사이트의 테마를 변경합니다.

html {
  --mdc-theme-primary: #F00;
}
html[dark] {
  --mdc-theme-primary: #F88;
}

하향식 테마 설정이 필수인 경우 스타일을 노출할 수 없다면 this를 반환하도록 createRenderRoot를 재정의하여 언제든지 Shadow DOM을 사용 중지할 수 있습니다. 그러면 맞춤 요소에 연결된 섀도 루트가 아닌 맞춤 요소 자체에 구성요소의 템플릿이 렌더링됩니다. 이 경우 스타일 캡슐화, DOM 캡슐화, 슬롯을 잃게 됩니다.

프로덕션

IE 11

IE 11과 같은 이전 브라우저를 지원해야 하는 경우 약 33KB에 이르는 폴리필을 로드해야 합니다. 자세한 내용은 여기에서 확인할 수 있습니다.

조건부 번들

Lit팀에서는 IE 11과 최신 브라우저에 각각 하나씩 번들 두 개를 제공할 것을 권장합니다. 이렇게 하면 다음과 같은 몇 가지 이점이 있습니다.

  • ES 6를 제공하면 더 빠르고 대부분의 클라이언트를 지원합니다.
  • 변환 컴파일된 ES 5는 번들 크기를 대폭 줄입니다.
  • 조건부 번들은 두 가지의 장점을 제공합니다.
    • IE 11 지원
    • 최신 브라우저에서 속도 저하 없음

조건부 제공 번들을 빌드하는 방법에 관한 자세한 내용은 문서 사이트(여기)에서 확인할 수 있습니다.