본문 바로가기

FrontEnd/React

[React] 이벤트 속성 이해하기

이벤트란?

 UI에서 버튼을 누르거나 텍스트를 입력하는 등의 행위가 일어날 때 이벤트(event)가 발생했다고 한다.

 

 Event 타입

/** 이름이 click(type 속성 값이 click')인 Event 객체를 생성하는 예 */
new Event('click', {bubbles : true })

   

  웹 브라우저의 자바스크립트 엔진은 Event 타입을 제공한다.

  • type : 이벤트 이름으로 대소문자를 구분하지 않는다.
  • isTrusted : 이벤트가 웹 브라우저에서 발생(true) / 프로그래밍에서 발생(false)를 판단한다.
  • target : 이벤트가 처음 발생한 HTML 요소이다.
  • currentTarget : 이벤트 버블링과 관계없이 현재 이벤트가 설정된 요소를 반환한다.
  • bubbles : 이벤트가 DOM을 타고 버블링될지 여부를 결정한다.

 EventTarget 타입

HTMLElement 부모 인터페이스 상속 구조 예시

  모든 HTML의 요소는 HTMLElement 상속 타입을 가지고 있다.   위의 그림과 같이 최상위 EventTarget타입을 시작으로 Node, Element와 같은 타입을 상속한다.  그러므로 모든 HTML요소는 EventTarget타입이 정의하는 속성과 메서드를 포함하고 있다.

 

 이벤트 처리기

  EventTarget은 addEventListener, removeEventListener, dispatchEvent라는 메서드 3개를 제공한다.  add를 제외한 EventListener는 '이벤트 + 귀기울여 듣기'를 뜻한다.  프로그래밍에서 '귀 기울여 듣기'를 구현하는 메커니즘은 콜백(callback) 함수이다.  이벤트를 기다리는 콜백 함수는 좀 더 간결하게 이벤트 처리기(event handler)라고 한다.  이벤트 처리기는 이벤트 발생시까지 대기하다 이벤트 발생 시 해당 이벤트를 코드쪽으로 알려주는 역할을 한다.

DOM_객체.addEventListener(이벤트_이름:string, 콜백_함수: (e: Event) => void)

 

  이제 add까지 추가해 생각하면 addEventListener 메서드는 이벤트 처리기를 추가한다는 의미이며, 하나의 이벤트에 이벤트 처리기를 여러 개 부착할 수 있다는 것을 뜻한다.

/** window객체는 Window타입이고 Window 타입은 EventTarget타입을 상속한다. */
/** window객체는 EventTarget을 상속 받았으므로 addEventListener를 사용할 수 있다. */
window.addEventListener('clcik', (e: Event) => console.log('mouse click occurs.'))

 

  하단의 코드에서는 옵셔널 체이닝(optional chaining) 연산자 ?. 를 사용하는데, 이는 getElementById 메서드가 null값을 반환할 수도있을 것을 대비해서 사용한다.  만약 값이 null일경우 addEventListener 메서드를 호출하지 않아 오류가 발생하지 않는다.

document.getElementById('root')?.addEventListener('click', (e: Event) => {
    const {isTrusted, target, bubbles} = e
    console.log('mouse click occurs.', isTrusted, target, bubbles)
})

 

 

// EventListener.tsx
// isTrusted : 웹브라우저에서 실행 시 true반환
// target : 마우스로 클릭한 대상
// bubbles : 버블링여부
document.getElementById('root')?.addEventListener('click', (e: Event) => {
  const {isTrusted, target, bubbles} = e
  console.log('mouse click occurs.', isTrusted, target, bubbles)
})

document.getElementById('root')?.addEventListener('click', (e: Event) => {
  const {isTrusted, target, bubbles} = e
  console.log('mouse click also occurs.', isTrusted, target, bubbles)
})

export default function EventListener() {
  return <div>EventListener</div>
}

 

  웹 브라우저에서 id가 'root'인 영역안을 마우스로 클릭할 경우 이벤트가 발생하는걸 확인할 수 있다.

실행결과 : addEventListener를 통한 click이벤트 설정

 

 물리 DOM 객체의 이벤트 속성

  addEventListener메서드는 사용법이 번거롭다.  이 때문에 window를 포함한 대부분의 HTML요소는 onclick처럼 'on'뒤에 이벤트이름을 붙인 속성을 제공한다.  참고로 옵셔널 체이닝 연산자는 사용할 수 없다.

const rootDiv = document.getElementById('root')
if (rootDiv) {
  // 옵셔널체이닝이 불가능하므로 rootDiv의 유효성 검사처리
  rootDiv.onclick = (e: Event) => {
    const {isTrusted, target, bubbles} = e
    console.log('mouse click occurs on rootDiv', isTrusted, target, bubbles)
  }

  rootDiv.onclick = (e: Event) => {
    const {isTrusted, target, bubbles} = e
    console.log('mouse click also occurs on rootDiv', isTrusted, target, bubbles)
  }
}
export default function Onclick() {
  return <div>OnClick</div>
}

실행결과로 Eventlistener를 마우스로 클릭한 결과 : AddEventListener와 달리 onclick이벤트는 단 한번만 호출되었다.

 

  onClick이벤트는 AddEventListener와 달리 가장 마지막에 설정된 콜백 함수를 호출한다.

 

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

 리액트 컴포넌트도 자바스크립트와 비슷하게 on이벤트명 형태로 된 HTML 요소의 이벤트 속성을 제공한다.  차이점은 HTML요소의 이벤트 속성은 모두 소문자지만, 리액트 코어 컴포넌트의 속성은 onClick, onMouseEnter같이 소문자로 시작하는 카멜 표기법을 채택하고 있다.  매개변수 였던 e의 타입도 Event가 아닌 합성이벤트(syntheticEvent) 타입으로 설정해야 한다.

 

// SyntheticEvent 선언문
interface SyntheticEvent<T = Element, E = Event> extends BaseSyntheticEvent<E, Event-Target & T, EventTarget> {}

  

  SystheticEvent는 BaseSyntheticEvent를 상속하는 타입이다.

 

// BaseSyntheticEvent의 주요내용
interface BaseSysntheticEvent<E = object, C= any, T = any> {
    nativeEvent: E;
    currentTarget: C;
    target: T;
    preventDefault(): void;
    stopPropagation(): void;
}

 

   리액트는 물리 DOM에서 발생하는 이벤트를 네이티브 이벤트라고 한다. 

  • nativeEvent : 물리 DOM에서 발생하는 Event의 세부 타입인 PointerEvent와 같은 이벤트 객체를 저장에 사용된다.
  • currentTarget : 이벤트 버블링 과정에서 현재 이벤트를 설정한 DOM 객체를 알고 싶을 때 사용한다.
  • preventDefault : 어떤 사용자 액션에 따라 이벤트 발생 시 해당 이벤트와 관련된 웹 브라우저의 기본 구현 내용을 실행하지 않게 막는다.
  • stopPropagation() : 이벤트 버블링이 발생하지 않도록 멈춰주는 역할을 수행한다.
import type {SyntheticEvent} from 'react'

export default function ReactOnClick() {
  const onClick = (e: SyntheticEvent) => {
    const {isTrusted, target, bubbles} = e
    console.log('mouse click occurs on <button>', isTrusted, target, bubbles)
  }
  return (
    <div>
      <p>ReactOnClick</p>
      <button onClick={onClick}>Click Me</button>
    </div>
  )
}

웹 브라우저에서의 리액트 프레임워크 이벤트 속성의 사용결과

 

 EventTarget의 dispatchEvent 메서드

  DOM의 최상위 타입인 EventTarget은 dispatchEvent 메서드를 제공한다.

dispatchEvent(event: Event): boolean;  // dispatchEvent
new Event('click', { bubbles : true})  // 이벤트 객체를 생성

/** dispacthEvent와 click은 완전히 똑같이 동작한다. */
타깃_DOM_객체.dispatchEvent(new Event('click', {bubbles: true}))  
타깃_DOM_객체.click()

 

  위와 같이 Event 타입 객체는 Event나 SyntheticEvent의 target 속성값이 되는 타킷_DOM_객체의 dispatchEvent메서드를     호출해 이벤트를 발생시킬 수 있다.  다만 모든 DOM객체의 부모 타입인 HTMLElement는 click 메서드를 제공 하고있다.

  이는 click 메서드가 dispacthEvent 코드로 구현 되었음을 짐작할 수 있다.

export default function DispatchEvent() {
  const onCallDispatchEvent = () => {
    console.log('onCallDispatchEvent')
    // new Event인 javascript로 발생시킨 이벤트는 click 이벤트의 속성 중 
    // isTrusted의 결과값이 false로 반환됨.
    document.getElementById('root')?.dispatchEvent(new Event('click', {bubbles: true}))
  }
  const onCallClick = () => {
    console.log('onCallClick')
    document.getElementById('root')?.click()
  }
  return (
    <div>
      <p>DispatchEvent</p>
      <button onClick={onCallDispatchEvent}>onCallDispatchEvent</button>
      <button onClick={onCallClick}>onCallClick</button>
    </div>
  )
}

 

DispatchEvent 실행결과를 확인

 

  여기서 특이점은 Event 타입의 isTrusted속성은 이벤트가 웹 브라우저에서 실행한 것인지 여부를 확인한다고 했었다.

 dispatchEvent와 click메서드로 발생한 이벤트는 isTrusted값이 false인 것을 볼 수 있다.

 

 이벤트 버블링

이벤트 버블링과 이벤트 캡처링

 

  이벤트 버블링(event bubbling)이란 자식 요소에서 발생한 이벤트가 가까운 부모 요소에서 가장 먼 부모 요소까지 계속 전달되는   현상을 의미한다.  반대의 이미로는 캡쳐링이 존재한다. 

  이벤트 버블링이 발생하면 이벤트가 직접 발생한 onButtonClick에서는 e.CurrentTarget값이 null이지만, 부모 요소의   onDivClick에서는 e.currentTarget값이 <div>의 DOM 객체로 설정된다.

import type {SyntheticEvent} from 'react'

export default function EventBubbling() {
  const onDivClick = (e: SyntheticEvent) => {
    const {isTrusted, target, bubbles, currentTarget} = e
    console.log('click event bubbles on <div>', isTrusted, target, bubbles, currentTarget)
  }
  const onButtonClick = (e: SyntheticEvent) => {
    const {isTrusted, target, bubbles} = e
    console.log('click event start at <button>', isTrusted, target, bubbles)
  }
  return (
    <div onClick={onDivClick}>
      <p>EventBubbling</p>
      <button onClick={onButtonClick}>Click Me</button>
    </div>
  )
}

EventBubbling 실행결과

 

  click Me 버튼을 선택시 버튼이벤트가 우선 실행되며 그 이후로 div부착된 onclick이벤트인 onDivClick이벤트가 수행된다. 

 이벤트의 target값은 <button>이지만 currentTarget값은 <div>로 서로 다르다.

 currentTarget은 이벤트의 현재 대상을 의미하며 이말은 즉, 현재 이벤트가 위치한 객체를 가르킨다는 뜻이다.

 

 stopPropagation 메서드와 이벤트 켑처링

  이벤트 버블링이 발생하지 않도록 중단하고 싶을때는 SyntheticEvent의 부모인 BaseSyntheticEvent 타입으로 제공하는 stopPropagation 메서드를 사용한다.  이 메서드는 가까운 부모에서 먼 부모 쪽으로 이벤트가 버블링되며 전달되는 것을 멈춘다.  이를 이벤트 캡처링(event capturing) 이라고 한다.

import type {SyntheticEvent} from 'react'

export default function SyntheticEvent() {
  const onDivClick = (e: SyntheticEvent) => {
    console.log('click event bubbles on <div>')
  }
  const onButtonClick = (e: SyntheticEvent) => {
    const {isTrusted, target, bubbles} = e
    console.log('click event start at <button>', isTrusted, target, bubbles)
    e.stopPropagation()
  }
  return (
    <div onClick={onDivClick}>
      <p>StopPropagation</p>
      <button onClick={onButtonClick}>Click Me</button>
    </div>
  )
}

stopPropagation 실행결과 : button을 선택한 결과로 버튼 이벤트까지만 실행된 것을 확인할 수 있다.

 

 <input> 요소의 이벤트 처리

export default function VariousInputs() {
  return (
    <div>
      <p>VariousInputs</p>
      <div>
        <input type="text" placeholder="enter some texts" />
        <input type="password" placeholder="enter tour password" />
        <input type="email" placeholder="enter email address" />
        <input type="range" />
        <input type="button" value="I'm a button" />
        <input type="checkbox" value="I'm a checkbox" defaultChecked />
        <input type="radio" value="I'm a radio" defaultChecked />
        <input type="file" />
      </div>
    </div>
  )
}

<input 요소>의 실행결과 : 화면상에 표기되는 type별 각각의 input 모양을 확인할 수 있다.

 

 <input>의 onChange 이벤트 속성

  <input> 요소에 마우스 클릭이 발생하면 <button>과 마찬가지로 click 이벤트가 발생한다.  다만 텍스트라면 change이벤트가 발생하고, 이 change 이벤트는 onChange 이벤트 속성으로 얻을 수 있다.

 

  아래 ChangeEvent<T> 선언문을 보면 SyntheticEvent에 target 이름의 속성을 추가한 타입임을 알 수 있다.  여기서 타입 변수는 EventTarget을 상속한 HTMLElement나 HTMLInputElement와 같은 DOM 타입이여야 한다.

// 제네릭 타입(generic type) ChangeEvent<T> 선언문
interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
    target : EventTarget & T;
}

 

 

  <input>의 onchange 이벤트 속성에 onchange라는 이름의 이벤트 처리기를 설정했다.  이벤트 처리기의 매개변수 타입을   ChangeEvent<HTMLInputElement>로 설정했기 때문에 HTMLInputelement 타입의 물리 DOM 객체 값을 e.target 형태로     얻어올 수 있다.

impport type {ChangeEvent} from 'react'

export default function OnChange() {
    const onChange = (e: ChangeEvent<HTMLInputElement>) => {
        console.log('onChange', e.target.value)
    }
    return <input type="text" onChange={onChange}
}

 

 <input> 요소의 이벤트 관련 속성들

  <input> 요소가 제공하는 속성들을 React.InputHTMLAttributes<HTMLInputElement> 형태로 얻을 수 있다.

// input 요소 정의
input : React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>

 

 

 InputHTMLAttributes<T>의 속성 가운데 onChange 이벤트와 관련 속성이다.  type 속성값이 'checkbox', 'radio'이면 checked 속성 값으로, 'text', 'email', 'password', 'range'면 value 속성 값으로, 'file'이면 files 속성 값으로 사용자가 입력한 구체적인 내용을 얻을 수 있다.

// InputHTMLAttributes<T>의 속성 가운데 onChange 이벤트와 관련 속성
interface InputHTMLAttributes<T> extends HTMLAttributes<T> {
    checked?: boolean | undefined;
    value?: string | ReadonlyArray<string> | number | undefined;
    files: FileList | null;
    onChange?: ChangeEventHandler<T> | undefined;
    ... (생략) ...
}

 

 <input>의 defaultValue와 defaultChecked 속성

  <input> 요소는 vlaue와 checked 속성 외 defaultValue와 defaultChecked속성도 제공한다.  두 속성은 초기값을 설정하는       용도로 제공한다.

 

 Onchange 컴포넌트 구현하기

  <input> type이 'text'일경우에는 e.target.value, 'checkbox'일땐 e.target.checked, 'file'일땐 e.target.files 형태로 입력결과를   얻을 수 있다.  여기서 e.target.files의 속성 타입은 FileList 객체이며 리액트가 아닌 웹 브라우저의 자바스크립트 엔진이 제공한다.

import type {ChangeEvent} from 'react'

export default function Onchange() {
  const onChangeValue = (e: ChangeEvent<HTMLInputElement>) => {
    e.stopPropagation() // 이벤트 버블링 제거
    e.preventDefault() // 웹 브라우저의 기본 구현내용을 실행하지 않게 제거한다.
    console.log('onChangeValue', e.target.value)
  }

  const onChangeChecked = (e: ChangeEvent<HTMLInputElement>) => {
    e.stopPropagation() // 이벤트 버블링 제거
    console.log('onChangeChecked', e.target.checked)
  }

  const onChangeFiles = (e: ChangeEvent<HTMLInputElement>) => {
    e.stopPropagation() // 이벤트 버블링 제거
    console.log('onChangeFiles', e.target.files)
  }

  return (
    <div>
      <p>Onchange</p>
      <input
        type="text"
        onChange={onChangeValue}
        placeholder="type some text"
        defaultValue="Hello"
      />
      <input type="checkbox" onChange={onChangeChecked} defaultChecked />
      <input type="file" onChange={onChangeFiles} multiple accept="images/*" />
    </div>
  )
}

OnChange 컴포넌트 구현 실행결과 : input 관련 요소와 이벤트 결과 정리

 

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

  input의 onChange의 값은 e.target.files로 얻어올 수 있으며 웹브라우저의 자바스크립트 엔진이 제공한다.  FileList의 item과 인덱스 연산자[]는 File 속성값을 얻을 수 있도록 고안되었다.  또한 자바스크립트 엔진은 Blob타입과 Blob타입을 확장한 File타입도 제공한다.

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

const files: FileList | null = e.target.files
if(files) {
    for(let i = 0; i < files.length; i++) {
        const file: File | null = files.item(i)   // 혹은 file = files[i]
        console.log(`file[${i}]: `, file)
    }
}

 

 

  onchage 실행결과로 File타입 데이터의 세부 내용을 출력할 수 있다.  name속성으로 이름을, size와 type속성으로 파일의 크기와 유형 등을 얻을 수 있다.

import type {ChangeEvent} from 'react'

export default function FileInput() {
  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    const files: FileList | null = e.target.files
    if (files) {
      for (let i = 0; i < files.length; i++) {
        const file: File | null = files.item(i) // or file = files[i];
        console.log(`file[${i}]`, file)
      }
    }
  }
  return (
    <div>
      <p>FileInput</p>
      <input type="file" onChange={onChange} multiple accept="image/*" />
    </div>
  )
}

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

 

 

 드래그 앤 드롭 이벤트 처리

  모든 HTMLElement 상속 요소는 dragable이라는 boolean 타입 속성을 제공한다.  해당 요소 적용 시 "마우스 클릭 후 드래그"를 통해 dragable할 수 있다.  하단의 표는 드래그 앤 드롭 이벤트의 종류이다.

종류 발생 시기 리액트 이벤트 
속성 이름
dragenter  마우스가 대상 객체의 위로 처음 진입할 때 발생 onDragEnter
dragstart 사용자가 객체(object)를 드래그하려고 시작할 때 발생 onDragStart
drag 대상 객체를 드래그하면서 마우스를 움직일 때 발생 onDrag
dragover 드래그하면서 마우스가 대상 객체의 영역 위에 자리 잡고 있을 때 발생 onDragOver
dragleave 드래그가 끝나서 마우스가 대상 객체의 위에서 벗어날 때 발생 onDragLeave
dragend 드래그가 끝나서 마우스가 대상 객체의 위에서 벗어날 때 발생 onDragEnd
drop 드래그가 끝나서 드래그하던 객체를 놓는 장소에 위치한 객체에서 발생. 리스너는 드래그된 데이터를 가져와서 드롭 위치에 놓는 역할을 한다. onDrop

 


  리액트는 드래그 앤 드롭 효과와 관련해 다음처럼 DragEvent 타입을 제공한다.  DragEvent타입에서 가장 중요한 속성은 dataTransfer이다.  dataTransfer 속성은 다음처럼 DataTransfer 속성을 가지는데,  파일이 드롭 되었을 때 files속성으로 들보한 파일의 정보를 알 수 있다.

// 리액트가 제공하는 DragEvent 타입
interface DragEvent<T = Element> extends MouseEvent<T, NativeDragEvent> {
    dataTransfer : DataTransfer;
}

// DataTransfer 속성
interface DataTransfer {
    files: FileList
    ... (생략) ...
}

 

 DragDrop 컴포넌트 구현하기

import type {DragEvent} from 'react'

export default function DropDrop() {
  const onDragStart = (e: DragEvent<HTMLElement>) =>
    console.log('onDragStart', e.dataTransfer)
  const onDragEnd = (e: DragEvent<HTMLElement>) =>
    console.log('onDragEnd', e.dataTransfer)

  // 이벤트 수행 시 웹 브라우저의 기본 이벤트를 발생하지 않도록 제어
  const onDragOver = (e: DragEvent) => e.preventDefault()
  const onDrop = (e: DragEvent) => {
    e.preventDefault()
    console.log('onDrop', e.dataTransfer)
  }
  return (
    <div>
      <p>DropDrop</p>
      <div draggable onDragStart={onDragStart} onDragEnter={onDragEnd}>
        <h1>Drag Me</h1>
      </div>
      <div onDrop={onDrop} onDragOver={onDragOver}>
        <h1>Drop over Me</h1>
      </div>
    </div>
  )
}

DragDrop 컴포넌트 구현하기

 

 

 FileDrop 컴포넌트 구현하기

import type {DragEvent} from 'react'

export default function FileDrop() {
  // 이벤트 수행 시 웹 브라우저의 기본 이벤트를 발생하지 않도록 제어
  const onDragOver = (e: DragEvent) => e.preventDefault()

  const onDrop = (e: DragEvent) => {
    e.preventDefault() // 새창에 드롭한 이미지가 나타나는 것을 방지한다.
    const files = e.dataTransfer.files
    if (files) {
      for (let i = 0; i < files.length; i++) {
        const file: File | null = files.item(i)
        console.log(`file[${i}]`, file)
      }
    }
  }

  return (
    <div>
      <p>FileDrop</p>

      <div onDrop={onDrop} onDragOver={onDragOver}>
        <h1>Drop Image files over Me</h1>
      </div>
    </div>
  )
}

브라우저에 이미지 파일을 드랍한 실행 결과