티스토리 뷰

React:

React에서 이벤트 다루기





정적인 UI를 사용하는 경우는 많지 않다. 사용자 조작에 반응 할 수 있는 영리한 UI를 만들어야 하는 경우가 대부분이다. 이벤트에 대한 지식은 7장에서 살펴볼 폼과 폼 요소를 다루는 데도 필요하다. React는 특정 이벤트만을 지원하므로 리액트가 지원하지 않는 이벤트를 다루는 방법도 살펴보자.


| React에서 DOM이벤트 다루기


속성으로 사용하는 이벤트 이름은 표준 W3C DOM 이벤트를 onClick, onMouserOver 처러 카멜 표기법으로 작성한다. 다음 예제 코드에서는 React에서 사용자가 버튼을 클릭했을때 실행할 이벤트 리스너를 정의하고 있다. 이벤트 리스너에서 this를 콘솔에표시하도록 했다. 여기서 event 객체는 내장DOM이벤트객체를 개선한 것으로, 합성 이벤트(Synthetic Event) 라고 부른다.

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


render()에서 같은 메서드를 한번 이상 사용한다면 생성자에서 바인딩하여 중복을 줄일 수 있다. SaveButton 컴포넌트의 생성자에서 이벤트 핸들러를 바인딩하면 아래와 같다.


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>
}
}
이벤트 핸들러를 생성자에서 바인딩하면 중복을 제거할 수 있고, 모든 바인딩을 한곳에서 작성할 수 있으므로 위의  방법을 추천한다! 실제로 나도, 위의 방식으로 constructor에 작성하여 어떤메소드를 작성했는지도 확인하며, 위와 같이 코드를 짜고있다.!\

  • 리액트에서 지원하는 DOM 이벤트




| 캡쳐 및 버블링 단계


  • React는 명령형이 아니라 선언형 스타일이므로, 객체를 조작할 필요가 없고 jQuery 처럼 이벤트를 등록하지 않는다. 대신, onClick={handleSave} 처럼 JSX 에 속성으로 이벤트를 선언한다. 
  • onMouseOver 같은 이벤트는 버블링 단계의 이벤트에서 실행된다. 버블링이나 타깃단계에 앞서, 캡쳐 단계가 있다. 첫번째는 캡처 단계로 window에서 대상요소까지다. 다음이 대상요소에 도착한 대상단계이다. 그후 비로소 버블링 단계가 되고, 그림 처럼 트리를 따라 window로 다시 돌아간다.
  • 대상요소와 그상위 요소에 같은 이벤트가 있을 때 단계 간의 구분이 중요해진다. 버블링 모드에서는 이벤트가 가장 내부에 있는 대상 요소에서 이벤트를 캡처한후, 대상 요소의 부모 요소를 시작으로 외부의 상위 요소로 이벤트가 전파된다. 캡처 모드에서는 이벤트가 가장 바깥 쪽의 요소에 의해 캡처된 후 내부 요소로 전파된다.




  • 아래와 같이, 캡처이벤트에 이어지는 버블링  이벤트를 작성할수 있다.


class Mouse extends React.Component {
 render() {
   return <div>
     <div
       style={{border: '1px solid red'}}
       onMouseOverCapture={((event)=>{
         console.log('mouse over on capture event')
         console.dir(event, this)}).bind(this)}
       onMouseOver={((event)=>{
         console.log('mouse over on bubbling event')
         console.dir(event, this)}).bind(this)} >
        Open DevTools and move your mouse cursor over here
       </div>
   </div>
}
}

위에서는 캡쳐 이벤트가 먼저 출력되고, 이러한 동작 원리를 응용해서 이벤트 전파를 중지시키거나 이벤트 간의 우선순위를 정할 수 있다.


결론: 이벤트가 UI의 기초이므로, 리액트가 이벤트를 어떻게 구현했는지 이해하는 것은 중요하다. 


React 이벤트 살펴보기


  • jQuery나 일반적인 자바스크립트에서는 DOM노드에 직접 이벤트 리스너를 연결하지만, React에서는 다른 방법으로 이벤트를 처리한다. 
  • 효율적인 방법은, 부모요소에 하나의 이벤트 리스너를 두고, 버블링되는 이벤트를 처리하는 것이다 (이벤트를 하위요소에서 처리하지 않으면 DOM트리를 따라 위로 버블링 된다.) React는 내부적으로 상위 요소 및 대상 요소에 연결된 이벤트를 매핑에서 추적한다. 아래 그림 참고


  • 리액트는 이벤트 리스너를 최상위 부모인 document 에 연결했다.

React가 이벤트를 각 요소가 아닌 document에 연결한다.  이 덕분에, 리액트는 좀 더 빠르게 작동한다.(특히 목록을 다룰때 그렇다.) 이는 jQuery에서 개별 요소에 이벤트를 연결하는 점과 비교할수 있다. 성능을 생각한 React이기 때문이다.



React 합성 이벤트 객체 다루기


  • 브라우저에 따라 W3C 명세를 다르게 구현할 수 있다. 브라우저 간의 차이로 인해 이벤트를 처리하는 코드를 작성할 때 크로스 브라우징 문제를 경험할 수 있다. 예를 들어 IE8에서 대상요소를 가져오려면 event.srcElement 에 접근 해야 하지만, Chrome, Safari, Firefox 브라우저 에서는 event.target으로 접근한다.
  • React의 해결책은 브라우저 내장 이벤트를 감싸는 것이다. 웹 페이지를 실행하는 브라우저의 구현에 관계없이, 이벤트가 W3C 명세를 따르도록 만들었다. 내부적으로 React는 합성 이벤트를 위한 특별한 클래스를 사용한다. SyntheticEvent 클래스의 인스턴스를 이벤트 핸들러에 전달 하는 것이다. 예를들어, 합성이벤트 객체에 접근하려면 아래 예제콛으 처럼 이벤트 핸들러 함수에 인자로 event 를 추가할 수 있다.

class Mouse extends React.Component {
 render() {
   return <div>
     <div
       style={{border: '1px solid red'}}
       onMouseOver={((event)=>{
         console.log('mouse is over with event')
         console.dir(event)}).bind(this)} >
        Open DevTools and move your mouse cursor over here
       </div>
   </div>
}
}
  • 이벤트의 프로퍼티와 메서드는 stopPropagation(), preventDefault(), target.currentTarget 처럼 대부분의 브라우저 내장 이벤트와 같다. 내장 프로퍼티나 메서드를 찾을 수 없을 때는 nativeEvent를 통해서 브라우저의 내장 이벤트에 접근 할 수 있다.

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

  1. currentTarget : 이벤트를 캡처한 요소의 DOMEventTarget (대상요소 또는 부모 요소일수 있다)
  2. target: DOMEventTarget, 이벤트가 발생한 요소
  3. nativeEvent: DOMEvent, 브라우저 내장 이벤트 객체
  4. preventDefault(): 링크나 폼 전송 버튼처럼 기본 동작을 방지하는 메서드
  5. isDefaultPrevented(): 기본 동작이 방지되었을 때 실행하면 true를 반환한다.
  6. stopPropagation():이벤트 전파 중단
  7. isPropagationStopped(): 합성 이벤트를 이벤트 풀에서 꺼낸 후 사용자 코드에서 이벤트에 대한 참조를 유지할 수 있도록 한다.
  8. type: 테그명 문자열
  9. persist(): 합성 이벤트를 이벤트 풀에서 꺼낸 후 사용자 코드에서 이벤트에 대한 참조를 유지할 수 있도록 한다.
  10. isPersistent(): 합성이벤트를 이벤트 풀에서 꺼낸 경우 실행하면 true를 반환한다.
  • 나는, 아직까지는 preventDefault, stopPropagation 등등의 메소드 들만 사용해 보았는데 위와 같은 이벤트 메소드들을 잘활용하면 ux면에서 더 훌륭한 리액트 웹앱을 만들 수 있을것이다. 유저와 이벤트는 밀접하게 관련이 있기 때문이다.! 그리고, 이벤트 객체를 리액트 자체에서, 새로운 합성이벤트로 만든다는 점이 흥미로웠다. 이는 크로스 브라우징을 위했다고 하니, 리액트는 여러 단점들을 많이보완한 라이브러리 라는 점을 여실히 느끼고 있다.
  • 그리고 위에서 주의할 점은 event 객체의 target 프로퍼티는 이벤트가 캡쳐된 곳이 아니라 이벤트가 발생한 DOM 노드로 currentTarget과는 차이가 있다.
https://webisfree.com/2017-09-06/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-event-target-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0%EC%99%80-currenttarget-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%9D%80 참고

(예를들어 쉽게말하면 div 태그안에 p 태그가 있고, 이벤트 핸들러가 div 태그에 있으면 클릭되었을때 event.taget은 클릭된 p태그, event.currentTarget은 이벤트 핸들러가 바인딩 된 부모요소인 div 태그를 가리키게 된다.)
  • 입력상자의 텍스트가 필요한 경우도 있는데, event.taget.value로 접근한다.
  • 이벤트핸들러가 한 번 실행되고 나면 합성 이벤트는 null이 되어 더이상 사용 할 수 없다. 따라서 이벤트 핸들러를 실행한 후에도 합성 이벤트를 유지하려면 event.persist() 메서드를 사용하면된다.
결론: 합성이벤트 덕분에 모든 브라우저에서 이벤트가 똑같이 작동한다.

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


  • 앞에서 리액트가 지원하는 이벤트 목록을 살펴 보았는데, 리액트가 지원하지않는 dom이벤트 들도 있다. 예를 들어, 창 크기가 바뀌는, resize 이벤트에 따라 크기를 크거나 작게 변경하는 ui를 만들어야 할수도 있다. 이 이벤트는 리액트가 지원하지 않는다. 
  • 따라서,이러한 미지원이벤트에 연결하려면 리액트 컴포넌트 라이프사이클 이벤트를 사한다. componentDidMount()에서, window의 resize이벤트리스너를 추가하고, 같은 이벤트리스너를 componentWillUnmount()에서 제거해서 컴포넌트가 dom에서 제거될때 이벤트 리스너도 제거한다. 
  • 컴포넌트를 제거한 후에 이벤트 리스너를 방치하는것은 메모리 누수를 일으켜 갑자기 애플리이션이 중단 될 수 있다고 한다.


아래가 책에 소개된 예제이다.미지원이벤트인 resize를 활용해 화면크기를 변경하면 이에 따라 크기가 바뀌는 라디오 버튼을 만드는 예제이다.

class Radio extends React.Component {
 constructor(props) {
   super(props)
   this.handleResize = this.handleResize.bind(this)
   let order = props.order
   let i = 1
   this.state = {
     outerStyle: this.getStyle(4, i),
     innerStyle: this.getStyle(1, i),
     selectedStyle: this.getStyle(2, i),
     taggerStyle: {top: order*20, width: 25, height: 25}
  }
}
 getStyle(i, m) {
   let value = i*m
   return {
     top: value,
     bottom: value,
     left: value,
     right: value,
  }
}
 componentDidMount() {
   window.addEventListener('resize', this.handleResize)
}
 componentWillUnmount() {
   window.removeEventListener('resize', this.handleResize)
}
 handleResize(event) {
   let w = 1+ Math.round(window.innerWidth / 300)
   this.setState({
     taggerStyle: {top: this.props.order*w*10, width: w*10, height: w*10},
     textStyle: {left: w*13, fontSize: 7*w}
  })
}
 render() {
   return <div>
     <div className="radio-tagger" style={this.state.taggerStyle}>
       <input type="radio" name={this.props.name} id={this.props.id}>
       </input>
       <label htmlFor={this.props.id}>
         <div className="radio-text" style={this.state.textStyle}>{this.props.label}</div>
         <div className="radio-outer" style={this.state.outerStyle}>
           <div className="radio-inner" style={this.state.innerStyle}>
             <div className="radio-selected" style={this.state.selectedStyle}>
             </div>
           </div>
         </div>
       </label>
     </div>
   </div>
}
}
  • 위의 요점은 컴포넌트 라이프사이클 이벤트를 이용하면 리액트가 지원하지 않는 이벤트 리스너도 생성할 수 있다는 점이다. 예제에서는 window를 활용했다.
React를 다른 라이브러리와 통합하기 : jQuery UI 이벤트

  • React에서 제공하지 않는 dom 이벤트를 컴포넌트 라이프사이클 이벤트인 componentDidMount와 componentWillUnmount 에서 등록할 수 있다.
  • jquery이벤트에서 window 로 이벤트를 전달하고, 리액트 컴포넌트 라이프사이클이벤트에서 window에 이벤트 리스너를 연결하는 방식으로 구현할 수 있다.

이는 예제코드가 있는 깃헙링크를 첨부하겠다.

https://github.com/gilbutITbook/006961/tree/master/ch06/slider

리액트는 componentDidMount() 라이프사이클 메서드를 통해서 다른 라이브러리의 이벤트를 멋지게 다룰수있다. 리액트는 그다지 자체적인 방식을 강요하지 않으므로, 라이브러리의 이벤트를 멋지게 다룰 수있다. 리액트를 다른 라이브러리와 쉽게 통합할 수 있다는것은 큰 장점이라고 한다. 개발자들은 전체 애플리케이션을 처음부터 다시 작성하지 않고도 서서히 react로 변경하거나, 기존에 사용해온 라이브러리를 리액트와 함께 사용할 수도 있는것이다. 

-> 기존 jQuery를 사용합 웹을 리액트로 마이그레이션할때 이점을 잘 활용할 수 있을 것 같았다. 더더욱 리액트를 열심히 해야겠다는 생각이 들었다 ㅎㅎ

| 마치며

브라우저의 이벤트는 웹 개발자로써는 꼭 알아야할 개념이기 때문에 이벤트의 개념에 (이벤트 캡쳐, 버블링 )대해서도 학습할수 있던 아주 좋은 내용이었고, 리액트는 합성이벤트를 활용해서 크로스 브라우징을 지원하고, 또 다른 라이브러리와도 쉽게 이벤트들을 연결시킬수 있다는점을 깨닫게 되어, 더더욱 리액트를 열심히 해야겠다는 생각이 들었던 챕터였다.


댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
TAG
more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함