공부

[이펙티브TS] DOM 계층구조 이해하기 (아이템 55)

jaeeedev 2023. 8. 14. 20:26

브라우저 타입 오류

function handleDrag(eDown: Event) {
  const targetEl = eDown.currentTarget;
  targetEl.classList.add("dragging");
  // ~~~~~~~           Object is possibly 'null'.
  //         ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'
  const dragStart = [eDown.clientX, eDown.clientY];
  // ~~~~~~~                Property 'clientX' does not exist on 'Event'
  //                ~~~~~~~ Property 'clientY' does not exist on 'Event'
  const handleUp = (eUp: Event) => {
    targetEl.classList.remove("dragging");
    //  ~~~~~~~~           Object is possibly 'null'.
    //           ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'
    targetEl.removeEventListener("mouseup", handleUp);
    //  ~~~~~~~~ Object is possibly 'null'
    const dragEnd = [eUp.clientX, eUp.clientY];
    // ~~~~~~~                Property 'clientX' does not exist on 'Event'
    //              ~~~~~~~   Property 'clientY' does not exist on 'Event'
    console.log(
      "dx, dy = ",
      [0, 1].map((i) => dragEnd[i] - dragStart[i])
    );
  };
  targetEl.addEventListener("mouseup", handleUp);
  // ~~~~~~~ Object is possibly 'null'
}

 

const div = document.getElementById("surface");
div.addEventListener("mousedown", handleDrag);
// ~~~ Object is possibly 'null'

자바스크립트 상에서 문제없이 동작하는 코드지만 아주 많은 타입 오류가 나타난다.

 

계층구조 이해하기

<p id="quote">and <i>yet</i> it moves</p>

여기서 p 엘리먼트는 HTMLParagraphElement 타입이다.

const p = document.getElementsByTagName("p")[0];

p instanceof HTMLParagraphElement; // true

HTMLParagraphElementHTMLElement의 서브타입이고, HTMLElementElement의 서브타입이다.

ElementNode의 서브타입이고, NodeEventTarget의 서브타입이다.

DOM 계층의 타입들

타입 예시
EvnetTarget window, XMLHttpRequest
Node document, Text, Comment
Element HTMLElement, SVGElement
HTMLElement
<i><b>
HTMLButtonElement
<button>

event.currentTarget

function handleDrag(eDown: Event) {
  const targetEl = eDown.currentTarget;
  targetEl.classList.add("dragging");
  // ~~~~~~~           Object is possibly 'null'.
  //         ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'
  // ...
}

eventTarget은 DOM 타입 중 가장 추상화된 타입이다. 이벤트 리스너를 추가하거나 제거하고, 이벤트를 보내는 것만 가능하다.

eventcurrentTarget의 타입은 EventTarget | null이다. EventTarget 타입에는 classList 속성이 없어 오류가 표시된다. edown.currentTarget가 실제로는 HTMLElement 를 지칭한다고 하더라도 타입 관점에서는 window나 XMLHttpRequest가 될 수도 있다.

Node

Node 타입이지만 Element가 아닌 경우가 있다.

<p>
  And <i>yet</i> it moves
  <!-- quote from Galileo -->
</p>

가장 바깥의 엘리먼트는 HTMLParagraphElement이다. 그리고 children과 childNodes 속성을 가지고 있다.

> p.children
HTMLCollection [i]

> p.childNodes
NodeList(5) [text, i, text, comment, text]

children은 자식 엘리먼트(<i>yet</i>)를 포함하는 HTMLCollection이다. 반면 childNodes는 NodeList이다. childNodes는 엘리먼트(<i>yet</i>) 뿐 아니라 텍스트 조각과 주석까지 모두 포함하고 있다.

Element, HTMLElement

SVGElement는 SVG 태그의 전체 계층 구조를 포함하지만 HTML 은 아닌 엘리먼트이다. 이것은 Element에 포함된다.

HTMLxxxElement

이 형태의 엘리먼트들은 자신만의 고유한 속성을 가지고 있다. HTMLImageElementsrc 속성을 가지고 있고 HTMLInputElement에는 value 속성이 있다. 이런 속성들에 접근하기 위해서는 타입을 구체적으로 지정해 주어야 한다.

타입 단언

일반적으로 타입 단언문은 지양해야 하지만 DOM과 관련해서 타입스크립트가 정확한 타입을 찾아내지 못하는 경우가 있다.

// 보통은 잘 추론함

document.getElementsByTagName("p")[0]; // HTMLParagraphElement
document.createElement("button"); // HTMLButtonElement
document.querySelector("div"); // HTMLDivElement

// 추론이 잘 안되는 경우

document.getElementById("my-div"); // HTMLElement

후자의 경우 작성자가 타입에 대해 더 정확히 알고있기 때문에 타입 단언문을 작성할 수 있다.

document.getElementById("my-div") as HTMLDivElement;

strictNullChecks 가 설정된 경우 !로 해당 요소가 무조건 존재한다는 것을 단언해준다.

const div = document.getElementById("my-div")!;

하지만 요소가 null일 가능성이 있다면 if문으로 분기처리 해주어야 한다.

Event

function handleDrag(eDown: Event) {
  //...
  const dragStart = [
     eDown.clientX, eDown.clientY];
        // ~~~~~~~                Property 'clientX' does not exist on 'Event'
        //                ~~~~~~~ Property 'clientY' does not exist on 'Event'
  const handleUp = (eUp: Event) => {
    targetEl.classList.remove('dragging');
   //...
  }

Event 타입에도 별도의 계층 구조가 있다. Event는 가장 추상화 된 타입이고 더 구체적인 타입으로 UIEvent, MouseEvent 등이 있다.

handleDrag 함수의 매개변수는 Event인데 clientX와 clientY 속성은 MouseEvent 타입에 있기 때문에 오류가 발생한다.

수정된 코드

function addDragHandler(el: HTMLElement) {
  el.addEventListener("mousedown", (eDown) => {
    const dragStart = [eDown.clientX, eDown.clientY];
    const handleUp = (eUp: MouseEvent) => {
      el.classList.remove("dragging");
      el.removeEventListener("mouseup", handleUp);
      const dragEnd = [eUp.clientX, eUp.clientY];
      console.log(
        "dx, dy = ",
        [0, 1].map((i) => dragEnd[i] - dragStart[i])
      );
    };
    el.addEventListener("mouseup", handleUp);
  });
}

const div = document.getElementById("surface");
// 요소가 무조건 존재하는것을 아는 경우 addDragHandler(div!) 로 변경 가능
if (div) {
  addDragHandler(div);
}

addDragHandler 내부에 mousedown 이벤트 리스너, 핸들러를 만들면 타입스크립트가 문맥을 통해 타입을 정확하게 추론할 수 있게 된다. (기존의 handleDrag 함수는 addEventListner와 분리된 상태로 작성되어 mousedown 이벤트임을 추론할 수 없음)

이를 통해 대부분의 오류를 제거할 수 있다. 또한 Event가 아닌 MouseEvent 타입을 지정해 오류를 제거할 수 있다.