React에서 Event를 다루는 방식과 HTML에서 Event를 다루는 방식에는 약간의 차이점이 있습니다.

1. Event 사용 방법

  • HTML에서는 event를 정의 할 때 모든 event를 소문자로 정의합니다. React에서는 camelcase를 이용하여 event 정의를 합니다.

React에서 제공해주는 DOM event ("이벤트 이름" 항목 부분을 참조) - https://ko.reactjs.org/docs/events.html#clipboard-events

아래의 예시와 같이 Button1처럼 onclick 이벤트를 작성하면 React에서는 정상적으로 이벤트가 발생하지 않는 것을 확인하실 수 있습니다. React는 JSX를 사용하기 때문에 HTML에서 event를 다루는 방식으로 하면 정상적으로 되지 않습니다. JSX는 HTML과 비슷하여 사용하기는 편하지만 약간의 차이점 또한 있으므로 React를 사용하기 위해서는 JSX 학습 또한 중요합니다.

JSX 참고 자료 - https://reactjs-kr.firebaseapp.com/docs/introducing-jsx.html

두 번째 버튼(Button2)은 JSX에 맞게 camelcase를 사용한 버튼입니다. 버튼을 클릭하면 정상적으로 이벤트가 발생 되는 것을 확인할 수 있습니다.

   ...
   <button onclick={this.handleEvent}>Button1</button>
   <button onClick={this.handleEvent}>Button2</button>
   ...

2. 현재 Event의 기본동작을 막는 방법

  • HTML에서는 return false;를 반환해서 event의 기본동작을 막았지만, React에서는 preventDefault를 이용하여 event의 기본동작을 막습니다.

아래의 예시는 handleClick이라는 이벤트를 실행할 때 기본 동작을 막는 방법을 나타내고 있습니다. 첫 번째 예시는 HTML에서 하는 방식이며 두 번째 예시는 React에서 사용하는 방식입니다.

...
<a href="#" handleClick="alert('clicked'); return false">
  Click me
</a>
...
...
    handleClick(e) {
        e.preventDefault();
        alert('clicked');
    }
...

3. Event Binding

Event에서 가장 중요한 부분은 binding입니다. binding이 중요한 이유는 binding을 따로 지정을 해주지 않으면 해당 method에서 호출하는 this가 해당 class 안에서의 event 값이 아닌 최상위인 windows에서 값을 가져올 수 있기 때문입니다.

아래의 예제를 보시면 test1 버튼은 따로 this binding을 하지 않고 console에 this 값이 나타나도록 했을 때 undefined가 결과로 나옵니다.

test2 버튼은 this binding(화살표 함수) 을(를) 사용하고 this 값을 나타내도록 했을 때 해당 class의 this 값이 결과로 나타납니다.

render() {
    return (
      // 1. test1 - undefined
      // 2. test2 - event log
      <div>
        <button onClick={ function() {console.log(this)}}>test1</button>
        <br />
        <button onClick={() => console.log(this)}>test2</button>
      </div>
    );
}

React에서는 event binding을 할 수 있는 4가지 방법이 있습니다.

아래의 예시들은 handleClick이라는 함수가 있다는 가정하에 만든 예시 코드들입니다.

  1. Constructor에서 정의를 해주는 방법
    ...
    class Example extends React.Component {
        constructor() {
            super();
            this.handleClick.bind(this);
        }
    }
    ...
  1. render function 안에서 정의를 해주는 방법
    ...
    class Example extends React.Component {
        ...
        render() {
            return(
                <button onClick={this.handleClick.bind(this)} />
            )
        }
    }
  1. ES6 화살표 함수를 사용하는 방법
    1 - 함수에서 선언할 때 화살표 함수 선언
    class Example extends React.Component {
        ...
        handleClick = () => {
            ...
        }
       ...
    }

2 - render function안에서 정의

    ...
    class Example extends React.Component {
        ...
        render() {
            return (
                <button onClick={() => this.handleClick} />
            )
        }
    }
  1. autobind-decorator 라이브러리를 사용하는 방법
    import autobind from 'autobind-decorator';
    
    class Example extends React.Component {
        @autobind
        handleClick() {
            ...
        }
        ...
    }

event binding를 하지 않아도 되는 경우

  1. this를 이용해서 해당 class 참조를 하지 않아도 되는 경우
  2. React.createClass()를 사용하는 경우
  3. 화살표 함수를 사용하는 경우
  4. 같은 method를 여러 번 호출할 경우 생성자에서 한 번만 binding을 해서 중복 binding 행동을 줄이는 경우

이전까지는 HTML과 React에서 event handling의 차이점과 React에서 event binding에 대한 설명이었습니다. 이제 실제 예시와 함께 해당 부분에 대해서 자세히 다루어 보겠습니다. 아래의 예시는 VendingMachine(음료 자판기)를 React로 만들어 본 예시입니다.

첫번째로 설명 드릴 이벤트는 가격 입력 이벤트입니다.

-----------2019-07-13-------10.43.21

해당 초록색 항목에 가격을 입력하면 VendingMachine.js 파일에 state값인 price에 입력한 가격이 저장됩니다. 해당 이벤트는 예시에서 VendingMachine 파일에서 setPrice라는 method로 설정한 다음 InputPanel(가격 입력창 component)로 method를 props로 넘겨주고 있습니다. InputPanel에서 price 또한 넘겨서 선택한 음료수의 가격만큼 차감한 금액을 보여줄 수 있도록 설정하기 위해 props로 넘겨받고 있습니다.

    class VendingMachine extends React.Componet {
        ...
        this.state = {
            price: null
            ...
        }
        ...
        setPrice = (amount) => {
            this.setState({ price: amount});
        }
        ...
        return (
        ...
        <InputPanel setPrice={this.setPrice} price={this.state.price} getDrink={this.getDrink}/>
        ...

아래의 예시는 InputPanel.js 파일에서 상위 클래스(부모 클래스, 즉 VendingMachine.js)에서 넘겨준 함수와 state 값을 props로 받는 것을 보여주고 있습니다. render 함수 안에서 미리 const로 해당 값들은 선언해주면 앞에 this.props를 생략할 수 있습니다. 만약 미리 선언을 해주지 않으면 매번 props의 값을 선언할 때마다 this.props를 앞에 붙여서 호출해야 합니다.

해당 input 상자에 값이 입력하는 이벤트(onChange)가 발생하면 해당 값을 setPrice함수의 파라미터 값으로 넘겨받아서 setState를 이용하여 VendingMachine에서 state로 미리 선언된 price에 입력된 값을 저장하는 함수입니다.

    class InputPanel extends React.Component {
        ...
        render() {
            const { setPrice, price, getDrink } = this.props;
            return (
                ...
            <input onChange={(e) => setPrice(e.target.value)} value={price} />
                ...

만약 가격이 정상적으로 넘어오는지 확인을 하고 싶을때에는 부모 클래스(VendingMachine.js)의 render 함수 안에서 console.log('price: ', this.state.price);를 찍으면 가격을 입력할때마다 해당 값이 정상적으로 저장이 되는지 확인 할 수 있습니다.

두 번째로 설명해 드릴 이벤트는 입력한 가격과 음료의 가격을 비교하여 구매할 수 있는 음료의 버튼만 활성화 시켜주는 이벤트입니다. 아래의 그림과 같이 1,000원을 입력했을 때 그 이상의 가격의 버튼은 비활성화되는 예시입니다.

-----------2019-07-13-------10.45.03

첫 번째 이벤트가 발생이 되면 가격이 변하면서 Drink.js 파일에 사용자가 입력한 가격이 props로 넘어옵니다. VendingMachine.js에서 Drink buttonClick={this.buttonClick} price={this.state.price} item={items[i++]} index={i}로 가격과 음료수 버튼을 클릭하였을 때 발생할 이벤트 함수(buttonClick)를 props로 넘겨줍니다.

처음 화면을 시작할 때에는 모든 음료수의 버튼이 비활성화되어 있습니다. 하지만 가격을 입력하고 나서 해당 가격과 같거나 낮은 가격이 있는 음료수의 버튼만 활성화되어 있는 걸 확인할 수 있습니다. 그 이유는 아래의 소스에서와같이 price를 props로 넘겨받아서 저장된 가격의 값(InputPanel에 입력된 값)을 음료수의 가격과 비교하고 음료수가 1개 이상 있을 때만 버튼이 활성화될 수 있도록 설정이 되어 있기 때문입니다.

JSX에서 Javascript를 사용해야 하면 아래의 예시와 같이 {}를 주위에 두르면 사용할 수 있습니다. 또한, 주의 깊게 보셔야 할 부분이 가격 비교 하는 부분을 javascript 문법인 삼항 조건 연사자를 사용하고 있다는 점입니다.

삼항 조건 연사자 참고 자료 - https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Conditional_Operator

간단히 설명해 드리자면 if…else문을 한 줄로 간결하게 표현한 형식입니다((상태) ? (사실일 경우) : (거짓일 경우)). 해당 예제에서는 입력한 가격이 한 개의 음료수 가격보다 많거나 같을 경우와 음료수의 가격이 0원이 아니며 수량이 0개가 아닐 경우, 버튼을 활성화 시키고 나머지 상황들은 다 비활성화된 버튼이 나타나도록 설정이 되어 있습니다.

class Drink extends React.Component {
    render() {
    const { item, buttonClick, price, index } = this.props;
    return (
        ...
        {price >= item.price && item.price !== 0 && item.quantity !== 0 ?
        <Button onClick={() => buttonClick(index, item)} size='mini' color='red'>GET</Button> :
        <Button size='mini' color='red' disabled>GET</Button>
        }
        ...

Drink.js에서는 buttonClick 함수에 선택을 한 음료수의 정보(음료수명, 가격, 수량)와 index를 파라메타로 넘겨주면서 VendingMachine.js에서 새로운 가격과 아이템의 수량을 setState로 업데이트를 하는 걸 확인하실 수 있습니다.

아래의 코드는 VendingMachine.js에서 buttonClick함수를 선언한 코드입니다. 입력한 가격에서 음료수 가격을 뺀 가격을 newPrice로 함수 안에서 새로 선언을 해주어서 나중에 따로 setState안에서 진행하는 번거로움을 덜어줍니다.

state에 item(음료수) 한 개가 아닌 여러 개가 선언이 되어 있는 상태라서, 기존에 state에 있는 itemsmap을 이용하여 모든 item의 정보들을 확인하도록 사용을 합니다. items안에 각각의 item 정보와 index값을 파라메타로 넘겨주며 buttonClick 함수로 넘어온 index와 map을 사용하여 state에 저장되어 있는 items의 index를 비교하고 음료수의 수량을 한 개씩 감소한 상태로 새로 state에 저장을 시킵니다.

...
buttonClick = ( i, item ) => {
    const newPrice = this.state.price - item.price;

    this.setState({
        items: this.state.items.map((item,index) => {
            if (i-1 === index && item.quantity>0) {
                return { ...item, quantity: --item.quantity }
            }
            return item
        }),
        buttonClicked: true,
        price: newPrice
      })
    }
    ...

고맙습니다.

React + MobX
SPA 라이브러리인 React를 MobX state 관리 라이브러리와 함께 배워봅시다.