이벤트 속성 이해하기

김동현·2023년 8월 9일
0

물리 DOM 객체의 이벤트 속성

document.addEventListener('click', (e)=>{
    console.log(e);
})

첫 번째 매개변수 타입에 따라 두 번째 매개변수인 콜백함수의 매개변수로 들어가는 이벤트 객체가 달라진다.

e 매개변수의 타입은 Event 타입을 상속받는 어떠한 이벤트 객체이다.

document.addEventListener('click', (e:Event)=>{
    console.log(e); // ok
  	console.log(e.x); // Error
})

클릭 이벤트일 경우 MouseEvent가 매개변수로 들어간다.
부모 클래스인 Event로 타입 애너테이션 설정을 해도 되지만 이 경우 MouseEvent의 속성을 사용하려면 타입 에러가 발생한다.
만약 Event객체의 속성 및 메서드만 이용할꺼면 Event 타입으로 타입 애너테이션을 설정해도 된다.
부모 자식 관계이기 때문이다.

리액트 프레임워크의 이벤트 속성

리액트 컴포넌트도 HTML 요소의 on이벤트명 과 같은 이벤트 속성들을 제공한다.
HTML 요소의 이벤트 속성은 모두 소문자지만 리액트 코어 컴포넌트의 속성은 카멜 표기법을 사용한다.

on이벤트명 으로 제공되는 속성의 타입을 확인해보자.
확인하기 위해서는 아래처럼 마우스를 onClick에 가져다 대면 나온다.

onClickReact.DOMAttributes<HTMLDivElement> 타입의 선택적 속성인 것을 알 수 있다.
onClick 의 타입은 React.MouseEventHandler<HTMLDivElement> 이다.
제네릭 타입 인수로 HTMLDivElement 가 주어졌다는걸 인지하고 MouseEventHandler<T> 타입의 정의로 가보자.

type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;

MouseEventHandler<HTMLDivElement> 타입은 EventHandler<MouseEvent<HTMLDivElement>> 타입의 별칭이다.

EventHandler<E> 의 정의로 가보자.

type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void }["bivarianceHack"];

복잡해 보이는데 불필요한 부분을 제거하면 아래와 같다.

type EventHandler<E extends SyntheticEvent<any>> = (event: E) => void;

기존 코드에서 사용된 bivarianceHack 이나 유사한 방법들은 일부 레거시 라이브러리나 코드에서 여전히 사용되고 있기 때문에 사용되는 것이다.
걸러서 이해하면 된다.

즉, EventHandler<E> 타입은 타입이 E인 매개변수를 받아 void를 리턴하는 함수타입이다.
여기서 매개변수 E 에는 MouseEvent<HTMLDivElement> 가 대입된다.
단, MouseEvent<HTMLDivElement>SynthetivEvent<any> 타입을 확장한 타입이어야만 한다.

자, 정리해보자.
이벤트 속성 onClick 의 타입은 MouseEvent<HTMLDivElement> 를 인자로 받고 void 를 출력하는 함수타입이다.
실행이 잘 되는지 확인해보자.

function App() {
  const onClick = (event:MouseEvent<HTMLDivElement>) => {
    console.log(event);

  }
  return (
    <div className="App" onClick={onClick}>클릭</div>
  )
}

MouseEvent 부분에 빨간 밑줄이 보이면서 제네릭 타입이 아니라고 한다.
MouseEvent 가 다른곳에서도 선언이 되었나 보다.
내가 본 MouseEvent 는 정확히 말하면 React.MouseEvent 이다.
따라서 import를 해야한다.

import { MouseEvent } from 'react'
function App() {
  const onClick = (event:MouseEvent<HTMLDivElement>) => {
    console.log(event);

  }
  return (
    <div className="App" onClick={onClick}>클릭</div>
  )
}

제대로 동작한다.

이제 MouseEvent<HtmlDivElement> 의 속성을 알아보자.

interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> {
        altKey: boolean;
        button: number;
        buttons: number;
        clientX: number;
        clientY: number;
        ctrlKey: boolean;
        getModifierState(key: ModifierKey): boolean;
        metaKey: boolean;
        movementX: number;
        movementY: number;
        pageX: number;
        pageY: number;
        relatedTarget: EventTarget | null;
        screenX: number;
        screenY: number;
        shiftKey: boolean;
    }

MouseEvent<HtmlDivElement> 는 알고보니
MouseEvent<HtmlDivElement, NativeMouseEvent> 이고
UIEvent<HtmlDivElement, NativeMouseEvent> 을 확장한 타입이다.

interface UIEvent<T = Element, E = NativeUIEvent> extends SyntheticEvent<T, E> {
        detail: number;
        view: AbstractView;
    }

UIEvent<T,E> 타입은 SyntheticEvent<T,E> 타입을 확장한 타입이라고 한다.
여기저 잠깐
SyntheticEvent<T,E> 타입은 위에서 본 적이 있다.
onClick 속성에 대입되는 함수타입의 매개변수로 들어가는 타입들은 이 SyntheticEvent<T,E> 타입을 확장한 타입이어야만 한다.
SyntheticEvent<HtmlDivElement, NativeMouseEvent> 를 보자.

interface SyntheticEvent<T = Element, E = Event> extends BaseSyntheticEvent<E, EventTarget & T, EventTarget> {}

SyntheticEvent<T,E> 는 또 BaseSyntheticEvent<E, EventTarget & T, EventTarget> 타입을 확장한 타입이다.
BaseSyntheticEvent<NativeMouseEvent, EventTarget & HtmlDivElement, EventTarget> 가 된다.

interface BaseSyntheticEvent<E = object, C = any, T = any> {
        nativeEvent: E;
        currentTarget: C;
        target: T;
        bubbles: boolean;
        cancelable: boolean;
        defaultPrevented: boolean;
        eventPhase: number;
        isTrusted: boolean;
        preventDefault(): void;
        isDefaultPrevented(): boolean;
        stopPropagation(): void;
        isPropagationStopped(): boolean;
        persist(): void;
        timeStamp: number;
        type: string;
    }

이제 다왔다.
E 는 NativeMouseEvent 타입이고 C는 EventTarget & HtmlDivElement 타입이고 T는 EventTarget 타입이다.

리액트에서는 Event 타입이 아니라 BaseSyntheticEvent 가 루트 타입이다.
BaseSyntheticEventnativeEvent 속성은 물리 DOM에서 발생하는 Event의 세부타입인 PointerEvent 와 같은 이벤트 객체를 저장하는 데 사용한다.
currentTargetEventTarget & HtmlDivElement 를 나타낸다.
즉, 이벤트 핸들러가 달린 엘리먼트를 의미한다.

target 은 원래라면 현재 클릭한 엘리먼트를 의미한다.
하지만 타입을 보면 EventTarget 타입으로 선언되어 있다.
모든 HTML 엘리먼트들은 EventTarget 의 하위 클래스이다.
따라서 p 엘리먼트를 클릭하면 HTMLParagraphElement 타입이 EventTarget 타입인 target에 할당된다.
하지만 값이 뭐든간에 타입상으로은 EventTarget 이므로 EventTarget 의 속성에만 접근할 수 있다.
만약 HTMLParagraphElement 타입의 속성을 이용하고 싶다면 타입 어서션을 이용해야 한다.

preventDefault() , stopPropagation() 는 자바스크립트의 Event 객체와 같은 기능을 한다.

<input type="file"> 에서의 onChange 이벤트 처리

const FileInput = () => {
  return <input type='file' onChange={} multiple accept='image/*' />
}

onChange 속성에 마우스를 갖다대자.

ChangeEventHandler<HTMLInputElement> 타입이다.

type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>;

EventHandler<ChangeEvent<HTMLInputElement>> 타입이다.
EventHandler 타입은 위에서 살펴보았다.
기억안나면 앞으로 돌아가서 다시 보자.

여하튼 ChangeEventHandler<HTMLInputElement> 타입은 ChangeEvent<HTMLInputElement> 를 매개변수로 받아서 void 를 리턴하는 함수타입이다.

interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
        target: EventTarget & T;
    }

ChangeEventSyntheticEvent 타입을 확장한 타입이며 target 속성을 재정의했다.
BaseSyntheticEventtargetEventTarget 타입이었는데 재정의한 타입은 EventTarget & HTMLInputElement 교차 타입이다.

file 관련 속성은 SyntheticEvent<T> 에 없었으니 HTMLInputElement 타입에 있을 것이다.
들어가보자.

두 군데에 선언이 되어있다.
그중 global.d.ts에 선언된 HTMLInputElement 타입은 빈 타입이다.
이 타입은 리액트에서 제공하는 타입이 아니라 타입스크립트에서 제공하는 DOM 타입이다.

interface HTMLInputElement extends HTMLElement {
    ...
    files: FileList | null;
    ...

files 속성을 찾았다.
FileList 타입을 보자.

interface FileList {
    readonly length: number;
    item(index: number): File | null;
    [index: number]: File;
}

인덱스 시그니처를 사용해서 마치 배열처럼 보이는 타입이다.
File 타입을 보자.

interface File extends Blob {
    readonly lastModified: number;
    readonly name: string;
    readonly webkitRelativePath: string;
}

Blob 타입을 보자.

interface Blob {
    readonly size: number;
    readonly type: string;
    arrayBuffer(): Promise<ArrayBuffer>;
    slice(start?: number, end?: number, contentType?: string): Blob;
    stream(): ReadableStream<Uint8Array>;
    text(): Promise<string>;
}

어떤 구조인지 대충 보인다.
코드를 작성해보자.

import { ChangeEvent } from 'react'

const FileInput = () => {
  const onChange = (event:ChangeEvent<HTMLInputElement>) =>{
    const files: FileList | null = event.target.files;
    if(files !== null){
      for(let i = 0; i < files.length; i++){
        const file = files[i]
        console.log(file);
      }
    }
  }
  return <input type='file' onChange={onChange} multiple accept='image/*' />
}

파일이 바뀔때마다 file 객체를 출력하는 코드이다.
여러가지 파일 정보가 담겨있다.

profile
프론트에_가까운_풀스택_개발자

0개의 댓글