Event Binding

React 컴포넌트 이벤트 연결

React에서 이벤트를 연결하는 방법은 DOM 이벤트 핸들링 방식과 유사합니다. 다만 몇 가지 차이점이 있습니다.

차이점

예시

이벤트 속성 이름은 camelCase 형식으로 작성

onClick, onKeyDown, onFocus, onChange

이벤트 리스너 연결은 JSX 인터폴레이션

<button onClick={ this.handleClick } />

브라우저 기본 동작 방지

event.preventDefault() 메서드만 허용

React의 합성 이벤트

React의 이벤트 리스너는 모든 브라우저에서 동일한 이벤트를 처리하기 위한 이벤트(events) 래퍼 객체를 전달받습니다. 합성 이벤트(Synthetic events) 객체는 preventDefault, stopPropagation 메서드를 포함하며, 인터페이스는 브라우저의 표준 이벤트 모델과 동일합니다.

DOM 이벤트 바인딩

HTML 요소의 표준 이벤트 속성 값으로 이벤트 리스너를 연결합니다.

<button onclick="handleEventBinding()">연결 된 DOM 이벤트</button>

React 이벤트 바인딩

React 이벤트 연결은 이벤트 속성 이름 표기는 camelCase 방식으로, 이벤트 리스너 연결은 함수 참조 또는 인라인 함수를 작성해 JSX에 인터폴레이션 합니다.

<button onClick={ handleEventBinding }>연결 된 React 합성 이벤트</button>

// 또는  

<button onClick={(e) => console.log(' ... ')}>인라인 이벤트 바인딩</button>

이벤트 연결 그리고 접근성

특별한 상황이 아닌 경우, HTML 요소 중 포커스(초점) 이동이 가능한 요소에 이벤트를 연결해야 합니다.

  • HTMLInputElement

  • HTMLSelectElement

  • HTMLTextAreaElement

  • HTMLAnchorElement

  • HTMLButtonElement

  • HTMLAreaElement

이 외의 요소 특히 <div />, <span /> 등 포커스 이동이 가능하지 않은 요소에 이벤트를 연결하면 안됩니다. 다만, 이벤트 위임(event delegation)을 목적으로 이벤트를 연결했다면 내부에 포커스 이동이 가능한 요소를 포함해야 합니다. 그렇지 않으면, 키보드 만으로 서비스를 동일하게 이용할 수 없어 접근성 문제가 됩니다.

브라우저 기본 동작 방지

특정 동작으로 작동하길 기대 할 경우, 브라우저에 설정 된 기본 동작이 작동되지 않도록 설정해야 합니다. React는 이벤트 속성에 연결 된 이벤트 리스너가 전달 받는 합성 이벤트의 preventDefault() 메서드를 사용해 방지할 수 있습니다.

const AppleMusicLink = () => {

  // 이벤트 리스너(함수)
  function handleClick(e) {
    e.preventDefault() // 브라우저 기본 동작 방지
    console.log('브라우저 기본 동작을 방지했습니다.')
  }
  
  return (
    <a 
      href="https://apple.com/kr/apple-music"
      rel="noopener noreferrer"
      target="_blank"
      onClick={ handleClick }
    >
      애플 뮤직
    </a>
  )
  
}

DOM 방식의 브라우저 기본 동작 방지는 React에서 작동하지 않습니다.

<a href="#" onclick="console.log('...'); return false">DOM 방식</a>

클래스 컴포넌트의 this

클래스 컴포넌트를 사용 할 경우, 클래스 인스턴스 메서드의 this 참조에 대한 주의가 필요합니다. 클래스 컴포넌트에서 작성 된 handleClick 인스턴스 메서드 안에서 this가 가리키는 참조를 확인해보면 기대와 달리 undefined가 출력됩니다. 왜 this 는 클래스로부터 생성 된 인스턴스를 가리키지 않는 것일까요?

import React from 'react'


class AppleMusicLink extends React.Component {
  
  // 인스턴스 메서드
  handleClick(e) {
    e.preventDefault()
    console.log(this) // this === undefined
  }

  render() {
    return (
      <a 
        href="https://apple.com/kr/apple-music"
        rel="noopener noreferrer"
        target="_blank"
        onClick={ this.handleClick }
      >
        애플 뮤직
      </a>
    )
  }
  
}

이 문제는 React의 문제가 아니라, JavaScript에서 함수가 작동하는 방식에 원인이 있습니다. 객체의 메서드가 아닌, 일반 함수로 실행 될 경우 this 참조는 엄격 모드('use strict')에서의 결과처럼 undefined가 됩니다.

(/* IIFE */ () => {
  'use strict'

  function whatDoesThisRef() { console.log(this) }
  const o = { whatDoesThisRef }

  // 일반 함수 실행 결과: undefined
  console.log( whatDoesThisRef() )
  
  // 객체의 메서드 실행 결과: o { whatDoesThisRef }
  console.log( o.whatDoesThisRef() )

})()

클래스 컴포넌트의 this 바인딩

클래스 컴포넌트에서 이벤트 속성에 연결 된 메서드의 this 참조가 undefined인 경우, 기대한 대로 앱이 작동하지 않습니다. 이 문제를 해결할 수 있는 다양한 방법을 살펴보겠습니다.

1. this 참조 변경

JavaScript 함수는 bind() 메서드를 사용해 this 참조를 임의로 변경할 수 있습니다. 이 방법을 사용하여 메서드의 this 참조가 컴포넌트 인스턴스를 가리키도록 변경할 수 있습니다.

import React, { Component } from 'react'


class AppleMusicLink extends Component {

  constuctor(props) {
    super(props)
    
    // this 참조를 컴포넌트 인스턴스로 변경
    this.handleClick = this.handleClick.bind(this)
  }
  
  handleClick(e) {
    e.preventDefault()
    console.log(this) // this === AppleMusicLink {}
  }
  
  render() {
    return (
      <a 
        href="https://apple.com/kr/apple-music"
        rel="noopener noreferrer"
        target="_blank"
        onClick={ this.handleClick }
      >
        애플 뮤직
      </a>
    )
  }
  
}

인라인 this 바인딩

클래스 생성자(constructor)에서 this 참조를 변경하지 않고, 인스턴스 메서드 참조에 직접 bind()를 사용해 this 참조를 인스턴스로 변경하는 방법을 활용할 수 있습니다.

class AppleMusicLink extends Component {

  handleClick(e) { /* ... */ }

  render() {
    return (
      <a 
        href="https://apple.com/kr/apple-music"
        rel="noopener noreferrer"
        target="_blank"
        onClick={ this.handleClick.bind(this) }
      >
        애플 뮤직
      </a>
    )
  }
}

2. 화살표 함수 활용

인스턴스 메서드를 래핑 하는 화살표 함수를 사용하면 this 참조를 인스턴스로 설정할 수 있습니다.

import React, { Component } from 'react'


class AppleMusicLink extends Component {
  
  handleClick(e) {
    e.preventDefault()
    console.log(this) // this === AppleMusicLink {}
  }
  
  render() {
    return (
      <a 
        href="https://apple.com/kr/apple-music"
        rel="noopener noreferrer"
        target="_blank"
        onClick={ (e) => this.handleClick(e) }
      >
        애플 뮤직
      </a>
    )
  }
  
}

3. 클래스 필드 선언

ES 표준에 제안 된 클래스 필드 선언을 사용해 문제를 해결할 수 있습니다. 향상된 객체 표기법 대신 화살표 함수 방식으로 메서드를 정의하면 this 참조가 인스턴스를 가리키게 됩니다.

import React, { Component } from 'react'


class AppleMusicLink extends Component {
  
  // 클래스 필드 선언 방식으로 인스턴스 메서드 정의
  handleClick = (e) => {
    e.preventDefault()
    console.log(this) // this === AppleMusicLink {}
  }
  
  render() {
    return (
      <a 
        href="https://apple.com/kr/apple-music"
        rel="noopener noreferrer"
        target="_blank"
        onClick={ this.handleClick }
      >
        애플 뮤직
      </a>
    )
  }
  
}

이벤트 리스너에 인자 전달

인스턴스 메서드(이벤트 리스너)에 특정 인자를 전달하는 방법은 다음의 2가지입니다.

1. 화살표 함수 활용

인스턴스 메서드를 래핑하는 화살표 함수를 이벤트 속성에 연결한 후, 특정 인자를 전달할 수 있습니다.

list.map(item => (
  <a 
    key={item.id} 
    href={item.link} 
    onClick={(e) => this.handleClick(item.id, e)}
  >
    아이템 ID, 합성 이벤트 객체를 이벤트 리스너에 전달
  </a>
))

2. 함수의 bind 활용

인스턴스 메서드의 this 참조를 bind() 메서드를 사용해 인스턴스로 변경한 후, 특정 인자를 전달합니다.

list.map(item => (
  <a 
    key={item.id} 
    href={item.link} 
    onClick={this.handleClick.bind(this, item.id)}
  >
    아이템 ID를 이벤트 리스너에 전달
  </a>
))

이벤트 위임 대상 변경

React 16 버전까지는 document 노드에 이벤트를 연결해 위임했습니다. 하지만 React 17 버전부터는 React 앱의 루트 DOM 노드에 이벤트를 연결해 위임합니다. (일반적인 경우 #root DOM 요소 노드)

ReactDOM.render(
  // React 가상 DOM 트리
  <App />,
  // 실제 DOM 요소 → 루트 DOM 컨테이너
  document.getElementById('root')
);

React 17 버전은 향후 점진적인 업데이트를 목적으로 기존의 이벤트 시스템 방식을 다소 변경했습니다.

이러한 변경으로 React 17 버전부터는 다음과 같은 경우에 더욱 안전하게 React를 사용할 수 있게 되었습니다.

  1. 다양한 버전의 React가 공존하며 DOM에 앱을 생성한 경우

  2. 다른 기술로 빌드 된 앱의 일부에 React를 적용 할 경우

포털(Portals) 사용에는 문제가 없을까요?

Portals은 React에서 이벤트를 리스닝하므로 문제가 발생하지 않습니다. 안심하고 사용해도 됩니다.

이벤트 위임이란?

특정 노드에 일일이 이벤트 리스너를 추가하는 대신, 이벤트 리스너를 특정 노드들을 포함하는 상위 노드에 연결하여 이벤트를 전파하는 것을 이벤트 위임이라고 합니다. 위임된 이벤트는 포함된 하위 노드에 전파됩니다.

이벤트가 전파되는 방식의 기본 값은 버블링(bubbling)이며, 캡처링(capturing) 방식으로 변경해 사용할 수도 있습니다.

<ul class="parentNode">
  <li class="childNode"><a href="/child-node-1">하위 노드 1</a></li>
  <li class="childNode"><a href="/child-node-2">하위 노드 2</a></li>
  <li class="childNode"><a href="/child-node-3">하위 노드 3</a></li>
</ul>
// 상위(부모) 노드
const parentNode = document.querySelector(".parentNode")

// 상위 노드에 이벤트 리스너 연결
parentNode.addEventListener('click', (e) => {
  const nodeName = target.nodeName.toLowerCase()
  // 하위 노드에 이벤트 전파 
  // (해당 대상 노드에 처리 코드 작성)
  switch(nodeName) {
    case 'li': 
      console.log('<li> 노드 클릭') 
      break
    case 'a': 
      // 이벤트 기본 동작 차단
      e.preventDefault()
      console.log('<a> 노드 클릭') 
      break
    defualt:
      console.log(`${nodeName} 노드 클릭`)
  }
})

만약 이벤트 위임을 사용할 수 없었다면? 하위 노드 들이 실시간으로 추가 또는 제거 될 때마다 이벤트 리스너를 매번 연결하거나 제거해야 하니 끔찍했을 것입니다. 이벤트 위임을 사용할 수 있기에 새로운 노드가 추가, 제거 또는 업데이트 되어도 별도로 이벤트 리스너를 연결하거나 제거하지 않아도 됩니다.

<ul class="parentNode">
  <li class="childNode"><a href="/child-node-1">하위 노드 1</a></li>
  <!-- 실시간 제거된 노드 -->
  <!-- <li class="childNode"><a href="/child-node-2">하위 노드 2</a></li> -->
  <li class="childNode"><a href="/child-node-3">하위 노드 3</a></li>
  <!-- 실시간 추가된 노드 -->
  <li class="childNode"><a href="/child-node-3">하위 노드 4</a></li>
  <li class="childNode"><a href="/child-node-3">하위 노드 5</a></li>
  <li class="childNode"><a href="/child-node-3">하위 노드 6</a></li>
</ul>

Last updated