본문으로 건너뛰기
Previous
Next
Lit Complete Guide | Fast Web Components Library

Lit Complete Guide | Fast Web Components Library

Lit Complete Guide | Fast Web Components Library

이 글의 핵심

Lit is a simple library for building fast, lightweight web components. Created by Google, it uses standard web platform features with reactive updates and declarative templates.

Introduction

Lit is a simple library for building fast, lightweight web components. It’s built on top of standard Web Components APIs and adds reactive properties, scoped styles, and declarative templates.

Developed by Google (formerly Polymer team), Lit represents the modern, production-ready approach to Web Components. It powers Google’s internal design systems and is used across Google products.

Why Lit Matters

Web Components without the pain:

  • 5KB gzipped — one of the smallest component libraries
  • Works everywhere — no framework lock-in, use in React/Vue/Angular
  • Future-proof — built on web standards, not framework trends
  • Fast — no Virtual DOM, direct DOM updates

Real-world adoption:

  • Google — uses Lit for YouTube, Google Photos, and internal tools
  • Adobe — Spectrum Web Components (design system) built with Lit
  • Microsoft — FAST framework built on similar Web Components approach
  • ING Bank — uses Lit for their design system across 40+ countries
  • ~500k weekly npm downloads (growing as Web Components mature)

Production use cases:

  • Design systems — reusable components across multiple frameworks
  • Micro-frontends — framework-agnostic components that work anywhere
  • WordPress plugins — works alongside jQuery without conflicts
  • CMS widgets — embeddable components that don’t conflict with host page

When to choose Lit:

  • Building a design system for multiple frameworks
  • Micro-frontend architecture where different teams use different frameworks
  • WordPress/PHP projects that need modern components
  • Long-term maintenance — Web Components standard won’t change

When to use React/Vue instead:

  • Building a single-framework SPA
  • Need large ecosystem of UI libraries
  • Team already expert in React/Vue
  • Need React Native for mobile

Why Lit?

Traditional Web Components:

class MyCounter extends HTMLElement {
  constructor() {
    super();
    this.count = 0;
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    this.render();
  }
  
  render() {
    this.shadowRoot.innerHTML = `
      <button>Count: ${this.count}</button>
    `;
    this.shadowRoot.querySelector('button').addEventListener('click', () => {
      this.count++;
      this.render();
    });
  }
}

With Lit:

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

@customElement('my-counter')
class MyCounter extends LitElement {
  @property({ type: Number }) count = 0;
  
  render() {
    return html`
      <button @click=${() => this.count++}>
        Count: ${this.count}
      </button>
    `;
  }
}

Much cleaner!

1. Installation

npm install lit

Using with TypeScript

npm install lit
npm install -D @types/node

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "experimentalDecorators": true,
    "useDefineForClassFields": false
  }
}

2. Basic Component

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

@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
  @property() name = 'World';
  
  render() {
    return html`<p>Hello, ${this.name}!</p>`;
  }
}

Usage in HTML:

<simple-greeting name="Alice"></simple-greeting>

3. Reactive Properties

@property Decorator

@customElement('user-profile')
export class UserProfile extends LitElement {
  // String property
  @property() name = '';
  
  // Number property
  @property({ type: Number }) age = 0;
  
  // Boolean property (attribute: has-admin)
  @property({ type: Boolean, attribute: 'has-admin' }) isAdmin = false;
  
  // Object/Array (don't sync with attributes)
  @property({ type: Object }) user = {};
  
  render() {
    return html`
      <div>
        <p>Name: ${this.name}</p>
        <p>Age: ${this.age}</p>
        <p>Admin: ${this.isAdmin}</p>
      </div>
    `;
  }
}

@state (Internal State)

@customElement('toggle-button')
export class ToggleButton extends LitElement {
  @state() private _open = false;
  
  render() {
    return html`
      <button @click=${this._toggle}>
        ${this._open ? 'Close' : 'Open'}
      </button>
      ${this._open ? html`<div>Content</div>` : ''}
    `;
  }
  
  private _toggle() {
    this._open = !this._open;
  }
}

4. Templates

Basic Templating

render() {
  const items = ['Apple', 'Banana', 'Orange'];
  
  return html`
    <h1>Fruits</h1>
    <ul>
      ${items.map(item => html`<li>${item}</li>`)}
    </ul>
  `;
}

Conditional Rendering

render() {
  return html`
    ${this.loading
      ? html`<p>Loading...</p>`
      : html`<p>Data loaded!</p>`
    }
  `;
}

Event Listeners

@customElement('click-counter')
export class ClickCounter extends LitElement {
  @state() count = 0;
  
  render() {
    return html`
      <button @click=${this._increment}>
        Clicked ${this.count} times
      </button>
    `;
  }
  
  private _increment() {
    this.count++;
  }
}

5. Styling

Static Styles

@customElement('styled-element')
export class StyledElement extends LitElement {
  static styles = css`
    :host {
      display: block;
      padding: 16px;
      background: #f0f0f0;
    }
    
    button {
      background: blue;
      color: white;
      border: none;
      padding: 8px 16px;
      cursor: pointer;
    }
    
    button:hover {
      background: darkblue;
    }
  `;
  
  render() {
    return html`<button>Styled Button</button>`;
  }
}

Dynamic Styles

import { styleMap } from 'lit/directives/style-map.js';

@customElement('dynamic-styles')
export class DynamicStyles extends LitElement {
  @property() color = 'blue';
  
  render() {
    const styles = { color: this.color, fontWeight: 'bold' };
    
    return html`
      <p style=${styleMap(styles)}>Dynamic color!</p>
    `;
  }
}

Class Maps

import { classMap } from 'lit/directives/class-map.js';

render() {
  const classes = {
    active: this.isActive,
    disabled: this.isDisabled,
  };
  
  return html`
    <div class=${classMap(classes)}>Content</div>
  `;
}

6. Lifecycle Methods

@customElement('lifecycle-demo')
export class LifecycleDemo extends LitElement {
  // Called when element is added to DOM
  connectedCallback() {
    super.connectedCallback();
    console.log('Connected');
  }
  
  // Called when element is removed from DOM
  disconnectedCallback() {
    super.disconnectedCallback();
    console.log('Disconnected');
  }
  
  // Called before update
  willUpdate(changedProperties) {
    console.log('Will update', changedProperties);
  }
  
  // Called after update
  updated(changedProperties) {
    console.log('Updated', changedProperties);
  }
  
  // Called on first update
  firstUpdated(changedProperties) {
    console.log('First update', changedProperties);
  }
}

7. Directives

repeat

import { repeat } from 'lit/directives/repeat.js';

@property() users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
];

render() {
  return html`
    <ul>
      ${repeat(
        this.users,
        (user) => user.id,
        (user) => html`<li>${user.name}</li>`
      )}
    </ul>
  `;
}

when

import { when } from 'lit/directives/when.js';

render() {
  return html`
    ${when(
      this.loading,
      () => html`<p>Loading...</p>`,
      () => html`<p>Loaded!</p>`
    )}
  `;
}

until (Async)

import { until } from 'lit/directives/until.js';

async fetchData() {
  const res = await fetch('/api/data');
  return res.json();
}

render() {
  return html`
    ${until(
      this.fetchData().then(data => html`<p>${data.message}</p>`),
      html`<p>Loading...</p>`
    )}
  `;
}

8. Real-World Example: Todo List

import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';

interface Todo {
  id: number;
  text: string;
  done: boolean;
}

@customElement('todo-list')
export class TodoList extends LitElement {
  static styles = css`
    :host {
      display: block;
      max-width: 400px;
      margin: 0 auto;
    }
    
    .todo-item {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 8px;
      border-bottom: 1px solid #eee;
    }
    
    .done {
      text-decoration: line-through;
      opacity: 0.6;
    }
    
    input[type="text"] {
      flex: 1;
      padding: 8px;
    }
  `;
  
  @state() private _todos: Todo[] = [];
  @state() private _newTodo = '';
  
  render() {
    return html`
      <h2>Todo List</h2>
      
      <form @submit=${this._addTodo}>
        <input
          type="text"
          .value=${this._newTodo}
          @input=${this._handleInput}
          placeholder="Add a todo..."
        />
        <button type="submit">Add</button>
      </form>
      
      <ul>
        ${repeat(
          this._todos,
          (todo) => todo.id,
          (todo) => html`
            <li class="todo-item">
              <input
                type="checkbox"
                .checked=${todo.done}
                @change=${() => this._toggleTodo(todo.id)}
              />
              <span class=${todo.done ? 'done' : ''}>
                ${todo.text}
              </span>
              <button @click=${() => this._deleteTodo(todo.id)}>
                Delete
              </button>
            </li>
          `
        )}
      </ul>
    `;
  }
  
  private _handleInput(e: InputEvent) {
    this._newTodo = (e.target as HTMLInputElement).value;
  }
  
  private _addTodo(e: Event) {
    e.preventDefault();
    if (!this._newTodo.trim()) return;
    
    this._todos = [
      ...this._todos,
      { id: Date.now(), text: this._newTodo, done: false }
    ];
    this._newTodo = '';
  }
  
  private _toggleTodo(id: number) {
    this._todos = this._todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    );
  }
  
  private _deleteTodo(id: number) {
    this._todos = this._todos.filter(todo => todo.id !== id);
  }
}

9. Using with React

// React component
import { useRef, useEffect } from 'react';
import './my-element'; // Import Lit component

function App() {
  const ref = useRef();
  
  useEffect(() => {
    const el = ref.current;
    el.addEventListener('custom-event', (e) => {
      console.log(e.detail);
    });
  }, []);
  
  return <my-element ref={ref} name="React" />;
}

10. Best Practices

1. Use @state for Internal State

// Good: private state
@state() private _count = 0;

// Bad: public property for internal state
@property() count = 0;

2. Avoid this.shadowRoot Access

// Good: use @query
@query('#myButton') button!: HTMLButtonElement;

// Bad: manual DOM access
this.shadowRoot.querySelector('#myButton');

3. Use Directives for Complex Logic

// Good: use repeat directive
${repeat(items, (item) => item.id, renderItem)}

// Bad: array.map() without keys
${items.map(renderItem)}

Summary

Lit makes web components simple and powerful:

  • Lightweight - only 5KB gzipped
  • Standard - built on Web Components
  • Fast - efficient reactive updates
  • Universal - works with any framework
  • TypeScript - excellent type support

Key Takeaways:

  1. Use @property for public API, @state for internal
  2. Templates with html tagged template
  3. Styles with css tagged template
  4. Lifecycle methods for complex logic
  5. Works everywhere - vanilla JS, React, Vue

Next Steps:

  • Learn [Web Components](/en/blog/web-components-complete-guide/
  • Try [Preact](/en/blog/preact-complete-guide/
  • Build with [Alpine.js](/en/blog/alpine-js-complete-guide/

Resources:


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Complete Lit guide for building web components. Learn reactive properties, templating, lifecycle, and building reusable … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [esbuild Complete Guide | Fastest JavaScript Bundler](/en/blog/esbuild-complete-guide/
  • [Preact Complete Guide | Fast 3KB React Alternative](/en/blog/preact-complete-guide/
  • [Web Components Complete Guide | Native Custom Elements](/en/blog/web-components-complete-guide/

이 글에서 다루는 키워드 (관련 검색어)

Lit, Web Components, JavaScript, Google, Frontend, Custom Elements 등으로 검색하시면 이 글이 도움이 됩니다.