본문 바로가기
JavaScript/React

[ReactJS] 이벤트 핸들링 - 리액트 방식

by 혀나Lee 2019. 9. 17.
이 페이지는 책(리액트 & 리액트 네이티브 통합 교과서)과 리액트 공식 사이트(ko.reactjs.org)를 참고하여 정리한 페이지입니다.

이벤트 처리

React 엘리먼트에서 이벤트를 처리하는 방식은 DOM 엘리먼트에서 이벤트를 처리하는 방식과 매유 유사합니다. 몇 가지 문법적인 차이는 다음과 같습니다.

  • React의 이벤트는 소문자 대신 캐멀 케이스(camelCase)를 사용합니다.
  • JSX를 사용하여 문자열이 아닌 함수로 이벤트 핸들러를 전달합니다.

예를 들어, HTML은 다음과 같습니다.

<button onclick="activateLasers()">
  Activate Lasers
</button>

React에서는 약간 다릅니다.

<button onClick={activateLasers}>
  Activate Lasers
</button>

또 다른 차이점으로, React에서는 false를 반환해도 기본 동작을 방지할 수 없습니다. 반드시 preventDefault를 명시적으로 호출해야 합니다. 예를 들어, 일반 HTML에서는 새 페이지를 여는 링크의 기본 동작을 방지하기 위해 다음과 같은 코드를 작성합니다.

<a href="#" onclick="console.log('The link was clicked.'); return false">
  Click me
</a>

React에서는 다음과 같이 작성할 수 있습니다.

function ActionLink() {
  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}

다중 이벤트 핸들러

하나의 엘리먼트에 2개 이상의 핸들러가 필요할 경우도 쉽게 작성할 수 있습니다.

import React, { Component } from 'react';

export default class MyInput extends Component {

  // Triggered when the value of the text input changes...
  onChange() {
    console.log('changed');
  }

  // Triggered when the text input loses focus...
  onBlur() {
    console.log('blured');
  }

  // JSX elements can have as many event handler
  // properties as necessary.
  render() {
    return (
      <input
        onChange={this.onChange}
        onBlur={this.onBlur}
      />
    );
  }
}

제네릭 이벤트 핸들러

React 애플리케이션은 서로 다른 컴포넌트에 동일한 이벤트 처리를 할 수 있습니다. 예를 들어 '버튼을 클릭하면 컴포넌트가 항목의 리스트를 정렬해야 한다.'면, 이러한 타입의 젠릭 핸들러는 여러 컴포넌트가 공유할 수 있도록 모듈로 만들면 좋습니다.

// Exports a generic function that changes the
// state of a component, causing it to re-render
// itself. Notice that the state property, "items",
// is very generic? This function is likely useful
// for several components as an event handler.
export default function reverse() {
  this.setState(this.state.items.reverse());
}
import React, { Component } from 'react';

// Import the generic event handler that
// manipulates the state of a component.
import reverse from './reverse';

export default class MyList extends Component {
  state = {
    items: ['Angular', 'Ember', 'React'],
  }

  // Makes the generic function specific
  // to this component by calling "bind(this)".
  onReverseClick = reverse.bind(this)

  render() {
    const {
      state: {
        items,
      },
      onReverseClick,
    } = this;

    return (
      <section>
        { /* Now we can attach the "onReverseClick" handler
             to the button, and the generic function will
             work with this component's state. */}
        <button onClick={onReverseClick}>Reverse</button>
        <ul>
          {items.map((v, i) => (
            <li key={i}>{v}</li>
          ))}
        </ul>
      </section>
    );
  }
}
func.bind(thisArg[, arg1[, arg2[, ...]]]) - 자세히 보기
[매개변수]
thisArg: 바인딩 함수가 대상 함수의(target function)의 this에 전달하는 값.
arg1, arg2, ...: 대상 함수의 인수 앞에 사용될 인수.
[반환값]
지정한 this 값 및 초기 인수를 사용하여 변경한 원본 함수의 복제본.

reverse() 함수를 임포트하여 onReverseClick 에 reverse를 바인딩해줍니다. 만약 reverse() 함수를 바인딩해주지 않고 button 엘리먼트에 바로 reverse() 함수를 사용하면 reverse() 함수에서 thisundefined값을 가져서 this.setState 에서 에러가 발생합니다. - 테스트 해보기

또한, reverse() 함수에서 this.state.items 에 접근할 수 있는 이유도 바인딩 해주었기 때문입니다.

reverse() 함수에서 console.log(this)를 해보면 아래처럼 MyList가 출력됩니다.

 

이벤트 핸들러 컨텍스트와 매개변수

컴포넌트 데이터 가져오기

import React from 'react';
import { render } from 'react-dom';

import MyList from './MyList';

// items는 프로퍼티 값으로 <MyList>에 전달한다.
const items = [
  { id: 0, name: 'First' },
  { id: 1, name: 'Second' },
  { id: 2, name: 'Thrid' }
];

// items 프로퍼티를 갖는 <MyList>를 렌더링한다.
render(<MyList items={items} />, document.getElementById('root'));

리스트의 각 항목을 식별할 수 있도록 id 프로퍼티를 갖습니다. 이벤트 핸들러가 해당 항목을 처리하기 위해서는 UI에서 클릭했을 때 이 id에 접근할 수 있어야 합니다.

import React, { Component } from 'react';

export default class MyList extends Component {
  constructor() {
    super();
    
    // "onClick()" 핸들러가 컨텍스트로
    // 이 컴포너트에 바인딩됐는지 확인해야 한다.
    this.onClick = this.onClick.bind(this);
  }
  
  // 리스트 항목을 클릭했을 때 "id" 인수를 가진 항목의 이름을 확인하자.
  // 이것이 컴포너트 프로퍼티에 "this"로 접근해야 하는 이유다.
  onClick(id) {
    const { name } = this.props.items.find(
      it => i.id === id
    );
    
    console.log('clicked', `"${name}"`);
  }
  
  render() {
    return (
      <ul>
        { /* 바인딩된 id 인수를 갖고 새로운
             핸들러 함수를 만든다. 
             컨텍스트의 왼쪽 인수가 null인 것은
             이미 생성자에서 바인딩했기 때문이다. */ }
        {this.props.items.map(({ id, name }) => )
          <li
            key={id}
            onClick={this.onClick.bind(null, id)}
          >
            {name}
          </li>
        ))}
      </ul>
    );
  }
}

 

잘못된 예제

만약 "onClick()" 핸들러가 바인딩되는 코드를 작성하지 않는다면 아래처럼 실행이 될 것입니다.

// 잘못된 코드 예제
import React, { Component } from "react";

export default class ClickTest extends Component {
  handleClick(id) {
    console.log(id);
  }
  render() {
    return <button onClick={this.handleClick(111)}>dddd</button>;
  }
}

위처럼 컴포넌트가 render될 때, 111이 한 번 찍히고 버튼의 클릭 이벤트에는 아무런 동작이 되지 않습니다.

 

render함수에서 바인딩

"onClick()" 이벤트의 핸들러 함수 바인딩은 render내부에서 바로 바인딩 할 수도 있습니다.

import React, { Component } from "react";

export default class ClickTest extends Component {
  handleClick(id) {
    console.log(id);
  }
  render() {
    return <button onClick={this.handleClick.bind(this, 111)}>dddd</button>;
  }
}

다만, 이렇게 할 경우 render가 실행될 때마다 새로운 함수를 생성하기때문에 성능상에 좋지 않습니다. 따라서 생성자에서 한 번 바인딩하거나 arrow function을 사용하여 핸들러 함수를 작성하는 것이 좋습니다. - 자세히 보기

주의
Function.prototype.bind를 render 메소드에서 사용하면 컴포넌트가 렌더링할 때마다 새로운 함수를 생성하기 때문에 성능에 영향을 줄 수 있습니다.

 

고차 이벤트 핸들러

고차 함수(higher-order function)은 새로운 함수를 반환하는 함수입니다. 앞 예제에서는 bind()를 사용해 이벤트 핸들러 함수의 컨텍스트 및 인수 값을 바인딩했습니다.

import React, { Fragment, Component } from 'react';

export default class App extends Component {
  state = {
    first: 0,
    second: 0,
    third: 0
  };
  
  // 이 함수는 화살표 함수로 정의됐다. 그래서 "this"는
  // 이 컴포넌트에 렉시컬 바인딩(Lexical Binding)된다.
  // name 인수는 계산된 프로퍼티 name에서 이벤트 핸들러로
  // 반환되는 함수에서 사용된다.
  onClick = name => () => {
    this.setState(state => ({
      ...state,
      [name]: state[name] + 1
    }));
  };
  
  render() {
    const { first, second, third } = this.state;
    
    return (
      <Fragement>
        { /* 매개변숫값과 함계 this.onClick()을 호출해 즉석에서 새로운 이벤트 핸들러 함수를 생성하고 있다. */ }
        <button onClick={this.onClick('first')}>First {first}</button>
        <button onClick={this.onClick('second')}?
          Second {second}
        </button>
        <button onClick={this.onClick('third')}>Thrid {thrid}</button>
      </Fragment>
    );
  }
}

이 컴포넌트는 3개의 버튼을 렌더링하고 각 버튼의 카운트 값을 갖는 3개의 상태를 갖습니다. onClick() 함수는 화살표 함수로 정의됐기 때문에 컴포넌트 컨텍스트에 자동으로 바인딩됩니다. name 값을 인수로 가지며 새로운 함수를 반환합니다. 반환된 함수는 호출될 때 name 값을 사용하며 주어진 name의 상태 값을 증가시키기 위해 계산된 프로퍼티 구문([] 안의 변수)을 사용합니다.

렉시컬 바인딩(Lexical Binding): 소스 코드를 읽는 단계에서 바인딩이 결정

 

인라인 이벤트 핸들러

인라인 함수의 사용법은 아주 간편합니다. 화살표 함수를 JSX의 이벤트 프로퍼티에 직접 할당해 사용합니다.

import React, { Component } from 'react';

export default class MyButton extends Component {
  // onClick() 핸들러와 함계 버튼 요소를 렌더링한다.
  // 이 함수는 인라인으로 선언됐는데 다른 함수를 호출할 필요가 있을 경우에 유용하다.
  render() {
    return (
      <button
        onclick={e => console.log('clicked', e)}
      >
        {this.props.childre}
      </button>
    );
  }
}

이러한 인라인 이벤트 핸들러는 주로 다은 함수에 전달할 정적 매개변수를 가졌을 때 사용합니다. 이 예제에서 항상 clicked 를 갖고 console.log()를 호출합니다. bind()를 사용하면 새 함수를 마들 수 있고 고차 함수를 사용하면 JSX 마크업의 바깥에 특수 함수를 설정할 수 있습니다. 그러나 이렇게 하면 또 다른 함수를 만들어야 합니다. (다만, 이렇게 구현시에 성능 문제에 조심하자.)

 

요소에 핸들러를 바인딩

JSX내에서 엘리번트에 이벤트 핸들러를 할당할 경우, React는 이벤트 리스너를 DOM 요소에 직접 연결하지 않고 함수의 내부 매핑에 이벤트 핸들러를 추가합니다. React 핸들러는 이벤트가 DOM 트리를 통해 문서 위로 올라가면서(bubbling) 컴포넌트와 일치하는 핸들러가 있는지 여부를 확인합니다. 

버블링: 특정 화면 요소에서 이벤트가 발생했을 때 해당 이벤트가 더 상위의 화면 요소들로 전달되어 가는 특성. (이 때문에 자식 요소에서 발생한 이벤트를 부모 이벤트에서 감지 가능하다.)

https://subscription.packtpub.com/book/application_development/9781789346794/4/ch04lvl1sec42/binding-handlers-to-elements

새로운 React 컴포넌트를 렌더링할 때, 이벤트 핸들러 함수는 React에 의해 유지되는 내부 매핑에 추가됩니다. 그리고 이벤트가 발생하여 document 객체에 도달하면 핸들러에 이벤트를 매핑하며 일치하는 핸들러를 호출합니다. 마지막으로 React 컴포넌트가 삭제되면 핸들러 함수는 핸들러 리스트로부터 삭제됩니다.

통합(합성) 이벤트 (SyntheticEvent)

이벤트 핸들러는 모든 브라우저에서 이벤트를 동일하게 처리하기 위한 이벤트 래퍼 SyntheticEvent 객체를 전달받습니다. stopPropagation()와 preventDefault()를 포함해서 인터페이스는 브라우저의 고유 이벤트와 같지만 모든 브라우저에서 동일하게 동작합니다.

브라우저의 고유 이벤트가 필요하다면 nativeEvent 어트리뷰트를 참조하세요. 모든 합성 이벤트 객체는 다음 어트리뷰트를 가집니다.

boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
DOMEventTarget target
number timeStamp
string type

이벤트 풀링

SyntheticEvent 풀링됩니다. 성능상의 이유로 SyntheticEvent 객체는 재사용되고 모든 속성은 이벤트 핸들러가 호출된 다음 초기화됩니다. 따라서 비동기적으로 이벤트 객체에 접근할 수 없습니다.

 

이벤트가 발생할 때마다 풀로부터 하나의 인스턴스를 가져오고 프로퍼티 값을 채웁니다. 통합 이벤트 인스턴스는 이벤트 핸들러의 동작이 끝날 때 해제돼 다시 풀로 돌아갑니다.

 

이러한 방식은 많은 이벤트가 발생했을 때 가비지 컬렉터의 빈번한 동작을 방지해줍니다. 풀은 통합 이벤트 인스턴스에 대한 참조를 유지하기 때문에 가비지 컬렌션의 대상이 되지 않습니다.


[참조]
- React Korea (ko.reactjs.org)
- Function.bind (MDN, Function.prototype.bind())
- 이벤트 할당 시스템 (네이버 D2, 
https://d2.naver.com/helloworld/9297403)

댓글