본문 바로가기
Front-end Framework/React (Reactjs)

6. React의 이벤트 처리

by kellis 2020. 10. 19.

이 글에서는 React에서의 이벤트를 다루는 방법에 대해서 알아보도록 하겠습니다.

 

본 글의 내용을 이해하기 위해서는 아래와 같은 사전 지식이 필요합니다.

 

Introduction to events

Events are actions or occurrences that happen in the system you are programming, which the system tells you about so you can respond to them in some way if desired. For example, if the user selects a button on a webpage, you might want to respond to that a

developer.mozilla.org

 

 

1. 들어가기

 

지금까지는 사용자와 상호작용 없이 그저 데이터를 보여주는 정도의 예제만을 다뤄보았습니다. 그러나 실제로 정적인 UI를 사용하는 경우는 많지 않고, 사용자 조작에 반응할 수 있는 UI를 만들어야 하는 경우가 대부분입니다. 

간단한 예제로 먼저 살펴보도록 하겠습니다. 

 

[saveButton]

import React, {Component} from 'react'
 
class SaveButton extends Component{
    handleSave(event){
      console.log(this,event)
    }
    render(){
      return <button onClick={this.handleSave.bind(this)}>Save</button>
    }
}
export default SaveButton

버튼이 클릭되었을 때 this와 event를 콘솔에 출력하는 간단한 예제입니다.  이전 장에서 언급했듯이 React에서는 카멜 표기법을 사용하는 것을 볼 수 있습니다. 위의 코드는 constructor에서 이벤트 핸들러를 클래스에 바인딩하도록 해줄 수도 있습니다. 기능적으로 차이가 없을 것 같지만, 만약 render()와 같은 메서드가 여러 번 호출된다면, 생성자에서 바인딩하는 것이 중복을 줄일 수 있습니다. 

class SaveButton extends Component{
    constructor(){
      super(props)
      this.handleSave = this.save.bind(this)
    }
    save(event){
      console.log(this,event)
    }
    render(){
      return <button onClick={this.handleSave}>Save</button>
    }
}

이벤트 핸들러를 생성자에서 바인딩하면 중복을 줄일 수 있을 뿐만 아니라, 모든 바인딩을 한 곳에서 작성할 수 있기 때문에 이 방법을 권장합니다.

React에서 지원하는 이벤트의 목록에 대한 내용은 https://reactjs.org/docs/events.html 에서 확인하실 수 있습니다. 

 

참고. JSX는 xml 형식으로 태그를 작성하여, 렌더링 시에 이 코드를 DOM 객체로 변환합니다. 그러므로 JSX 코드 내부에서 사용하는 this는 html 태그를 지칭하는 this가 아닌, 이 JSX가 사용되는 SaveButton을 지칭하게 됩니다. 

 


2. React의 이벤트 리스너는 document에 등록된다


jQuery나 일반적인 자바스크립트에서 DOM 노드에 직접 이벤트 리스너를 연결하는 방식과 달리 React는 다른 방법으로 이벤트를 처리합니다. 이벤트를 노드에 직접 연결하는 방법은 UI 라이프사이클에서 이벤트를 추가하거나 제거할 때 문제가 생길 수 있기 때문입니다. 

다음 예를 살펴보도록 하겠습니다. 

import React, {Component} from 'react'
class MouseEvent extends Component{
  render(){
    return <div>
              <div onMouseOver={
                      ((event)=>{
                          console.log('mouse over')
                          console.dir(event)
                      })
                    }
              >
               This is Mouse over Event
              </div>
           </div>;
  }
}
export default MouseEvent

참고. 화살표 함수는 자기 자신의 스코프가 존재하지 않습니다. 그러므로 위 onMouseOver에는 bind(this)를 할 필요가 없습니다.

부모 요소인 div에 하나의 이벤트 리스너를 두고, 버블링 되는 이벤트를 처리하도록 하는 것입니다. React는 내부적으로 상위 요소 및 대상 요소에 연결된 이벤트를 매핑에서 추적합니다. React가 부모 요소에서 대상 요소를 추적할 수 있는데, 이는 다음 그림과 같습니다. 

React는 최상위 부모인 document에 이벤트 리스너를 연결하였습니다. 각 요소가 아닌 document에 연결하는 것입니다. 이 덕분에 React는 좀 더 빠르게 동작하게 되는데, 이는 특히 목록에 대한 것을 다룰 때 더욱 그렇습니다. 

콘솔에서 조회해 보면 실제로 document에 이벤트 리스너가 등록되어 있는데, 마우스 오버 이벤트가 제대로 동작하는 것을 볼 수 있습니다. 

 


3. React의 합성 이벤트 객체 다루기

 

브라우저 간의 차이로 인해 이벤트를 처리하는 코드를 작성할 때 크로스 브라우징 문제를 만날 수 있습니다. 일반적으로 이 차이를 처리하기 위해서는 if/else문을 더 작성하고, 서로 다른 브라우저에서 테스트를 해보아야 했습니다. React는 이를 해결하기 위해 브라우저 내장 이벤트를 감쌉니다. 웹 페이지를 실행하는 브라우저의 구현에 관계없이 이벤트가 W3C 명세를 따르도록 만들었습니다. 내부적으로는 합성이벤트(SyntheticEvent)를 위한 특별한 클래스를 사용합니다. SyntheticEvent 클래스의 인스턴스를 이벤트 핸들러에 전달하는 것입니다. 이것이 바로, 위의 예제에서도 보았듯이 이벤트 핸들러 함수에 인자로 event를 넘겨주는 것입니다. 이 이벤트의 프로퍼티와 메서드는 stopPropagation(), preventDefault(), target, currentTarget과 같이 대부분 브라우저 내장 이벤트와 동일하며, 이 내장 프로퍼티나 메서드를 찾을 수 없는 경우에는 nativeEvent를 통해 브라우저의 내장 이벤트에 접근할 수 있습니다. 

React 버전 15의 합성 이벤트 인터페이스에 포함되어 있는 몇 가지 주요한 프로퍼티와 메서드는 다음과 같습니다. 

  • currentTarget : 이벤트를 캡처한 요소의 DOMEventTarget(대상 요소 또는 부모 요소)
  • target : DOMEventTarget. 이벤트가 발생한 요소
  • nativeEvent : DOMEvent. 브라우저 내장 이벤트 객체
  • preventDefault() : 링크나 폼 전송과 같은 기본 동작을 방지
  • stopPropagation() : 이벤트 전파 중단

참고. 언급된 프로퍼티와 메서드만을 다뤘으나 이외에도 굉장히 많은 프로퍼티와 메서드가 존재합니다. 자세한 내용은 Event.target에서 확인해 보시기 바랍니다. 

이벤트 핸들러가 한 번 실행되고 나면 합성 이벤트는 null이 되어 더 이상 사용할 수 없습니다. 그래서 이벤트 핸들러가 실행된 후에 이벤트 객체에 접근하기 위해 event 객체를 전역변수에 담거나, 콜백 함수에서 비동기적으로 사용하려고 생각할 수도 있습니다. 아래 예를 보겠습니다. (위의 마우스 이벤트를 수정하여 warning이 발생하도록 하였습니다)

 

[합성 이벤트는 이벤트 핸들러 실행 후 null이 됩니다]

 

import React, {Component} from 'react'
class MouseEvent extends Component{
  handleMouseOver(event){
    console.log('mouse over')
    window.e = event
    console.dir(event.target)
    setTimeout(()=>{
      console.table(event.target)
      console.table(window.e.target)
    },2000)
  }
  render(){
    return <div onMouseOver={this.handleMouseOver.bind(this)}>mouse over</div>
  }
}
export default MouseEvent

이 코드는 window.e.target에 접근하려고 했을 때 아래와 같은 에러를 뱉어내며, window의 객체역시 null이 됩니다. 

참고. Warning: 이 합성 이벤트는 성능을 이유로 재사용됩니다. 만약 이를 보고 있다면, 이미 방출되었거나 null 처리된 합성 이벤트의 target 프로퍼티에 접근한 것입니다. 이 값은 null로 세팅되어 있습니다. 만약 이벤트 핸들러를 실행한 후에도 합성 이벤트를 유지하고 싶다면, event.persist() 메서드를 사용하여야 합니다.

 

setTimeout 내에서는 event가 클로저되므로, 데이터가 방출됩니다. 따라서 위와 같은 warning이 발생하게 되는 것이며, 이러한 경우가 아닌 null 처리된 합성 이벤트의 target에 접근하게 되면 에러가 발생하게 됩니다. 

 


4. 이벤트 핸들러를 속성으로 전달하기

 

앞 장에서 보았던 예제를 다시 한 번 살펴보겠습니다. 

 

[src/ChangeState.js]

import React, {Component} from 'react';
  
class ChangeState extends Component{
  constructor(props){
    super(props)
    this.state = {input: ""}
  }
  changeInput(e){
    this.setState({input: e.target.value});
  }
  render(){
    return (
      <div>
        <input type="text" value={this.state.input} onChange={this.changeInput.bind(this)}/>
        &nbsp; <span>input : {this.state.input}</span>
      </div>
    )
  };
}
export default ChangeState;

input 창에 사용자가 입력을 넣으면, 우측의 input: 란에 그대로 출력이 되는 예제였습니다. 이 예제를 조금 수정하여, 부모 컴포넌트에서 자식 컴포넌트로 이벤트 핸들러 함수를 전달하여 실행하도록 만들어 보겠습니다.

 

[부모 컴포넌트인 SendEventHandler.js]

import React, {Component, Fragment} from 'react'
import ChangeState from './ChangeState'
 
class SendEventHandler extends Component{
  constructor(props){
    super(props)
    this.changeState = this.changeState.bind(this)
    this.state = {input: ""}
  }
  changeState(event){
    this.setState({input: event.target.value});
  }
  render(){
    return (
      <Fragment>
        <ChangeState input={this.state.input} handler={this.changeState} />
      </Fragment>
    )
  };
}
export default SendEventHandler

ChangeState에게 상태를 넘겨주는 부모 컴포넌트인 SendEventHandler는 자체적인 onClick 이벤트 핸들러가 없습니다. 

 

[수정된 src/ChangeState.js]

import React, {Component} from 'react'
 
class ChangeState extends Component{
  render(){
    return (
      <div>
        <input type="text" value={this.props.input} onChange={this.props.handler}/>
        &nbsp; <span>input : {this.props.input}</span>
      </div>
    )
  };
}
export default ChangeState;

부모 컴포넌트인 SendEventHandler가 props로 input값과 handler를 넘겨주기 때문에, ChangeState는 Stateless 컴포넌트가 됩니다. 또한 ChangeState는 props로 input을 받기 때문에 내부에서 값을 변경할 수 없고, 부모 컴포넌트인 SendEventHandler에서는 state이므로 변경할 수 있습니다.

 

결과는 이전과 동일하게 동작하므로 생략하도록 하겠습니다. 

 

그렇다면 이제 의문이 생기게 될 것입니다. 과연 이벤트 핸들러와 같은 로직은 자식 컴포넌트와 부모 컴포넌트, 어느 쪽에 있어야 하는 것일까요?

 


5. 컴포넌트 간 데이터 교환

 

위의 예제에서는 이벤트 핸들러를 부모 컴포넌트에 작성하였습니다. 이벤트 핸들러를 자식 컴포넌트에 둘 수도 있지만, 부모 컴포넌트에 두게 되면 자식 컴포넌트들과 정보를 교환할 수 있습니다. 

이번에는 두 예제에서 input 값을 제거해보겠습니다. 컴포넌트는 단일 지향적이며 세분화된 표현의 일부로서, input 값을 처리하는 내용에 대해서는 InputValue라는 새로운 컴포넌트를 작성하여 동작하도록 할 것입니다. 

 

부모 컴포넌트는 두 개의 컴포넌트에게 데이터를 전달합니다. 

 

[src/SendEventHandler.js]

import React, {Component} from 'react';
import ChangeState from './ChangeState'
import InputValue from './InputValue'
 
class SendEventHandler extends Component{
  constructor(props){
    super(props)
    this.changeState = this.changeState.bind(this)
    this.state = {input: ""}
  }
  changeState(event){
    this.setState({input: event.target.value});
  }
  render(){
    return (
      <div>
        <ChangeState handler={this.changeState} />
        <InputValue input={this.state.input} />
      </div>
    )
  };
}
export default SendEventHandler

ChangState 자식 컴포넌트는 이벤트를 받아들이고, 

 

[src/ChangeState.js]

import React, {Component} from 'react'
 
class ChangeState extends Component{
  render(){
    return <input type="text" onChange={this.props.handler}/>
  };
}
export default ChangeState;

InputValue 자식 컴포넌트는 이벤트로 들어온 텍스트를 출력합니다. 

 

[src/InputValue.js]

import React, {Component} from 'react'
 
class InputValue extends Component{
  render(){
      return <span>&nbsp;input : {this.props.input}</span>
  }
}
export default InputValue

두 자식 컴포넌트는 부모 컴포넌트를 통해 데이터를 교환하게 됩니다. (참고. 두 자식 컴포넌트는 Stateless 컴포넌트이므로 함수형으로 작성할 수 있습니다)

 

위의 예제의 결과는 마찬가지로 동일하므로 생략하도록 하겠습니다.

 

예제에서 보았듯이 자식 컴포넌트 간에 상호작용이 필요한 경우에는 부모나 컨테이너 컴포넌트에 이벤트 핸들러를 두는 것이 가장 좋은 방법입니다. 그러나 이벤트가 하나의 자식 컴포넌트 에게만 영향을 끼친다면, 상위 컴포넌트를 이벤트 처리 메서드까지 작성하여 어지럽게 만들 필요는 없습니다.

 


6. 폼 다루기

 

React에서 폼이나 텍스트 입력 상자, 버튼 같은 사용자 입력 영역을 처리하려면 문제가 발생합니다. React에서는 "React 컴포넌트는 초기화 시점을 포함하여 어느 시점에서든 뷰의 상태를 표현해야 한다"라고 말합니다. React는 선언적으로 UI를 묘사함으로써 모든 것을 단순하게 유지하는데, 이 말인즉 React는 UI가 결과적으로 어떻게 보여야 할 지에 대해 묘사한다는 것입니다. 전통적인 HTML의 입력 요소들은 사용자 입력에 의해 요소의 상태가 변경됩니다. React는 선언적 스타일을 사용하기 때문에 상태를 적절히 반영하려면 입력이 동적이어야 합니다. 그러므로 컴포넌트의 상태를 자바스크립트에서 관리하지 않고, 뷰를 동기화하지 않으면 내부 상태와 뷰가 다른 경우가 발생할 수도 있는 것입니다.

 

예를 들어보겠습니다. 

render(){
    return <input type="text" name="title" value="title" />
}

이 코드는 state에 관계없이 항상 동일한 뷰이므로 input 영역의 입력값은 항상 title로 유지됩니다. 그러나 원하는 것은 사용자의 입력이나 클릭에 의해 변경되는 것입니다. 

render(){
    return <input type="text" name="title" value={this.state.title} />
}

그럼 위와 같이 변경한다면 state에 따라 입력 값이 갱신되도록 할 수 있을 것입니다. 그렇다면 여기에서 생기는 문제는 state의 값이 무엇인가 하는 것입니다. React는 사용자가 폼 요소에 무언가 작성한다는 것을 알 수 없습니다. 이 변경을 감지하기 위해서는 onChange에 이벤트 핸들러를 추가해야 합니다. 

handleChange(event){
    this.setState({title: event.target.value})
}
render(){
    return <input type="text" name="title" value={this.state.title} onChange={this.handleChange.bind(this)} />
}

이는 지금까지 다뤄왔던 이벤트 처리 방식입니다.

 

정리하자면 내부 상태와 뷰를 동기화하도록 구현하여야 하는 것입니다.

  1. render()에서 상태 값을 이용해 엘리먼트를 정의
  2. onChange를 이용하여 폼 요소에 발생하는 변경 사항 감지
  3. 이벤트 핸들러에서 내부 상태 갱신
  4. 새로운 값이 상태에 저장되면 새로운 render()가 실행되어 뷰 갱신

 

이 방식을 단방향 바인딩이라고 부릅니다. 상태가 뷰를 갱신하는 것이 전부이며, 뷰에서 상태를 바꾸는 반대의 경우가 없습니다. 이는 상태가 뷰를 갱신하는 거대한 규모의 앱을 다룰 때 복잡도를 제거할 수 있다는 장점이 있습니다. 

물론, 단순하다는 것은 항상 적은 양의 코드와 항상 쉬운 것을 의미하지는 않습니다. 바로 위의 예제만 보아도 양방향 바인딩(명시적으로 과정을 구현하지 않아도 뷰에서 알아서 상태를 갱신)을 사용할 때보다 코드가 많습니다. 그러나 이 방식은 복잡한 UI를 다루거나 무수히 많은 뷰와 상태를 가진 단일 페이지 애플리케이션(SPA)을 만들 때 더욱 유리합니다. 

 

Controlled Component
 
이렇게 변경을 감지하여 이벤트 리스너로 상태에 데이터를 저장하는 방식을 제어되는 컴포넌트(controlled component)를 사용하는 방식이라고 합니다. 이 방법을 사용하면 컴포넌트 내부 상태와 뷰를 항상 동기화시킬 수 있습니다. React가 값을 통제하거나 설정하기 때문에 제어되는 컴포넌트라고 부르며, 제어되지 않는 컴포넌트(uncontrolled component)는 value 속성을 설정하지 않는 것을 의미하는데 안티 패턴으로 분류됩니다.  정확한 차이에 대해 알고 싶다면 여기에서 확인해보시기 바랍니다.

 


Next...

다음 글에서는 라우팅에 대한 내용을 살펴보도록 하겠습니다.

 

 

[references]

 

「리액트 교과서」 - 아자트 마르단

React에서 지원하는 이벤트의 목록

Event.target

controlled component

 

Forms – React

A JavaScript library for building user interfaces

reactjs.org

uncontrolled component

 

Uncontrolled Components – React

A JavaScript library for building user interfaces

reactjs.org

controlled component vs uncontrolled component

 

Glossary of React Terms – React

A JavaScript library for building user interfaces

reactjs.org

W3C 명세

 

'Front-end Framework > React (Reactjs)' 카테고리의 다른 글

5. 컴포넌트 라이프사이클 이벤트  (0) 2020.10.19
4. 컴포넌트  (0) 2020.10.19
3. 리액트의 문법 (+JSX)  (0) 2020.10.19
2. React 개발 설정  (0) 2020.10.19
1. React?  (0) 2020.10.19

댓글