내부 버튼의 click을 처리한 다음, 컴포넌트 호스트에서 이름이 지정된 CustomEvent를 디스패치합니다. 소비 페이지는 해당 호스트에서 이벤트를 수신하고 event.detail을 읽습니다. 이 가이드에서는 Mac에서 완전한 로컬 브라우저 데모를 만들고, 각 리스너에 필요한 이벤트 옵션을 설명합니다.

소비자에게 필요한 알림 정의하기

일반 JavaScript 웹 컴포넌트에서 소비자는 사용자 지정 요소를 사용하는 페이지 또는 바깥쪽 컴포넌트입니다. 여기서 구체적인 작업은 컴포넌트의 섀도 DOM을 조회하지 않고도 내부 버튼이 활성화될 때 소비자가 반응하도록 하는 것입니다.

예제에서는 Tile A라는 레이블이 붙은 가상 타일을 사용합니다. 공개 이벤트는 tile-activate이며, itemId과 활성화 count을 전달합니다. CustomEvent는 이 애플리케이션 데이터를 위한 detail 필드를 제공합니다. 이벤트 이름은 대소문자를 구분합니다. 디스패치하는 코드와 리스너는 정확히 같은 이름을 사용해야 합니다. CustomEvent 생성자 문서.

이 선택 사항을 간단한 인터페이스 계약으로 정리하세요.

  • 이름: tile-activate.
  • 의미: 내부 버튼이 활성화되어 횟수가 업데이트되었습니다.
  • 페이로드: detail.itemId는 타일을 식별하고, detail.count은 업데이트된 횟수를 담습니다.
  • 발신 위치: 사용자 지정 요소 호스트가 알림을 디스패치합니다.
  • 전달: 직접 소비자와 조상 리스너가 이벤트를 수신할 수 있으며, 둘러싼 섀도 경계를 넘어서도 전달됩니다.

이 항목은 이 예제에 적용한 설계 선택입니다. 유용한 이벤트는 소비자가 관심을 두는 동작을 설명합니다. 따라서 컴포넌트 내부 마크업을 나중에 변경하더라도 같은 알림 인터페이스를 유지할 수 있습니다.

Mac에서 전체 예제를 만들고 열기

최신 데스크톱 브라우저와 일반 텍스트 편집기가 필요합니다. 이 단일 파일 예제에는 패키지 설치나 빌드 단계가 필요하지 않습니다. HTML 표준의 사용자 지정 요소 섹션에 설명된 플랫폼 패턴에 따라 HTMLElement를 확장하는 자율 사용자 지정 요소를 사용합니다.

TextEdit에서 파일 > 새로운 문서를 선택한 다음 포맷 > 일반 텍스트로 만들기를 선택합니다. 아래의 전체 문서를 붙여 넣으세요. 파일 > 저장을 선택하고 파일 이름을 component-click.html로 지정합니다. 메시지가 표시되면 Use .html을 선택하세요. 이는 TextEdit에서 HTML 파일을 만드는 방법에 대한 Apple의 안내입니다. Mac의 TextEdit에서 HTML 문서 작업하기.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Component click notification</title>
</head>
<body>
  <h1>Component click notification</h1>
  <section id="demo">
    <action-tile id="tile" item-id="tile-a"></action-tile>
    <p id="direct-status">Direct listener: waiting</p>
    <p id="parent-status">Parent listener: waiting</p>
  </section>

  <script>
    class ActionTile extends HTMLElement {
      constructor() {
        super();
        this._count = 0;
        this._button = null;
        this._root = this.attachShadow({ mode: "open" });
        this._handleClick = () => {
          this._count += 1;
          this.dispatchEvent(new CustomEvent("tile-activate", {
            detail: {
              itemId: this.getAttribute("item-id"),
              count: this._count
            },
            bubbles: true,
            composed: true
          }));
        };
      }

      connectedCallback() {
        if (!this._button) {
          this._button = document.createElement("button");
          this._button.type = "button";
          this._button.textContent = "Activate Tile A";
          this._root.append(this._button);
        }
        this._button.addEventListener("click", this._handleClick);
      }

      disconnectedCallback() {
        this._button.removeEventListener("click", this._handleClick);
      }
    }

    customElements.define("action-tile", ActionTile);

    const tile = document.getElementById("tile");
    const demo = document.getElementById("demo");
    const directStatus = document.getElementById("direct-status");
    const parentStatus = document.getElementById("parent-status");

    tile.addEventListener("tile-activate", (event) => {
      const { itemId, count } = event.detail;
      directStatus.textContent =
        `Direct listener: ${itemId}, activation ${count}`;
    });

    demo.addEventListener("tile-activate", (event) => {
      const { itemId, count } = event.detail;
      parentStatus.textContent =
        `Parent listener: ${itemId}, activation ${count}`;
    });
  </script>
</body>
</html>

저장한 파일을 브라우저에서 열고 Activate Tile A를 클릭하세요. 첫 결과로 Direct listener: tile-a, activation 1과 Parent listener: tile-a, activation 1이 표시되어야 합니다. 이후 클릭할 때마다 표시된 두 횟수가 모두 1씩 증가합니다. 이는 예제에서 예상되는 결과이며, 브라우저 테스트 결과를 보고한 것은 아닙니다.

코드에서 중요한 순서를 확인할 수 있습니다. 컴포넌트 상태를 업데이트하고, 알림을 만든 다음, this에서 디스패치하고, 소비자가 자체 인터페이스를 업데이트하도록 합니다. dispatchEvent()은 반환하기 전에 해당하는 리스너를 동기적으로 호출하므로, 소비자는 핸들러에서 업데이트된 횟수를 확인할 수 있습니다. dispatchEvent 문서.

올바른 버블링 및 섀도 경계 옵션 선택하기

디스패치 대상과 리스너의 위치에 따라 어떤 옵션이 필요한지가 결정됩니다. 호스트에 직접 연결한 리스너는 bubbles이 false여도 해당 호스트에서 디스패치된 이벤트를 수신합니다. 일반 조상 리스너에는 버블링이 필요합니다. 섀도 경계를 통과하려면 composed도 필요합니다. bubbles, composed, cancelable의 기본값은 모두 false입니다. DOM 표준: Event 인터페이스.

다음 의사결정 표는 이러한 전파 규칙을 바탕으로 작성했습니다. 캡처 없이 등록된 일반 리스너를 사용하며, 전파를 중단하는 핸들러는 없다고 가정합니다.

사용자 지정 이벤트가 시작되는 위치 소비자가 수신하는 위치 해당 경로의 설정
컴포넌트 호스트 같은 호스트 두 플래그 모두 true일 필요 없음
컴포넌트 호스트 같은 트리의 일반 조상 bubbles: true
내부 섀도 버튼 해당 섀도 루트 바깥의 일반 조상 bubbles: true, composed: true
다른 섀도 트리 내부의 컴포넌트 호스트 둘러싼 섀도 루트 바깥의 일반 조상 bubbles: true, composed: true

데모에서 this.dispatchEvent(...)는 호스트에서 알림을 시작합니다. 따라서 컴포넌트 자체의 섀도 루트를 벗어날 필요는 없습니다. composed: true를 설정하면 나중에 이 타일이 중첩될 경우 둘러싼 컴포넌트의 섀도 경계를 통과해 알림을 사용할 수 있습니다. 이러한 더 넓은 전달 범위는 공개 인터페이스의 일부로 의도적으로 정해야 합니다. Event composed 문서.

간단히 확인하려면 bubbles: true만 bubbles: false로 바꾸고 저장한 다음 페이지를 새로고침하세요. 활성화하면 직접 상태는 업데이트되지만 부모 상태는 Parent listener: waiting으로 남아야 합니다. 확인한 후 버블링 설정을 복원하세요. composed만 false로 변경해도 이 문서에서는 어느 경로도 섀도 경계를 통과하지 않으므로 두 리스너 모두 계속 작동합니다.

초기화 및 재연결 동작 유지하기

처음에는 이벤트 전달이 정상으로 보이다가 컴포넌트를 이동하거나 다시 연결한 후 알림이 중복될 수 있습니다. 생성, 연결, 정리 작업을 명확히 구분하세요.

생성자는 먼저 super()를 호출하고 상태를 초기화한 뒤 섀도 루트를 연결합니다. 렌더링은 connectedCallback()에서 수행하며, 기존 버튼 참조를 확인해 한 번만 실행되도록 합니다. 이러한 선택은 HTML 표준의 생성자 요구사항과 연결 콜백이 다시 실행될 때 반복되지 않아야 하는 초기화를 가드하라는 지침을 따릅니다. 사용자 지정 요소 생성자 및 반응 요구사항.

컴포넌트는 연결될 때 내부 클릭 리스너를 등록하고 연결이 끊길 때 제거합니다. 다시 연결되면 기존 버튼을 재사용하고 _count을 유지합니다. 연결 및 연결 해제 콜백은 요소가 문서에 들어오거나 문서에서 나갈 때 호출되는 수명 주기 훅입니다. 사용자 지정 요소 사용하기.

저장해 둔 _handleClick 함수가 중요합니다. 제거할 때는 등록에 사용한 것과 같은 함수와 같은 캡처 설정을 지정해야 합니다. 제거 호출에서 새로운 화살표 함수를 만들면 등록된 리스너를 지정할 수 없습니다. removeEventListener 문서.

더 큰 컴포넌트에서 window, document 또는 다른 외부 대상에 리스너를 추가한다면, 해당 리스너도 똑같이 명확하게 정리하세요. 수명 주기 변경은 알림의 의미와 분리해, 요소를 다시 연결하는 일이 사용자 동작을 뜻하지 않도록 하세요.

알림 누락 또는 반복 진단하기

이벤트 플래그를 변경하기 전에 화면에 표시되는 두 상태 줄을 사용해 문제 위치를 파악하세요. 두 줄을 통해 컴포넌트의 알림 문제와 조상에게 전달되는 문제를 구분할 수 있습니다.

  1. 두 상태 모두 바뀌지 않음: 디스패치와 등록에 사용한 이벤트 이름을 비교한 다음 사용자 지정 요소 정의와 버튼 핸들러가 실행되는지 확인하세요. 이 문서에서는 사용자가 버튼을 활성화하기 전에 소비자 리스너가 등록됩니다.
  2. 직접 상태만 바뀜: bubbles을 확인하세요. 전파를 중단하는 중간 핸들러가 있는지도 살펴보세요.
  3. 다른 컴포넌트 안에 중첩할 때만 전달 실패: 해당 경로의 섀도 경계를 파악하고 composed을 확인하세요.
  4. 재연결 후 횟수가 크게 증가함: 반복적인 리스너 등록을 확인하세요. 특히 요소가 연결될 때마다 새 익명 함수를 생성하는지 살펴보세요.

익명 함수는 소스 코드가 같아도 서로 별개의 리스너 객체입니다. 안정적인 함수 참조를 유지하면 등록과 정리를 더 쉽게 파악할 수 있습니다. addEventListener()는 리스너의 캡처 옵션도 구분하며, 이 옵션은 리스너가 실행되는 시점에 영향을 줍니다. addEventListener 문서.

브라우저가 디스패치하는 클릭은 composed 이벤트이므로 네이티브 click이 이미 섀도 트리 바깥까지 도달할 수 있습니다. 소비자에게 이름이 있는 컴포넌트 동작과 정의된 페이로드가 필요하다면 별도의 사용자 지정 이벤트가 유용합니다. 중복을 의도한 경우가 아니라면 두 알림을 같은 애플리케이션 동작으로 처리하지 마세요. Event composed 문서.

마지막으로, 알림과 진행 허용을 이벤트 설계에서 구분하세요. 이 예제는 디스패치 전에 횟수를 증가시키며 이벤트는 취소할 수 없도록 설정합니다. 소비자가 preventDefault()를 호출해도 업데이트를 되돌릴 수 없습니다. 취소 가능한 요청을 구현하려면 요청된 작업을 수행하기 전에 dispatchEvent()의 반환값을 확인하는 로직이 필요합니다. dispatchEvent 반환값.

이 작업의 전체 계약은 간단합니다. 내부 버튼 핸들러 하나가 상태를 업데이트하고, 이름이 지정된 사용자 지정 이벤트 하나가 호스트에서 나가며, 소비자는 문서화된 페이로드를 읽습니다. 먼저 직접 리스너가 작동하도록 한 다음, 포함하는 인터페이스에 실제로 필요한 전파 경로를 활성화하세요.

출처