React.js - 리액트에서 이벤트 처리하기

Gyu·2022년 4월 11일
0

React.js

목록 보기
7/20
post-thumbnail

React에서 이벤트 처리하기

  • 리액트도 결국에는 자바스크립트이기 때문에 이벤트를 처리하는 방식은 순수 자바스크립트와 동일하다. 먼저 이벤트를 리스닝 후 리스너를 작성하는 것이다.

이벤트 리스닝 : 특정 엘리먼트가 어떤 이벤트를 수신할지 결정하는 것

이벤트 리스너 : 이벤트 리스너란 이벤트가 발생했을 때 그 처리를 담당하는 함수를 가리키며, 이벤트 핸들러(event handler)라고도한다. 지정된 타입의 이벤트가 특정 요소에서 발생하면, 웹 브라우저는 그 요소에 등록된 이벤트 리스너를 실행시킨다.

리액트에서 이벤트 처리하는 방식

  • React 엘리먼트에서 이벤트를 처리하는 방식은 DOM 엘리먼트에서 이벤트를 처리하는 방식과 매우 유사하지만, 몇 가지 문법 차이가 있다. 때문에 이 차이에 주의하여 이벤트를 처리해야 한다.

React의 이벤트는 소문자 대신 카멜 케이스(camelCase)를 사용

  • DOM 엘리먼트는 on+이벤트명 형식의 이벤트 속성을 가지고 있으며, 이벤트 속성은 전부 소문자로 작성을 한다. 하지만 하지만 리액트 엘리먼트에서는 전부 카멜 케이스로 이벤트 속성을 작성해야 한다.
  • DOM 엘리먼트에서 onclick, onmouseover, onsubmit, onchange 등 전부 소문자로 작성됐던 이벤트 속성들은 리액트 엘리먼트에서는 각각 onClick, onMouseOver, OnSubmit, OnChange 등 카멜 케이스로 작성되어야 한다.

JSX를 사용하여 문자열이 아닌 함수로 이벤트 핸들러를 전달

  • 리액트에서는 JSX 자체에 인라인 형식으로 이벤트를 리스닝한다. 리스닝할 이벤트와 호출될 이벤트 핸들러 모두 JSX 마크업 안에 인라인 형식으로 지정해야한다.
    // DOM 엘리먼트에서 이벤트 속성 작성 시 전부 소문자로 작성하며
    // 속성 값을 문자열로 작성하여 이벤트 핸들러를 전달한다. 
    <button onclick="activateLasers()">
      Activate Lasers
    </button>
    
    // 리액트 엘리먼트에서 이벤트 속성 작성 시 카멜케이스로 작성하며
    // 속성 값은 문자열이 아닌 표현식을 이용해 함수로 이벤트 핸들러를 전달한다.
    <button onClick={activateLasers}>
      Activate Lasers
    </button>
  • React를 사용할 때 DOM 엘리먼트가 생성된 후 리스너를 추가하기 위해 addEventListener를 호출할 필요가 없다. 대신, 엘리먼트가 처음 렌더링될 때 리스너를 제공하면 된다.

이벤트 기본 동작 방지

  • DOM 엘리먼트의 경우 이벤트 핸들러가 false를 반환해도 해당 엘리먼트의 기본 동작을 방지할 수 있었지만, 리액트 엘리먼트의 경우 반드시 핸들러가 preventDefault를 호출해야 한다.
    // DOM 엘리먼트에서는 핸들러가 false를 반환하면 a 태그의 기본 동작이 방지됨
    <a href="#" onclick="console.log('The link was clicked.'); return false">
      Click me
    </a>
    
    // 리액트에서는 a 태그의 기본 동작을 막기위해서 반드시 
    // preventDefault를 호출해야한다.
    function ActionLink() {
      function handleClick(e) {
        e.preventDefault();
        console.log('The link was clicked.');
      }
    
      return (
        <a href="#" onClick={handleClick}>
          Click me
        </a>
      );
    }

이벤트 핸들러 내부의 this

  • 클래스 컴포넌트의 경우 JSX 콜백 안에서 this의 의미에 대해 주의해야 한다. JavaScript에서 클래스 메서드는 기본적으로 바인딩되어 있지 않기 때문이다. 따라서 기본적으로 onClick등으로 전달한 이벤트에서 this를 호출한 경우 바인딩 되어있지 않기 때문에 undefined로 표시된다.

💡 이벤트 콜백에서 window가 아닌 undefined가 나오는 이유는 React가 development 모드에서는 strict mode로 검사를 하기 때문이다. production build 에서는 포함되지 않는다.

  • 리액트에서는 onClick={this.handleClick}과 같이 뒤에 ()를 사용하지 않고 메서드를 참조할 경우, 해당 메서드에 this를 바인딩 해야한다.
  • 리액트에서 권장하는 이벤트 바인딩 방법
    // 1. 생성자에서 bind()를 사용하는방법
    class LoggingButton extends React.Component {
      Constructor(props) {
        super(props);
    		// 콜백에서 `this`가 작동하려면 아래와 같이 바인딩해야 한다.
        this.handleClick = this.handleClick.bind(this);
      }
    
      handleClick() {
        console.log('this is:', this);
      }
    
      render() {
        return (
          <button onClick={this.handleClick}>
            Click me
          </button>
        );
      }
    }
    
    // 2. 사용할 곳에서 bind()를 사용하는 방법
    class LoggingButton extends React.Component {
      handleClick() {
        console.log('this is:', this);
      }
    
      render() {
        return (
          <button onClick={this.handleClick.bind(this)}>
            Click me
          </button>
        );
      }
    }
    
    // 3. 화살표 함수로 직접 호출하는 방법 
    // 이 방법은 LoggingButton이 랜더링 될 때마다 새로운 함수를 생성하는 문제가 있다.
    // 콜백 함수 내에서 재랜더링을 발생시키는 경우 성능 문제가 발생할 수 있다.
    class LoggingButton extends React.Component {
      handleClick() {
        console.log('this is:', this);
      }
    
      render() {
        return (
          // 이 문법은 `this`가 handleClick 내에서 바인딩되도록 한다.
          <button onClick={(e) => this.handleClick(e)}>
            Click me
          </button>
        );
      }
    }
    
    // 4. 화살표 함수로 선언하고 바인딩하는 방법
    // 이 방법으로 이벤트를 바인딩 하는것을 권장한다.
    class LoggingButton extends React.Component {
      handleClick = () => {
        console.log('this is:', this);
      }
    
      render() {
        return (
          <button onClick={this.handleClick}>
            Click me
          </button>
        );
      }
    }

이벤트 속성

class LoggingButton extends React.Component {
  Constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick(e) {
    console.log('this is:', this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}
  • handleClick(e) 처럼 모든 이벤트 핸들러에는 해당 이벤트에 대한 정보를 담고 있는 이벤트 객체가 매개변수로 전달된다.
  • 리액트에서 이벤트 핸들러에 전달된 이벤트 객체는 DOM 엘리먼트에서 전달되는 이벤트 객체와 다르다. JSX에 이벤트를 지정하는 경우 DOM 이벤트가 아닌 합성 이벤트라고 하는 리액트의 특별한 이벤트 유형인 SyntheticEvent를 다룬다.
  • 즉 리액트에서 이벤트 핸들러는 네이티브 이벤트 객체가 아닌 브라우저의 네이티브 이벤트를 래핑하는 SyntheticEvent를 매개변수로 전달받는다.
  • React는 W3C 명세에 따라 SyntheticEvent를 정의하기 때문에 기존 DOM 엘리먼트에서 이벤트 핸들러가 이벤트 객체를 이용해 작업하는 것과 동일한 작업을 리액트에서도 할 수 있다. 즉 브라우저 호환성에 대해 걱정할 필요가 없다는 것이다.
  • 하지만 SyntheticEvent는 기존 브라우저 이벤트를 래핑하는 것이지 일대일 대응하는 것이 아니기 때문에 DOM 이벤트 문서가 아닌 리액트 합성 이벤트 문서를 참고해야 한다. 일부 DOM 이벤트는 SyntheticEvent에 존재하지 않을 수 있다.

리액트 합성 이벤트 문서

이벤트 핸들러에 매개변수 전달하기

  • 이벤트 핸들러에 매개변수를 전달하는 방법 또한 기존의 방식과 조금 다르다.
class ClickEventEx extends React.Component {
    constructor(props) {
        super(props);
        this.clickHandle = this.clickHandle.bind(this);
    }

    clickHandler(msg) {
        console.log(msg);
    }

    render() {
        return (
            <button onClick={this.clickHandler("Hi")}>Click</button>
        );
    }
}
  • 위와 같이 코드를 작성하면 버튼을 눌렀을 때가 아닌 화면이 렌더링 됐을 때 clickHandler() 함수의 명령이 실행된다.
  • onClick={함수이름} 이 아닌 onClick={함수이름()} 으로 이벤트 핸들러를 작성할 경우 clickHandler() 함수를 먼저 실행 한 뒤 리턴 값을 onClick에 전달하게 된다.
  • clickHandler() 함수를 아래와 같이 함수의 return 값을 함수로 전달하는 방식으로 수정할 경우, 버튼을 눌렀을 때 return 한 함수가 작동한다.
    clickHandle(msg) {
        console.log(msg);
        return () => {
            console.log("클릭 시 리턴한 함수가 전달된다.")
        }
    }
  • 이벤트 핸들러에 매개변수를 전달하는 방법에는 2가지가 있다.
    1. 화살표함수를 사용하는 방법

      <button onClick={(e) => this.clickHandler("Hi", e)}>Click!</button>
    2. bind() 함수를 사용하는 방법

      <button onClick={this.clickHandler.bind(this,"Hi")}>Click!</button>
    • 화살표 함수를 사용할 경우 불필요한 랜더링 이슈가 있으므로 2번 방식을 사용는 것을 권장한다.
    • 두 경우 모두 React 이벤트를 나타내는 e 인자가 두 번째 인자로 전달된다. 화살표 함수를 사용하면 명시적으로 인자를 전달해야 하지만 bind를 사용할 경우 추가 인자가 자동으로 전달된다.

컴포넌트간 이벤트 통신

  • 아래의 코드는 문법상의 오류는 없지만 버튼을 클릭 했을 때 아무 작동을 하지 않는다.
    class Counter extends React.Component {
        constructor(props) {
            super(props);
            this.state = {num: 0}
            this.increase = this.increase.bind(this);
        }
    
        increase(e) {
            this.setState({
                num: this.state.num + 1
            })
        }
    
        render() {
            return (
                <div>
                    <p>{this.state.num}</p>
                    <BtnComp onClick={this.increase}/>
                </div>
            )
        }
    }
    
    class BtnComp extends React.Component {
        constructor(props) {
            super(props);
        }
    
        render() {
            return(
                <button>Click +</button>
            );
        }
    }
    
    ReactDOM.render(
        <Counter/>,
        document.getElementById('app')
    )
  • BtnComp 컴포넌트는 HTML 엘리먼트 하나민 리턴할 뿐 클릭 시 아무 작동을 하지 않는다. 컴포넌트는 이벤트를 직접 리스닝 할 수 없다. 왜냐하면 컴포넌트는 DOM 엘리먼트를 감싸는 Wrapper이기 때문이다.
  • 위 예제의 경우 onClick 이벤트를 리스닝하고 increase() 함수를 실행해야 하는 것은 정확히 말하면 BtnComp 컴포넌트 자체가 아니라 BtnComp의 button 태그다.
  • 때문에 BtnComp의 button 태그 클릭 시 우리가 원하는 동작을 하기 위해서는 컴포넌트 안에서 DOM 엘리먼트에 이벤트를 할당하고 props로 이벤트 핸들러는 전달하면 된다.
class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {num: 0}
        this.increase = this.increase.bind(this);
    }

    increase(e) {
        this.setState({
            num: this.state.num + 1
        })
    }

    render() {
        return (
            <div>
                <p>{this.state.num}</p>
            	// BtnComp에 clickHandler라는 이름으로 increase()를 props로 전달한다.
                <BtnComp clickHandler={this.increase}/>
            </div>
        )
    }
}

class BtnComp extends React.Component {
    constructor(props) {
        super(props);
    }

    render() {
        return(
          // BtnComp에서는 increase() props로 전달받아
          // onClick의 이벤트 핸들러로 설정한다. 
          <button onClick={this.props.clickHandler}>Click +</button>
        );
    }
}
  • 또 다른 예시
const ClickCounterButton = props => {
  return (
    <button onClick={props.handler}>Don`t touch me with your dirty hands!</button>
  );
};

class Counter extends React.Component {
  render() {
    return <span>Clicked {this.props.value} thies.</span>;
  }
}

class Content extends React.Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
  }

  handleClick = (event) => {
    this.setState({counter: ++this.state.counter });
  }

  render() {
    return  (
      <div>
        <ClickCounterButton handler={this.handleClick} />
        <br/>
        <Counter value={this.state.counter}/>
      </div>
    )
  }
}

React가 지원하지 않는 DOM 이벤트 처리하기

  • 모든 DOM 이벤트가 SyntheticEvent에 대응하는 것은 아니다. 가령 SyntheticEvent은 resize 이벤트를 인식하지 못한다.
  • resize나 사용자 정의 이벤트 처럼 리액트가 인식할 수 없는 이벤트의 경우, 생명주기 이벤트에서 addEventListener를 사용하는 전통적인 방법을 사용해야 한다.
class Something extends React.Component {
    handleMyEvent(e) {
        // 이벤트 처리
    }

    componentDidMount() { // 이벤트 등록
        window.addEventListener("someEvent", this.handleMyEvent);
    }

    componentWillUnmount() { // 이벤트 제거
        window.removeEventListener("someEvent", this.handleMyEvent);
    }

    render() {
        return (
            <div onSomeEvent={this.handleMyEvent}>Some Event</div>
        )
    }
}

리액트 이벤트 처리 방식이 전통적인 방식과 다른 이유

  1. 브라우저 호환성
    • 낮은 버전의 브라우저의 경우 이벤트 처리가 일관되게 작동하지 않을 수 있다. 때문에 리액트는 기존 이벤트 객체를 SyntheticEvent로 래핑함으로써 호환되지 않는 환경에서도 이벤트 처리를 동일한 방법으로 할 수 있게 한다.
  2. 성능 향상
    • 이벤트 핸들러는 많으면 많을 수록 많은 메모리를 차지한다.
    • 리액트는 DOM 엘리먼트에 직접 이벤트 핸들러를 부착하지 않는다. 리액트는 문서 최상위에 있는 하나의 이벤트 핸들럴르 사용한다. 이 핸들러는 모든 이벤트를 리스닝하며, 이벤트 발생 시 적합한 개별 핸들러를 호출하는 역할을 한다.
    • 이는 이벤트 처리 코드를 개발자가 직접 최적화 하지 않아도 되게 해준다.
profile
애기 프론트 엔드 개발자

0개의 댓글