[React][공식문서] 이벤트 처리하기

Gyuwon Lee·2022년 6월 8일
0
post-thumbnail

1. 이벤트?

웹페이지에서 마우스를 클릭했을 때, 키를 입력했을 때, 특정요소에 포커스가 이동되었을 때 어떤 사건을 발생시키는 것이다.

이벤트의 종류는 아래와 같이 아주 많다:

  • 마우스 이벤트
    • click
    • dbclick
    • mouseover, mouseout, mousedown, mouseup
  • 키 이벤트
    • keydown, keyup
    • keypress
  • 폼 이벤트
    • focus, blur
    • change

이외에도 다양한 이벤트들이 발생할 수 있으며 이 글에 잘 정리되어 있어 참고했다.


📌 이벤트 핸들러

사용자가 위와 같은 이벤트를 발생시켰다면, 이에 대응하는 함수를 실행시켜 이벤트에 반응해야 한다. 이벤트가 발생했을 때 실행되는 함수를 핸들러(handler)라고 한다. 핸들러는 사용자의 행동에 어떻게 반응할지를 자바스크립트 코드로 표현한 것이다.


📌 다양한 핸들러 할당 방식

핸들러는 여러 가지 방법으로 할당할 수 있다.

1. HTML 속성: on<event>

HTML 안의 on<event> 속성에 핸들러를 할당할 수 있다:

<input value="클릭해 주세요." onclick="alert('클릭!')" type="button">

위 코드는 input 태그의 onclick 속성에 click 이벤트의 핸들러가 할당된 경우다.

이렇게 작성할 경우 핸들러의 코드가 길어지면 태그 전체가 길어져 가독성을 떨어뜨리므로, 이를 JS 함수로 분리한 다음 함수를 호출하는 방식이 권장된다.

2. DOM 프로퍼티: on<event>

DOM 프로퍼티 on<event> 를 사용해도 핸들러를 할당할 수 있다.

모든 HTML 태그는 객체다. 이런 모든 객체는 자바스크립트를 통해 접근할 수 있고, 페이지를 조작할 때 이 객체를 사용한다.

브라우저가 웹페이지를 만나면 HTML을 읽어 DOM 객체를 생성하기 때문인데, 이 때 요소 노드(element node)에서 대부분의 표준 HTML 속성(attribute)은 DOM 객체의 프로퍼티(property)가 된다.

따라서, 각 DOM 객체 (요소 노드)는 프로퍼티를 갖는다. 즉 HTML이 아니라 JS 등으로 이를 조작할 때는 HTML 속성이 아닌 DOM 프로퍼티 관점이 되겠다.

<input id="elem" type="button" value="클릭해 주세요.">
<script>
  elem.onclick = function() {
    alert('감사합니다.');
  };
</script>

📌 이벤트 리스너

1. addEventListener

HTML 속성과 DOM 프로퍼티를 이용한 이벤트 핸들러 할당 방식엔 근본적인 문제가 있다. 하나의 이벤트에 복수의 핸들러를 할당할 수 없다는 문제다. 대부분의 이벤트가 한 개당 하나의 프로퍼티에 대응하고 있기 때문이다.

input.onclick = function() { alert(1); }
// ...
input.onclick = function() { alert(2); } // 이전 핸들러를 덮어씀

따라서 이 문제를 해결하고 핸들러를 여러 개 할당할 수 있도록 addEventListenerremoveEventListener 라는 특별한 메서드를 이용하자는 대안이 제시되었다. 이렇게 하면 하나의 요소에서 복수의 이벤트를 처리할 수 있고, 이 때 할당되는 함수들을 핸들러가 아닌 리스너라고 한다.

EventTarget.addEventListener(event, listener, options);
  • event: 이벤트 이름 (ex. "click")
  • listener: 지정한 이벤트를 수신할 리스너. 함수뿐 아니라, EventListener 인터페이스를 구현하는 객체도 올 수 있다.
  • options: 객체 형태로, 아래와 같은 프로퍼티를 갖는다.
    • once: 이 속성의 값이 true면 이벤트가 트리거 될 때 리스너가 자동으로 삭제된다.
    • capture: 어느 단계에서 이벤트를 다뤄야 하는지를 알려주는 속성
    • passive: 이 속성의 값이 true 면 리스너에서 지정한 함수가 preventDefault()를 호출하지 않는다.

📌 이벤트 객체

이벤트를 제대로 다루려면 어떤 일이 일어났는지 상세히 알아야 한다. click 이벤트가 발생했다면 마우스 포인터가 어디에 있는지, keydown 이벤트가 발생했다면 어떤 키가 눌렸는지 등에 대한 상세한 정보가 필요하다.

이벤트가 발생하면 브라우저는 이벤트 객체(event object)라는 것을 만든다. 여기에 이벤트에 관한 상세한 정보를 넣은 다음, 핸들러에 인수 형태로 전달한다. 어떤 방법으로 이벤트 핸들러를 할당하든, 첫 번째 인자는 이벤트 객체다.

<input type="button" value="클릭해 주세요." id="elem">

<script>
  elem.onclick = function(event) {
    // 이벤트 타입과 요소, 클릭 이벤트가 발생한 좌표를 보여줌
    alert(event.type + " 이벤트가 " + event.currentTarget + "에서 발생했습니다.");
    alert("이벤트가 발생한 곳의 좌표는 " + event.clientX + ":" + event.clientY +"입니다.");
  };
</script>
$> click 이벤트가 [object HTMLInputElement]에서 발생했습니다.
$> 이벤트가 발생한 곳의 좌표는 77:18입니다.

2. 리액트 엘리먼트의 이벤트 처리

1) 문법적 차이

  • 리액트의 이벤트는 소문자 대신 캐멀 케이스(camelCase)를 사용한다.
  • JSX를 사용하여 문자열이 아닌 함수로 이벤트 핸들러를 전달한다.
// HTML
<button onclick="activateLasers()">
  Activate Lasers
</button>

// React
<button onClick={activateLasers}>
  Activate Lasers
</button>

2) 브라우저 기본 동작 막기

많은 이벤트들은 발생 즉시 브라우저에 의해 특정 동작을 자동으로 수행한다.

  • 링크를 클릭하면 해당 URL로 이동한다.
  • 폼 전송 버튼을 클릭하면 서버에 폼이 전송된다.
  • 마우스 버튼을 누른 채로 글자 위에서 커서를 움직이면 글자가 선택된다.

그러나, 이런 브라우저 기본 동작 대신 자바스크립트를 사용해 직접 동작을 구현해야 하는 경우가 생길 수 있다.

(1) return false

이벤트 핸들러가 addEventListener가 아닌 on<event> (HTML 속성 또는 DOM 프로퍼티)를 사용해 할당되었다면 false 를 반환하게 해서 기본 동작을 막을 수 있다.

기본적으로, 이벤트 핸들러에서 무언가 값이 반환되면 대개 무시된다. on<event> 를 사용해 할당한 핸들러에서 false 를 반환하는 것은 예외사항으로 처리된다. 이 외의 값들은 return 되어도 무시되는데, true 도 당연히 마찬가지다.

<form onsubmit="console.log('You clicked submit.'); return false">
  <button type="submit">Submit</button>
</form>

위 코드를 예시로 들어 보자. 일반 HTML의 폼에서 타입이 submit 인 버튼을 누르면 데이터가 전송되는 것이 기본 동작이다. 이 경우 HTML 속성 on<event> 를 사용하여 이벤트 핸들러를 전달하고 있고, 따라서 return false 를 사용하여 기본 동작을 막았다. 따라서 submit 버튼을 누르면 콘솔 창에 You clicked submit. 이라는 메세지만 출력될 뿐 기본 동작인 데이터 전송은 일어나지 않는다.

(2) event.preventDefault()

그러나, 리액트에서는 false를 반환해도 기본 동작을 방지할 수 없다. 반드시 preventDefault를 명시적으로 호출해야 한다.

preventDefault() 는 위에서 보았던 event 객체에 구현되어 있는 메소드다. 이 event 객체란 이벤트가 발생했을 때, 브라우저가 이벤트에 관한 상세한 정보를 담아 핸들러에게 인수 형태로 전달하는 값이라고 했다.

따라서 앞서 return false 를 사용하여 기본 동작을 막았던 코드를 리액트에서는 아래와 같이 작성할 수 있다.

function Form() {
  function handleSubmit(e) {
    e.preventDefault();
    console.log('You clicked submit.');
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Submit</button>
    </form>
  );
}
  • 함수 handleSubmit() 의 인자 e 가 바로 이벤트 객체를 의미한다.
    • 공식 문서에는 "여기서 e는 합성 이벤트입니다" 라고 적혀 있는데, e 의 형태가 합성 이벤트라는 것이 아니라 합성 이벤트에 대한 정보를 담은 이벤트 객체 라는 뜻이다.
    • 이는 크로스브라우징 문제를 해결하기 위한 방법으로, 브라우저마다 다른 native 이벤트를 묶어서 처리한다.
    • 즉, 이벤트 핸들러는 모든 브라우저에서 이벤트를 동일하게 처리하기 위한 이벤트 래퍼 SyntheticEvent 객체를 전달받는다. preventDefault()를 포함해서 인터페이스는 브라우저의 고유 이벤트와 같지만, 모든 브라우저에서 동일하게 동작한다.
    • 어렵게 생각할 것 없이, 이 합성 이벤트 객체 역시 W3C 명세를 따라 정의되어 있으므로 순수 자바스크립트에서의 이벤트 객체 사용과 동일하게 사용할 수 있다.

3) 이벤트 핸들링 예시

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // 콜백에서 `this`가 작동하려면 아래와 같이 바인딩 해주어야 합니다.
    this.handleClick = this.handleClick.bind(this);
  }

  // 콜백
  handleClick() {
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}
  • 위 코드는 클래스형 컴포넌트에서의 일반적인 이벤트 핸들링 패턴으로, 이벤트 핸들러를 클래스의 메서드 handleClick() 으로 만들었다.

  • 여기서 JSX 콜백 안에서 this 의 의미에 대해 주의해야 한다.

    • 자바스크립트에서 클래스의 메서드는 기본적으로 바인딩되어 있지 않다. 즉, 메서드와 객체가 이어져있지 않다는 뜻이다. 그래서 객체 메서드를 콜백으로 전달하면 객체에서 분리된 함수가 전달된다.
    • Toggle 이라는 객체 내부의 메소드 handleClick() 이라는 컨텍스트를 잃어버리고, 함수로서의 handleClick 을 호출한 셈이 된다.
    • this 값은 호출 시점에 결정되므로, 컨텍스트를 잃으면 콜백 내부의 this.setState 에서 this 가 가리키는 값이 undefined 가 되어 버린다.
    • 따라서 아래의 onClick={this.handleClick} 과 같이 뒤에 () 를 사용하지 않고 메서드를 참조할 경우, 해당 메서드를 바인딩 해야 한다. 괄호 유무가 중요하다.
// Toggle의 메서드
let expect = Toggle.handleClick();

// Toggle의 메서드라는 컨텍스트를 잃어버림. 함수로써 호출
let actual = Toggle.handleClick;

콜백함수?
콜백 함수는 코드를 통해 명시적으로 호출하는 함수가 아니라, 개발자는 단지 함수를 동록하기만 하고, 어떤 이벤트가 발생했거나 특정 시점에 도달했을 때 시스템에서 호출하는 함수를 말한다.
즉 콜백함수는 콜백함수라는 유니크한 문법적 특징을 가지고 있는 것이 아니라, 호출방식에 의한 구분이다.
위와 같은 이벤트 핸들러 처리가 콜백 함수의 대표적인 사용 예시에 해당한다.

이러한 바인딩 문제를 해결하기 위해서는

  • 위 코드처럼 생성자 안에서 바인딩하거나
  • 클래스 필드 문법을 사용하거나
  • 콜백에 화살표 함수를 사용하는 방법이 있다.

다만 콜백에 화살표 함수를 사용하는 방법은 불필요한 추가 렌더링을 발생시킬 수 있다는 문제점이 있어, 위의 두 방법이 좀 더 권장된다.

클래스 필드 문법은 아직 어떤 원리인지 제대로 이해하지 못해서 이 글에서 설명하기는 어려울 것 같다. 일단은 저 정도의 방법이 있다는 것만 이해하고 마무리한 후 좀 더 공부한 뒤 추후 다시 정리해야겠다.

profile
하루가 모여 역사가 된다

0개의 댓글