끄적끄적 코딩
article thumbnail
Published 2023. 6. 20. 15:51
[React] 이벤트 핸들링 React

4.1 리액트의 이벤트 시스템

HTML에서 이벤트를 작성하던 것과 비슷합니다. 아래는 HTML에서의 이벤트 입니다.

<button onclick="실행 코드">예시 버튼</button>

리액트에서의 이벤트는 어떨까요?

이벤트를 사용할 때 주의 사항

1. 이벤트 이름은 카멜 표기법으로 작성

- React: onClick
- HTML: onclick

2. 이벤트에 실행할 자바스크립트 코드를 전달하는 것이 아니라, 함수 형태의 값을 전달

- React: 함수 형태의 객체
- HTML: 큰따옴표 안에 실행할 자바스크립트 코드

3. DOM 요소에만 이벤트를 설정

div, button,,, 등의 DOM 요소에는 이벤트를 설정 할 수 있지만, 직접 생성한 컴포넌트는 불가능 합니다.

<MyComponent onClick={doSomething}/>

위와 같이 직접 만든 MyComponent에 onClick 이벤트를 설정한다고 해서 해당 컴포넌트를 클릭 시 doSomething 함수가 실행되는 것이 아닌, onClick이라는 props에 doSomething 함수를 담아 MyComponent에 전달해 줄 뿐입니다.
=> 따라서 컴포넌트에 자체적으로 이벤트 설정은 불가능합니다.

이벤트 종류

- Clipboard
- Composition
- Keyboard
- Focus
- Form
- Mouse
- Selection
- Touch
- UI
- Wheel
- Media
- Image
- Animation
- Transition
* 더 많은 이벤트는 리액트 메뉴얼을 참고합니다.

onChange 이벤트 설정

아래와 같이 컴포넌트를 만들고 개발자 도구를 열어 input 태그에 입력해보세요.

import React, { Component } from "react";

class EventPractice extends Component {
  render() {
    return (
      <div>
        <input
          type="text"
          name="message"
          placeholder="아무거나 입력해 보세요"
          onChange={(e) => {
            console.log(e);
          }}
        />
      </div>
    );
  }
}

export default EventPractice;
onChange={(e) => {
  console.log(e);
}}

위 코드로 인하여 아래와 같이 e 객체가 출력되는 것을 확인할 수 있습니다.

e 객체는 SyntheticEvent로 웹 브라우저의 네이티브 이벤트를 감싸는 객체입니다.

웹 브라우저의 네이티브 이벤트란 브라우저에서 기본적으로 제공하는 이벤트를 말합니다.

그렇다면 Chrome, Edge, Safari,, 등등 다양한 브라우저에서 이벤트 핸들링 방식이 동일할까요?
당연히 차이가 있고 브라우저 별로 각각 핸들링 처리를 해준다면 성능을 저하시킬 것 입니다.

이를 위해 리액트에서 SyntheticEvent 객체를 제공합니다.

SyntheticEvent는 브라우저 네이티브 이벤트를 추상화하여 브라우저 호환성을 높이고 브라우저 마다 다른 이벤트 이름이나 속성 등을 일관된 방식으로 처리할 수 있도록 도와줍니다.

또한 SyntheticEvent 다음과 같은 기술을 통해 이벤트 처리의 성능을 최적화하고 효율적인 코드를 제공합니다.

이벤트 풀링(Event Pooling)

SyntheticEvent 객체는 이벤트가 처리되는 동안에만 유효하며, 완료되면 메모리에서 해제됩니다. 이 때 매번 이벤트 객체를 새로 생성하고 해제하는 것은 성능상의 문제가 있습니다.

특히 연속적으로 이벤트가 발생하는 상황, 예를 들어 빠르게 클릭 이벤트를 발생할 때는 매번 생성한다면 비용이 커질 것 입니다.

이럴 때를 위해 이벤트 풀링을 사용합니다. 이벤트 풀에 생성된 이벤트 객체, SyntheticEvent 객체를 저장하여 이를 재활용 합니다. 그리고 이벤트 사용이 더 이상 필요없다고 브라우저에서 판단되면 삭제합니다.

즉 이벤트 풀링을 사용한다고 해서 이벤트 객체의 수명이 증가하는 것이 아니라 연속적인 사용에서 불필요한 이벤트 생성을 막는 것 입니다.

하지만 리엑트 17부터 다음과 같은 이유로 이벤트 풀링 메커니즘이 삭제되었다고 합니다.

1. 자동적으로 사라지는 메모리에 의한 혼란
2. 높은 이벤트 실행 수 동안은 할당을 저장하지만 해제, 파기, 및 재사용에서 발생하는 오버헤드

위의 이유와 최신 브라우저에서 성능 향상에 도움이 되지 않는다는 이유로 더이상 사용되지 않습니다.
그래서 **persist()와 같은 메서드를 사용하지 않아도 이제는 비동기적인 방식으로 접근 가능합니다.**

이벤트 위임(Event Delegation)

이벤트 위임은 이벤트를 처리하기 위해 각각의 요소에 이벤트 리스너를 등록하는 것이 아니라, 상위 요소에 하나의 이벤트 리스너를 등록하여 처리하는 기술입니다.

예시로 다음의 코드를 봅시다.

function handleClick(event) {
  console.log(event.target.innerText);
}

function App() {
  return (
    <ul id="myList" onClick={handleClick}>
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </ul>
  );
}

위와 같이 하면 각각 li 태그마다 핸들링을 해줄 필요 없이 상위 요소에 핸들링을 해주어 event.target을 통해 클릭된 요소를 확인 할 수 있습니다.

이를 통해 코드의 복잡성을 줄일 수 있습니다.

이벤트 객체의 라이프 사이클

생성된 e객체는 기본적으로 버블링(bubbling)을 통해 중첩된 구조에서 하위 요소에서 상위 요소 방향으로 전파됩니다. 가장 상위요소까지 전파가 되면 이벤트가 종료되고 생성된 e객체는 자동으로 메모리에서 해제됩니다.

반대로 상위요소에서 하위요소로 전파되는 캡처링(capturing) 방식도 있습니다. 이때 주의해야 할 점은 캡처링 방식의 방향은 본인 요소에서 하위요소가 아닌 본인 요소의 가장 상위요소에서 본인 요소까지의 전파를 말하는 것입니다.

addEventListener() 메소드의 세 번째 인자로 capture 프로퍼티를 true로 설정하여 캡처링 방식까지 사용한다면 캡처링 방식으로 상위요소 → 하위요소로 전파 후 다시 버블링 방식으로 하위요소 → 상위요소로 전파하는 단계를 거친다음 e객체의 해제로 진행됩니다.

결론적으로 이벤트 객체의 라이프 사이클은 DOM요소를 클릭하여 이벤트 발생 후 e객체가 생성되고 나서부터 전파 방식에따라 모든 전파가 이루어지고 난 후까지 입니다.


SyntheticEvent는 이벤트가 끝나면 정보가 초기화 됩니다.

만약 비동기적으로 이벤트 객체를 참조할 일이 있다면 e.persist() 함수를 사용하거나 e.target.value를 참조하여 input값을 state에 기록해 두는 등의 처리를 해줘야 합니다.

e.persist() 함수

class MyComponent extends React.Component {
//   handleClick = (e) => {
//     setTimeout(() => {
//       console.log(e.type); // null이 출력됨
//     }, 1000);
//   };

  handleClick = (e) => {
    e.persist(); // SyntheticEvent 객체를 유지하도록 지시

    setTimeout(() => {
      console.log(e.type); // "click"이 출력
    }, 1000);
  };

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}

 

state에 input 값 담기

import React, { Component } from "react";

class EventPractice extends Component {
  state = {
    message: "",
  };

  render() {
    return (
      <div>
        <input
          type="text"
          name="message"
          placeholder="아무거나 입력해 보세요"
          value={this.state.message}        // 0, 3, 6: message값 출력
          onChange={(e) => {                // 1: input태그에 입력
            this.setState({
              message: e.target.value,      // 2: 입력 값 state에 저장
            });
          }}
        />
        <button
          onClick={() => {                  // 4: 버튼 클릭
            this.setState({
              message: "",                  // 5: state값 ''으로 수정
            });
          }}
        >
          삭제
        </button>
      </div>
    );
  }
}

export default EventPractice;

위 과정을 그림으로 표현하면 다음과 같습니다.


이벤트 파라미터 안에는 화살표 함수만 가능할까?

이벤트 핸들러로 전달되는 함수는 우선 일반 객체가 아닌 함수형 객체여야만 합니다.

하지만 화살표 함수를 활용한다면 그 안에서 바로 함수를 작성할 수도 있습니다. 이를 인라인 함수, 또는 익명 함수라 합니다.

<button onClick={() => {
	// 이벤트 처리 로직
}}>Click me</button>

위 같은 경우 간결성이 더해지지만 남용 시 코드 가독성이 떨어질 수 있고 동일한 함수가 여러번 호출되는 경우 함수가 매번 새로 생성되므로 성능상의 문제가 발생할 수 있으므로 적절히 활용하는 것이 중요합니다.

그렇다면 인라인 함수 말고 함수 선언문도 활용할 수 있을까? 가능합니다.

<button onClick={function handleClick(event) {
	// 이벤트 처리 로직
}}>Click me</button>

주의할 점은 함수 선언문은 인라인 함수와 다르게 함수가 선언된 시점에서 전역 스코프에 등록되어 이벤트 핸들러가 렌더링 될 때마다 함수가 다시 생성되어 성능 문제를 일으킬 수 있습니다.

두 방식의 정확한 차이는 다음과 같습니다.

함수 선언문

function 함수 선언문의 등록 방식에 의해 렌더링 될 때마다 매번 새로운 함수를 생성하고 전역 스코프에 등록합니다.

인라인 함수

렌더링이 아닌 이벤트 발생할 때마다 함수를 생성하고 전역 스코프에 등록합니다.

가장 좋은 방법은 외부에서 함수를 선언하는 방법입니다.

그 이유는 외부에서 선언된 함수는 렌더링 될 때마다 새로운 함수가 생성되지 않고, 컴포넌트가 재렌더링되어도 함수는 변경되지 않습니다.

이는 함수가 컴포넌트 내부에 선언되어 전역 스코프에 등록되는 것과 달리, 해당 함수가 컴포넌트 외부에 선언되어 전역 스코프에 등록되기 때문입니다.

이를 통해 불필요한 함수 생성을 피할 수 있고, 메모리 사용량을 줄일 수 있습니다.


임의 메서드 만들기

지금까지 이벤트를 처리할 때 렌더링을 하는 동시에 함수를 만들어서 전달해 주었습니다.

이 방법 대신 함수를 미리 준비하여, 즉 외부에 있는 함수를 호출하는 방식도 있습니다. 성능상으로 차이가 거의 없지만, 가독성은 훨씬 높습니다. 때문에 해당 방식을 권장합니다.

기본 방식

import React, { Component } from "react";

class EventPractice extends Component {
  state = {
    message: "",
  };

  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.setState({
      message: e.target.value,
    });
  }

  render() {
    return (
      <div>
        <input
          type="text"
          name="message"
          placeholder="아무거나 입력해 보세요"
          value={this.state.message}
          onChange={this.handleChange}
        />
      </div>
    );
  }
}

export default EventPractice;

함수가 호출될 때 this는 호출부에 따라 결정되므로 클래스의 임의 메서드가 특정 HTML 요소의 이벤트로 등록되는 과정에서 메서드와 this의 관계가 끊어져 버립니다.

→ 이벤트 핸들러는 일반적으로 전역에서 호출되기 때문에 메서드 내부의 this 값은 전역 객체를 가리키게 됩니다.

때문에 임의 메서드가 이벤트로 등록되어도 this가 컴포넌트 자신으로 가리키기 위해서는 메서드를 this와 바인딩하는 작업이 필요합니다. 만약 바인딩하지 않는다면 this가 undefined를 가리킵니다.

위 코드에선 constructor 함수에서 바인딩하는 작업이 이루어집니다.

바인딩(Binding)이란, 함수의 this와 객체를 연결하는 과정을 말합니다. 함수가 호출될 때 this가 어떤 객체를 가리키도록 바인딩하는 것을 함수의 this 바인딩이라고 합니다.


Property Initializer Syntax를 사용한 메서드 작성

위 방법이 정석이긴 하지만 새 메서드를 만들 때마다 constructor도 수정해야 하기 때문에 불편합니다.

이것을 간단하게 하기 위해선 바벨의 transform-class-properties 문법을 사용하여 화살표 함수 형태로 메서드를 정의하면 됩니다.

import React, { Component } from "react";

class EventPractice extends Component {
  state = {
    message: "",
  };

  handleChange = (e) => {
    this.setState({
      message: e.target.value,
    });
  };

  render() {
    return (
      <div>
        <input
          type="text"
          name="message"
          placeholder="아무거나 입력해 보세요"
          value={this.state.message}
          onChange={this.handleChange}
        />
      </div>
    );
  }
}

export default EventPractice;

화살표 함수 내부에서 this 키워드를 사용하면 함수가 정의된 시점에서 상위 스코프의 this 값을 참조하기 때문에 자동으로 해당 클래스의 인스턴스를 가리키도록 바인딩이 이루어집니다.

때문에 화살표 함수를 사용하면 별도의 바인딩 과정도 생략할 수 있고 가독성 또한 높일 수 있습니다.


input 여러 개 다루기

input 태그가 여러 개일 경우 각각 메서드를 만들어줘도 되지만 더 쉽고 가독성을 챙기면서 해결할 수 있습니다.

바로 e.target.name을 활용합니다. e.target.name은 input 태그의 name을 가리킵니다.

import React, { Component } from "react";

class EventPractice extends Component {
  state = {
    username: "",
    message: "",
  };

  handleChange = (e) => {
    this.setState({
      [e.target.name]: e.target.value,
    });
  };

  render() {
    return (
      <div>
        <input
          type="text"
          name="username"
          placeholder="사용자 명"
          value={this.state.username}
          onChange={this.handleChange}
        />
        <input
          type="text"
          name="message"
          placeholder="아무거나 입력해 보세요"
          value={this.state.message}
          onChange={this.handleChange}
        />
      </div>
    );
  }
}

export default EventPractice;

위 코드에서 눈여겨보아야 할 점은 다음 코드입니다.

handleChange = (e) => {
  this.setState({
    [e.target.name]: e.target.value,
  });
};

우선 name을 통해 태그를 특정하여 하나의 메서드로 여러개의 태그를 관리할 수 있다는 점이 핵심이고,

두번째로 Javascript는 객체 안에서 key를(변수를) [ ]로 감싸면 해당 변수 안에 있는 값이 key 값으로 적용 된다는 점입니다.


oneKeyPress 이벤트 핸들링

이번엔 키를 눌렀을 때 발생하는 KeyPress 이벤트를 처리하는 방법을 알아보겠습니다.

다음은 input 태그에서 입력을 하고 포커싱되어 있을 때 Enter을 눌렀을 시 동작하는 코드 입니다.

import React, { Component } from "react";

class EventPractice extends Component {
  state = {
    message: "",
  };

  handleChange = (e) => {
    this.setState({
      message: e.target.value,
    });
  };

  handleKeyPress = (e) => {
    if (e.key === "Enter") {
      this.setState({
        message: "",
      });
    }
  };

  render() {
    return (
      <div>
        <input
          type="text"
          name="message"
          placeholder="아무거나 입력해 보세요"
          value={this.state.message}
          onChange={this.handleChange}
          onKeyPress={this.handleKeyPress}
        />
      </div>
    );
  }
}

export default EventPractice;

서적에서는 onKeyPress를 알려주지만 onKeyPress는 브라우저 별 적용되는 부분이 통일되지 않고 최신 브라우저에서 onKeyDown 혹은 onKeyUp 이벤트로 대체되었기 때문에 권장하지 않는다고 합니다.

코드는 정상적으로 동작하는 것으로 보이지만 어떤 브라우저에서 문제가 생길지 모르기 때문에 지양하는 것이 좋습니다.

위 코드에서는 onKeyDown으로 대체할 수 있습니다.


함수 컴포넌트로 구현해 보기

다음은 지금까지 했던 실습은 함수 컴포넌트로 구현한 코드입니다.

import React, { useState } from "react";

const EventPractice = () => {
  const [form, setForm] = useState({
    username: "",
    message: "",
  });

  const { username, message } = form;

  const onChange = (e) => {
    const nextForm = {
      ...form,
      [e.target.name]: e.target.value,
    };

    setForm(nextForm);
  };

  const onKeyDown = (e) => {
    if (e.key === "Enter") {
      setForm({
        username: "",
        message: "",
      });
    }
  };

  return (
    <div>
      <input
        type="text"
        name="username"
        placeholder="사용자 명"
        value={username}
        onChange={onChange}
      />
      <input
        type="text"
        name="message"
        placeholder="아무거나 입력해 보세요"
        value={message}
        onChange={onChange}
        onKeyDown={onKeyDown}
      />
    </div>
  );
};

export default EventPractice;


정리

리액트에서 이벤트를 다루는 것은 순수 Javascript 또는 jQuery를 사용한 이벤트를 다루는 것과 비슷합니다.

리액트의 장점 중 하나는 Javascript에 익숙하다면 쉽게 활용할 수 있다는 점이니 HTML DOM Event와 Javascript에 대해 잘 알고 있다면 리액트의 컴포넌트 이벤트도 쉽게 다룰 수 있을 것입니다.

지금까지 클래스형 컴포넌트와 함수 컴포넌트 모두 보았는데요. 후에는 함수 컴포넌트에서 유용한 커스텀 Hooks을 더욱 접할 수 있을 것 입니다.

개인적인 의견으로는 비교하면 비교할수록 클래스형 컴포넌트가 왜 뒷전으로 가는지 알게되는 것 같습니다.

특정 상황이 아니고서야 간결성, 가독성, 성능, 상태 변화 처리, 테스트 용이 등등 함수 컴포넌트의 장점이 훨씬 많기 때문에 실제 프로젝트를 할 때는 함수 컴포넌트 사용을 추천합니다.

'React' 카테고리의 다른 글

[React] 컴포넌트 반복  (0) 2023.06.22
[React] ref란  (0) 2023.06.20
[React] 컴포넌트  (0) 2023.06.20
[React] 리액트란  (0) 2023.06.20
[React] 리덕스 미들웨어  (0) 2023.05.30

검색 태그