React 이벤트 처리

Y·2021년 5월 11일
18
post-thumbnail

✍ React에서의 이벤트처리

html에서와의 차이점

1) React 이벤트 이름은 소문자 대신 camelCase를 사용
2) JSX에 문자열 대신 함수를 전달

html에서는 아래와 같이 이벤트를 넣었다면,

<button onclick="activateButton()">클릭하세요</button>

React에서는 이벤트이름을 onClick, onSubmit 등과 같이 camelCase로 설정한다는 차이점이 있습니다. event handler는 JSX 표기인 { }를 사용하여 연결합니다.

<button onClick={activateButton}>클릭하세요</button>

주의할 점

DOM 요소에만 이벤트 설정이 가능합니다. div, button, input, form, span 등의 DOM 요소에는 이벤트 설정이 가능하지만, 리액트의 컴포넌트에는 불가능합니다. 예를 들면 위와 같이 button이라는 DOM 요소에 이벤트를 설정했는데, 아래처럼 <Component>라는 리액트 컴포넌트에는 onClick을 달아서 우리가 의도한대로 이벤트가 실행되지 않습니다. 그냥 color, name과 같은 props를 전달해주는 것에 불과합니다.

function App() {
  const sayHi = function () {
    alert('hello');
  }
  return (
    <>
      <Component onClick={sayHi} name="홍길동" nickname="홍홍" />
    </>
  );
}

이벤트 핸들러 네이밍

  • Props의 경우 : 보통 onClick과 같이 on접두사를 지정합니다.
  • Function Names의 경우: 보통 handleClick과 같이 handle접두사를 지정합니다.
    이 두 경우를 함께 사용한 결과 코드는 이런 패턴을 가지겠죠
    <Component onClick={handleClick}/>

정리하면! on접두사가 붙었을때는 이 prop에 실제 이벤트가 연결되어 있으며, handle접두사가 붙은건 이벤트가 발생했을 때 호출되는 실제 function을 의미한다고 생각하면 됩니다.

✍ React 합성 이벤트(Synthetic Event)

  • 브라우저마다 이벤트 이름부터 시작해서 이벤트 종류나 이벤트가 처리되는 방식이 다르다. 이를 동일하게 처리하기 위해 React는 Synthetic 이벤트로 브라우저마다 다른 native 이벤트를 묶어서 처리한다. (크로스브라우징 문제 해결)
  • 즉, 이벤트 핸들러는 모든 브라우저에서 이벤트를 동일하게 처리하기 위한 이벤트 래퍼 SyntheticEvent 객체를 전달받는다. stopPropagation()preventDefault()를 포함해서 인터페이스는 브라우저의 고유 이벤트와 같지만 모든 브라우저에서 동일하게 동작한다. 모든 합성 이벤트 객체는 다음 어트리뷰트를 가진다.
boolean bubbles
boolean cancelable
DOMEventTarget currentTarget : 이벤트가 바인딩된 요소
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent : 브라우저 내장 이벤트 객체
void preventDefault() : 링크나 폼 전송과 같은 기본 동작을 방지
boolean isDefaultPrevented()
void stopPropagation() : 이벤트 전파 중단
boolean isPropagationStopped()
void persist()
DOMEventTarget target : 이벤트가 실제로 발생한 요소
number timeStamp
string type

이와 함께 React에서 지원하는 DOM이벤트는

Mouse 이벤트:
onClick onContextMenu onDoubleClick onDrag onDragEnd
onDragEnter onDragExit
onDragLeave onDragOver onDragStart onDrop onMouseDown 
onMouseEnter onMouseLeave
onMouseMove onMouseOut onMouseOver onMouseUp

Form 이벤트:
onChange onInput onInvalid onReset onSubmit

이것말고도 종류는 많으니 필요할때 React 공식문서를 참고해보면 좋을 것 같다.

리액트에서 전달되는 event 객체는 W3C 명세에 따라 SyntheticEvent를 정의하기 때문에 순수 자바스크립트에서 사용하는 것과 동일하게 사용하면 된다.

✍ 이벤트의 기본동작 방지

preventDefault()

예를 들어, 새로운 페이지로 연결되는 기본 링크 동작을 막으려면 html에서는 이렇게 사용했습니다.

<a href="#" 
onclick="console.log('the link is clicked');"
return false> 클릭하세요 </a>

그러나 React에서는 preventDefault()를 사용합니다.

function App () {
  function handleClick(e) {
    e.preventDefault();
    console.log('the link is clicked');
  }
  return (
    <a href="#" onClick={handleClick}>클릭하세요</a>
  )
}

보통 form에서 input을 받을때 콘솔창에 출력해볼때 자주 사용합니다. form의 onSubmit으로 이벤트를 수행하면 바로 새로운 페이지로 연결되기에 콘솔창을 제대로 확인할 수 없기때문이죠.

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(text);
  };

e.preventDefault()e.stopPropagation()의 차이점은?

  • e.preventDefault()는 고유 동작을 중단시키고,
  • e.stopPropagation()은 상위 엘리먼트들로의 이벤트 전파를 중단시킵니다

stopPropagation()과 이벤트버블링

자바스크립트에서도 이벤트 버블링, 캡쳐링을 본 적이 있다. 다시 정리해보면

이벤트 버블링이랑 특정한 요소에서 어떤 이벤트가 발생했을 때, 상위에 있는 요소까지 이벤트가 전파되는 것을 말한다

버블링은 이벤트가 발생한 요소를 기준으로 가까운 상위 요소부터 하나씩 탐색한다면, 이벤트 캡쳐링은 이벤트가 발생한 요소의 최상위 요소부터 처음 이벤트가 발생한 요소까지 탐색한다

import React, { useState } from "react";
import "../styles.css";

const Banner = () => {
  const [isVisible, setIsVisible] = useState(true);

  const getAlert = () => alert("ALERT!!");
  
  const closeBanner = () => setIsVisible(false);

  return (
    <div
      className="banner"
      onClick={getAlert}
      style={{ display: isVisible ? "flex" : "none" }}
    >
      이 곳을 클릭해서 alert창 띄우기
      <button className="close-btn" onClick={closeBanner}>
        닫기
      </button>
    </div>
  );
};

export default Banner;

닫기 버튼을 눌렀을 때 딱 배너만 닫고 싶은데, alert창도 함께 뜬 후에 닫히는 것을 확인할 수 있다. 닫기 버튼의 onClick이벤트가 발생하면 closeBanner 함수가 실행되고, 상위요소에도 onClick이벤트가 등록되어있기 때문에 getAlert함수도 실행되게 되는 것이다.
** 이때 바로 stopPropagation()을 사용할 수 있다. 특정한 요소에 특정한 이벤트만 실행하고, 전파되는 것을 막고 싶을 때 적용하면 된다.

const closeBanner = (e) => {
  e.stopPropagation(); //이벤트 전파 방지
  setIsVisible(false);
}

✍ 이벤트 핸들링

1. 클래스형 컴포넌트

React에서는 DOM엘리먼트가 생성된 후 리스너를 추가하기 위해 addEventListener를 호출할 필요가 없다. 대신, 엘리먼트가 처음 렌더링될때 리스너를 제공하면 된다.
아래의 예시를 보면, 이벤트핸들러를 클래스의 메서드로 만들었고 이를 컴포넌트 내에서 사용할 수 있다.

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};
    //콜백에서 `this`가 작동하려면 바인딩을 해줘야한다
    this.handleClick = this.handleClick.bind(this); 
  }

  handleClick() {
    this.setState(state => ({
      isToggleOn: !state.isToggleOn
    }));
  }

  render() {
    return (
      //render() 함수 안에서 this 값은 render() 함수가 속한 컴포넌트를 가리킴
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}
  • 여기에서 주의해야할 것은, JS의 클래스 메서드는 기본적으로 바인딩되어있지 않기 때문에 this.handleClick을 바인딩하지 않고 호출하면 this는 undefined가 된다. 따라서 바인딩해주고 사용해야한다.
  • 즉 binding을 따로 지정해주지 않으면 해당 메서드에서 호출하는 this가 해당 클래스안에서의 event값이 아닌 최상위의 window에서 값을 가져올 수 있기 때문에 주의해야한다.

매번 bind을 호출하는 것이 불편하다면, 아래와 같이 클래스필드를 사용하여 콜백을 올바르게 바인딩할 수 있다.

class Test extends React.Component {
  // 이 문법은 `this`가 handleClick 내에서 바인딩되도록 합니다.
  handleClick = () => {
    console.log(this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}
  • 실행결과
    이렇게 현재 this가 가리키는 Test객체에 대한 내용을 출력합니다.

2. 함수형 컴포넌트

일단 함수형 컴포넌트의 상태값은 useState훅으로 관리되기 때문에 컴포넌트의 this로부터 자유롭습니다. 또한 함수형 컴포넌트 자체와 함수형 컴포넌트 안에서 선언한 함수들 모두 전역 객체를 this로 가지기 때문에 애초에 this가 다 같습니다. 그래서 이벤트 핸들러에 콜백 함수를 넘기는 상황에도 딱히 신경 쓸 필요가 없습니다.

  • const 키워드 + 함수 형태로 선언해야한다
  • 요소에 적용하기 위해 this가 따로 필요하지 않다
import React, {useState} from "react";

function NumberTest() {
  const [num, setNumber] = useState(0);

  const increase = () => {
    setNumber(num+1);
  }

  return (
    <div>
      <h1>Number Test</h1>
      <h2>{num}</h2>
      <button onClick={increase}>증가</button>
    </div>
  );
}

export default NumberTest;

✍ 예시로 이해하기

상위 컴포넌트 App에서 하위 컴포넌트인 PhoneForm의 input값을 전달받아서 그 값을 콘솔에 출력해보자.
이 예시에서 크게 두가지를 중점으로 다뤄보겠습니다.
1. 상위컴포넌트로 데이터 전달하는 과정
2. 여러개의 input 입력받기 (하나의 useState만으로 구현)

//App.js(상위컴포넌트)
import React from "react";
import PhoneForm from "./PhoneForm";

const App = () => {
  const handleCreate = (data) => {
    console.log(data);
  }
  return (
    <div className="App">
      <PhoneForm onCreate={handleCreate}/>
    </div>
  )
}

export default App;
//PhoneForm.js(하위 컴포넌트)
const PhoneForm = ({onCreate}) => {
  const [inputs, setInputs] = useState({
    name: "",
    phone: "",
  })
  
  const { name, phone } = inputs;

  const handleChange = (e) => {
    { name, value } = e.target;
    setInputs({
      ...inputs, //이전의 inputs을 복사(spread operator)
      [name]: value, //새로운 값만 덮어쓰여짐
    });
  }
  
  const handleSubmit = (e) => {
    e.preventDefault();
    onCreate(inputs);
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input name="name" value={name} onChange={handleChange}/>
        <input name="phone" value={phone} onChange={handleChange}/>
        <button type="submit">등록</button>
      </form>
    </div>
  )
}

export default PhoneForm;

1. 상위컴포넌트로 데이터 전달하는 과정
1) PhoneForm에서 입력 받은 input값을 onChange를 사용해서 값의 변화가 생길 때 마다 자신의 state에 동기화한다.
2) ApphandleCreate 메소드를 PhoneFormonCreate라는 props로 전달한다.
3) 사용자가 등록 버튼을 누르면(onSubmit 하면) e.preventDefault()로 페이지전환을 막고, props로 전달된 onCreate(=handleCreate) 함수의 매개변수로 state 객체를 넘긴다.
4) 전달 받은 하위 컴포넌트의 state 객체를 App의 내부 메소드인 handleCreate에서 사용할 수 있다.


2. 여러개의 input 입력받기

form에서 많은 input 입력을 받게 되는 경우

const [name, setName] = useState("");
const [phone, setPhone] = useState("");

이와 같이 불필요한 중복과정이 일어나므로, useState에 문자열이 아닌 객체를 넣어주면서 해결할 수 있다.
1) useState에 문자열이 아닌 객체를 넣어준다
2) 구조분해할당을 이용해 inputs 객체의 값을 name, phone에 할당해준다.
const { name, phone } = inputs;
3) ...inputs처럼 spread operator을 이용해 과거의 값을 복사하고, 바뀐 값만 [e.target.name]: e.target.value 갱신해준다.


[References]

profile
기록중

0개의 댓글