event.target과 event.currentTarget의 차이점

nearworld·2023년 4월 27일
0

typescript

목록 보기
25/28

자바스크립트에서 이벤트리스너를 사용할때 e.target.innerText 를 사용한 적이 있던 기억이 있었기에 타입스크립트에서 아래와 같은 코드가 정상작동하리라 생각했다.

function App() {
  const clickHandler = (e: MouseEvent<HTMLElement>) => {
    const target = e.target.innerText;
  };
  
  return (
    <div>
      App Component
    </div>
  )
}

위 코드에서는 innerText 부분에서 에러가 발생했는데 이유는 e.target의 타입이 EventTarget 인데 EventTarget 타입은 innerText 를 갖지 않고 있기 때문이다.

그래서 as 단언 방식으로 타입스크립트 컴파일러가 e.targetHTMLElement로 보게했다.
왜냐하면 HTMLElement 타입에는 innerText 속성을 가지고 있기 때문이다.

function App() {
  const clickHandler = (e: MouseEvent<HTMLElement>) => {
    const target = e.target as HTMLElement;
    const text = target.innerText;
  };
  
  return (
    <div>
      App Component
    </div>
  )
}

위 코드는 정상작동했다. 하지만 as를 쓰는게 꺼림칙했고 이렇게 innerText 에 접근하는 것이 옳은 것인지 의문도 들었다. 그래서 React의 타입 선언 파일을 확인하여 MouseEvent 타입에 대해 알아보고자 했다.

MouseEvent 타입은 아래와 같이 서브 타이핑을 구현하고 있었다.

interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> {}
interface UIEvent<T = Element, E = NativeUIEvent> extends SyntheticEvent<T, E> {}
interface SyntheticEvent<T = Element, E = Event> extends BaseSyntheticEvent<E, EventTarget & T, EventTarget> {}
interface BaseSyntheticEvent<E = object, C = any, T = any> {
	nativeEvent: E;
	currentTarget: C;
	target: T;
}

위 코드의 마지막 인터페이스인 BaseSyntheticEvent를 보면 제네릭 타입 CEventTarget & T 임을 알 수 있다.

interface BaseSyntheticEvent<E = object, EventTarget & T, T = any> {
	nativeEvent: E;
	currentTarget: EventTarget & T;
	target: T;
}

서브 타입에서 들어오는 타입 인자를 제네릭 C에 풀어 써보면 위와 같이 된다.
더 풀어 써보면

interface BaseSyntheticEvent<E = object, EventTarget & HTMLElement, T = any> {
	nativeEvent: E;
	currentTarget: EventTarget & HTMLElement;
	target: T;
}

위의 형태가 됨을 알 수 있다.

여기까지 와서 EventTarget & HTMLElement 교차타입 연산이 어떻게 결과가 나오는지 생각해봐야 했다.

// node_modules/typescript/lib/lib.dom.d.ts

interface EventTarget {
  addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean): void;
  dispatchEvent(event: Event): boolean;
  removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
}

interface HTMLElement extends Element, DocumentAndElementEventHandlers, ElementCSSInlineStyle, ElementContentEditable, GlobalEventHandlers, HTMLOrSVGElement {
    accessKey: string;
    readonly accessKeyLabel: string;
    autocapitalize: string;
    dir: string;
    draggable: boolean;
    hidden: boolean;
    inert: boolean;
    innerText: string;
    lang: string;
    readonly offsetHeight: number;
    readonly offsetLeft: number;
    readonly offsetParent: Element | null;
    readonly offsetTop: number;
    readonly offsetWidth: number;
    outerText: string;
    spellcheck: boolean;
    title: string;
    translate: boolean;
    attachInternals(): ElementInternals;
    click(): void;
    addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
    addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
    removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
    removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}

타입스크립트에서 교차타입은 합집합이므로
두 타입의 교차타입이 innerText 속성을 가지고 있음을 알 수 있다.

function App() {
  const clickHandler = (e: MouseEvent<HTMLElement>) => {
    const text = e.currentTarget.innerText;
  };
  
  return (
    <div>
      App Component
    </div>
  )
}
profile
깃허브: https://github.com/nearworld

0개의 댓글