Reactㅣ이벤트 다루기, 캡쳐, 버블링, 합성 이벤트, 핸들러 속성 등등

휘Bin·2023년 7월 1일
0
post-thumbnail

이벤트 핸들러를 정의해서 사용자 조작에 대응할 수 있는 React 엘리먼트를 만들 수 있어야 한다. JSX로 작성한 엘리먼트에 속성 값으로 이벤트 핸들러를 정의한다. 속성으로 사용하는 이름은 카멜 표기법으로 작성한다.

<button onClick={(function(event){
	console.log(this, event)
}).bind(this)}>
Save
</button>

위 코드는 사용자가 버튼을 클릭했을 때 실행할 이벤트 리스너를 정의한 것이다. 이벤트 리스너에서 this를 콘솔에 출력하도록 했다. 여기서 event 객체는 내장 DOM 이벤트 객체를 개선한 것이다. '합성 이벤트(SyntheticEvent)' 라고 부르기도 한다.

bind()를 이용하면, 이벤트 핸들러 함수가 클래스의 인스턴스인 React 엘리먼트에 대한 참조를 유지할 수 있게 한다. 만약 바인딩하지 않으면 use strict를 선언한 상태에서 this는 null이 된다. 하지만 아래와 같은 경우에는 bind(this)로 바인딩 하지 않을 수 있다.

  • this를 이용해서 해당 클래스를 참조할 필요가 없을 때

  • ES6+ 클래스 대신 예전 방식인 React.createClass()를 사용할 때, 이 때는 createClass()가 자동으로 바인딩해준다.

  • 화살표 함수 (() =>{})를 사용할 때

또한, 클래스의 constructor에서 이벤트 핸들러를 클래스에 바인딩할 수도 있다. 기능적으로는 차이가 없다. 하지만 render()에서 같은 메서드를 한 번 이상 사용한다면 생성자에서 바인딩하여 중복을 줄일 수 있다.
예시를 들면 아래와 같다.

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

이벤트 핸들러를 생성자에서 바인딩하면 중복을 제거할 수도 있고, 모든 바인딩을 한 곳에서 작성할 수 있어 이 방법이 좋다.

캡쳐 및 버블링 단계

React는 명령형이 아니라 선언형 스타일이라 객체를 조작할 필요가 없다. 대신에 onClick={handleSave}처럼 JSX에 속성으로 이벤트를 선언한다.

예를들어 onMouseOver 같은 이벤트는 버블링 단계의 이벤트에서 실행된다.
첫 번째는 '캡처 단계'로로 window에서 대상 요소까지다. 두 번째는 대상 요소에 도착한 '대상 단계' 이다. 그리고 나서야 세 번째로 '버블링 단계'로 이벤트가 트리를 따라 다시 window로 돌아가게 된다.

대상 요소와 그 상위 요소에 같은 이벤트가 있을 때 단계 간의 구분이 중요하게 작용한다. **버블링 모드에서는 이벤트가 가장 내부에 있는 대상 요소에서 이벤트를 캡처한 후, 대상 요소의 부모 요소를 시작으로 외부의 상위 요소로 이벤트가 전파된다. 반면 캡처 모드**는 이벤트가 가장 바깥 쪽의 요소에 의해 캡처된 후 내부 요소로 전파된다.

캡처 단계를 위한 이벤트 리스너를 등록할 때는 이벤트 이름 뒤에 Capture를 붙여 작성한다. 예를 들면, onMouseOverCapture 라고 쓰는 것이다.

onMouseOver와 onMouseOverCapture가 있다면 당연히 캡처 이벤트가 먼저 출력이 된다.

이러한 동작 원리를 응용해 이벤트 전파를 중지시키거나 이벤트 간 우선순위를 정할 수 있게 한다.

React 이벤트

jQuery나 이반적인 자바스크립트에서는 DOM 노드에 직접 이벤트 리스너를 연결한다. 하지만 React는 다르다. 이벤트를 노드에 직접 연결하는 방식은 UI 라이프사이클에서 이벤트를 추가하거나 제거할 때 문제가 생길 수 있다.

더 나은 방법은 부모요소에 하나의 이벤트 리스너를 두고, 버블링되는 이벤트를 처리하는 것이다. => 이벤트를 하위 요소에서 처리하지 않으면 DOM 트리를 따라 위로 버블링 된다.

React는 내부적으로 상위 요소 및 대상 요소에 연결된 이벤트를 매핑에서 추적한다. React가 부모요소(document)에서 대상 요소를 추적할 수 있다.
즉, React의 이벤트는 최상위(Document)에서 처리한다.

React는 최상위에서 이벤트 리스너를 재사용하므로 이벤트를 연결한 엘리먼트가 여러 개더라도 각 종류별로 하나의 이벤트 리스너만 사용한다.

React 합성 이벤트

브라우저 간의 차이로 인해 이벤트를 처리하는 코드를 작성할 때, 크로스 브라우징 문제를 경험할 수 있다.

이를 위한 React의 해결책은 브라우저 내장 이벤트를 감싸는 것이다. 즉, 웹 페이지를 실행하는 브라우저의 구현에 상관없이 이벤트가 W3C 명세를 따르도록 했다. 그래서 내부적으로 React는 '합성 이벤트(SyntheticEvent)'를 위한 특별한 클래스를 사용한다. 합성 이벤트 클래스의 인스턴스를 이벤트 핸들러에 전달하는 것이다.

class Mouse extends React.Component{
	render(){
    	return <div>
        	<div
            	style={{border: '1px solid red'}}
                onMouseOver={((event) => {
                	console.log('thanks')
                    console.dir(event)})}>
                    hahaha
            </div>
       	</div>
    }
}
  • 첫 번째 event 줄에서 event 인자를 정의한다.

  • 두 번째 event 줄에서 합성 이벤트 객체에 접근해서 console.dir로 콘솔에 노출하게 한다.

이벤트의 프로퍼티와 메서드는 메서드는 stopPropagation(), preventDefault(), target, currentTarget처럼 대부분의 브라우저 내장 이벤트와 같다.
내장 프로퍼티나 메서드를 찾을 수 없을 때는 nativeEvent를 통해 브라우저의 내장 이벤트에 접근할 수도 있다.

React의 합성 이벤트 인터페이스에 포함되어 있는 프로퍼티와 메서드를 몇 가지 살펴보면 아래와 같다.

  • currentTarget : 이벤트를 캡처한 요소의 DOMEventTarget

  • target : DOMEventTarget, 이벤트가 발생한 요소

  • nativeEvent : DOMEvent, 브라우저 내장 이벤트 객체

  • preventDefault() : 링크나 폼 전송 버튼처럼 기본 동작을 방지하는 메서드

  • isDefaultPrevented() : 기본 동작이 방지되었을 때 실행하면 true 반환

  • stopPropagation() : 이벤트 전파 중단

  • isPropagationStopped() : 이벤트 전파가 중단되었을 때 실행하면 true 반환

  • type : 태그명 문자열

  • persist() : 합성 이벤트를 이벤트 풀에서 꺼낸 후 사용자 코드에서 이벤트에 대한 참조 유지를 할 수 있게 한다.

  • isPersistent() : 합성 이벤트를 이벤트 풀에서 꺼낸 경우 실행하면 true 반환

이벤트 핸들러가 한 번 실행되면 합성 이벤트는 null이 되어 더 이상 사용할 수 없다.
당연히 비동기 콜백함수나 전역변수에 담아 사용할 수 없다.

이벤트 핸들러 실행 후 합성 이벤트를 유지하고 싶다면 event.persist() 메서드를 사용하면 이벤트 객체가 재사용되지 않게되어 null로 처리되지 않는다.

다시 한번 간단히 정리해보자면,
React는 내장 이벤트 객체를 크로스 브라우징 목적으로 감싸서 브라우저 이벤트를 합성한다. 합성 이벤트 덕분에 모든 부라우저에서 이벤트가 똑같이 작동하게 된다.

이벤트와 상태 사용하기

이벤트와 함께 상태를 사용하여 이벤트를 처리하고 컴포넌트의 상태를 변경할 수 있게하여 사용자 조작과 상호작용하는 UI를 만들 수 있다.

클릭하면 숫자가 증가하는 예시를 들어보면, 코드는 아래와 같다.

class Content extends React.Component {
  constructor(props) {
    super(props)
    this.state = {counter: 0}
  }
  handleClick(event) {
    this.setState({counter: ++this.state.counter})
  }
  render() {
    return (
      <div>
        <button
          onClick={this.handleClick.bind(this)}
          className="parc">
          {this.state.counter} times
        </button>
      </div>
    )
  }
}

코드를 보면 그래도 이해하기 쉬울 것이라 생각한다. 그래도 조금 몇 가지 힌트를 주면

  • constructor() : this.state에 카운터 값을 0으로 설정해 뷰에서 사용

  • handleClick() : 카운터의 숫자를 증가시키는 이벤트 핸들러

  • render() : JSX로 작성한 버튼을 반환하는 render() 메서드

이벤트 핸들러 속성으로 전달

만약 상태비저장 컴포넌트로 만든 버튼이 있다면, 이벤트 리스너를 연결해 실행시킬 수 있을까?

상태비저장 컴포넌트에서 발생하는 이벤트를 처리하는 방법은 이벤트 핸들러를 상태비저장 컴포넌트의 속성으로 전달하고, 전달한 이벤트 핸들러 함수를 상태비저장 컴포넌트에서 실행하도록 하는 것이다.

※ 상태저장 컴포넌트, 상태비저장 컴포넌트를 각각 '둔한 컴포넌트', '영리한 컴포넌트', 또는 '프레젠테이션 컴포넌트', '컨테이너 컴포넌트' 라고 부른다.

profile
One-step, one-step, steadily growing developer

0개의 댓글