들어가기 전에

리액트 공식문서로 배워보자 #6, 이벤트 다루기!

리액트 엘리먼트가 가진 이벤트를 다루는 것(handling)은 DOM 엘리먼트의 이벤트를 다루는 것과 매우 흡사합니다. 다만, 몇가지 문법적인 차이가 있습니다.

  • 리액트의 이벤트는 lowercase가 아니라 camelCase를 이용해 이름이 지어집니다.
  • JSX를 이용해, 문자열 말고 함수를 이벤트 핸들러로 보낼 수 있습니다.

HTML의 예:

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

리액트의 예: (약간 차이가 있습니다.)

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

또 다른 차이로, 기본으로 설정된 행동(behavior)을 막기 위해 false를 반환할 수 없습니다. preventDefault를 명시적으로 호출해야 합니다. 예를 들면, 순수한 HTML에서 새로운 페이지를 열었을 때의, 기본 행위(behavior)를 막기 위해 다음과 같이 작성할 수 있습니다.

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

리액트에서는 다음과 같습니다.

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

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

e 는 synthetic 이벤트입니다. 리액트는 이런 synthetic 이벤트들을 W3C spec에 따라 정의합니다. 그래서 cross-browser 호환성에 대해 걱정할 필요가 없습니다. SyntheticEvent 레퍼런스 가이드를 통해 더 알아보셔도 좋습니다.

리액트를 사용할 때, 일반적으로 DOM 엘리먼트가 생성된 이후에 DOM 엘리먼트에 listener를 추가하기 위해서 addEventListener를 호출할 필요가 없습니다. 대신, 엘리먼트가 처음 렌더링 됐을 때, listener를 제공하세요.

ES6 class를 이용하여 컴포넌트를 정의할 때, 일반적인 패턴은 클래스 위에 이벤트 핸들러를 메소드로 얹는 것입니다. 예를 들면, 이 Toggle 컴포넌트는 사용자가 "ON" 또는 "OFF"를 토글할 수 있는 버튼을 렌더링합니다.

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 (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

ReactDOM.render(
  <Toggle />,
  document.getElementById('root')
);

CodePen에서 직접 해보기

JSX 콜백에서 this의 의미에 대해서 반드시 주의해야 합니다. 자바스크립트에서, 클래스 메소드는 기본값으로 바인드되지 않습니다. 만일, this.handleClick을 바인드하는 것을 까먹고 onClick에 넘기게 된다면, 함수가 실제로 호출됐을 때, thisundefined가 될 것입니다.

이건 리액트에 한정된 행위(behavior)가 아닙니다. 이건 자바스크립트 함수 작동방식의 일부입니다. 일반적으로, () 없이 메소드를 참조하려고 하면 이후에, onClick={this.handleClick}처럼, 그 메소드를 바인드해주어야 합니다.

만일 bind를 호출하는 것이 짜증난다면, 이것을 해결할 수 있는 방법이 두가지 있습니다. 만일 실험적인 public class field 문법을 사용중이라면, 올바르게 콜백을 바인딩하기 위해서 클래스 필드를 쓸 수도 있습니다.

class LoggingButton extends React.Component {
  // 이 문법에서는 this가 handleClick 내부에 바인딩 되는 것을 보장합니다.
  // 경고: 이 문법은 *실험적인* 문법입니다.
  handleClick = () => {
    console.log('this is:', this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

이 문법은 Ceate React App에서 기본값으로 활성화됩니다.

public class field 문법을 사용하지 않는다면, 콜백에 화살표 함수를 사용해도 됩니다.

class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    // 이 문법은 this가 콜백에 바인드가 되는 것을 보증합니다.
    return (
      <button onClick={(e) => this.handleClick(e)}>
        Click me
      </button>
    );
  }
}

사실 이 문법의 문제는 LoggingButton이 렌더링될 때마다 다른 콜백이 만들어진다는 것입니다. 대부분의 경우에는, 이러한 문법으로 작성해도 아무런 문제가 없지만, 콜백이 prop으로 더 하위 단계의 컴포넌트에 넘어가면, 그 컴포넌트들은 추가적인 재렌더링 과정을 수행할 수 있습니다. 우리는 일반적으로 이러한 종류의 성능 문제를 피하기 위해서 생성자에서 binding을 하거나 class field 문법을 사용하는 것을 권장합니다.

이벤트 핸들러로 인자 넘기기

루프 내에서 추가적인 파라미터를 이벤트 핸들러로 넘기는 것은 매우 흔한 일입니다. 예를 들면 id가 row ID일 때, 아래 두가지 문법 중 어떤 것을 써도 잘 작동합니다.

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

위의 두 라인은 동일합니다. 화살표 함수Function.prototype.bind냐의 차이 입니다.

두 개의 케이스 모두, e 인자는 리액트 이벤트가 ID 뒤에 두번째 인자로서 넘겨질 것이라는 것을 말합니다. 화살표함수에서 우리는 이것을 명시적으로 넘겨줘야 합니다. 하지만 bind를 사용하면 어떤 추가적인 인자(argument)라도 자동으로 넘어갑니다.