[리액트 공식문서 읽기] ADDING INTERACTIVITY - Responding to Events

JaeHong Jeong·2023년 8월 28일
post-thumbnail

Overview

리액트를 사용하면 JSX에 이벤트 핸들러를 추가할 수 있다. 이벤트 핸들러는 클릭, 마우스 호버, 양식 입력 포커싱 등과 같은 상호작용에 대한 응답으로 트리거되는 자체 함수이다.

Adding event handlers

이벤트 핸들러를 추가하려면 먼저 함수를 정의한 다음 이를 적절한 JSX태그에 props로 전달한다. 예를 들어 아직 아무 것도 하지 않은 버튼이 있다.

export default function Button() {
  return (
    <button>
      I don't do anything
    </button>
  );
}

다음 세 단계에 따라 사용자가 클릭할 때 메시지가 표시되도록 할 수 있다.

  1. Button 컴포넌트 내에서 handleClick 함수를 선언한다.
  2. 해당 함수 내부에 로직을 구현한다. (alert 을 사용하여 메시지 표시)
  3. <button> JSX에 onClick={handleClick} 을 추가한다.
export default function Button() {
  function handleClick() {
    alert('You clicked me!');
  }

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

handleClick 함수를 정의한 다음 이를 <button> prop으로 전달했다. handleClick은 이벤트 핸들러이다. 이벤트 핸들러의 기능 :

  • 일반적으로 컴포넌트 내부에 정의된다.
  • 이름은 handle 로 시작하고 그 뒤에 이벤트 이름이 온다.

관례적으로 이벤트 핸들러의 이름은 handle 뒤에 이벤트 이름이 오는 형태로 지정하는 것이 일반적이다. onClick={handleClick}, onMouseEnter={handleMouseEnter} 등으로도 자주 볼 수 있다.

JSX에서 이벤트 핸들러를 인라인으로 정의할 수도 있다.

<button onClick={function handleClick() {
  alert('You clicked me!');
}}>

더 간결하게 화살표 함수를 사용할 수도 있다.

<button onClick={() => {
  alert('You clicked me!');
}}>

이러한 스타일은 모두 동일하다. 이라인 이벤트 핸들러는 짧은 함수에 편리하다.

💡 Pitfall

이벤트 핸들러에 전달된 함수는 호출되지 않고 전달되어야 한다.

그 차이는 미묘하다. 첫 번째 예에서는 handleClick 함수가 onClick 이벤트 핸들러로 전달된다. 리액트가 이를 기억하고 사용자가 버튼을 클릭할 때만 함수를 호출하도록 지시한다.

두 번째 예에서, handleClick() 끝에 있는 () 는 클릭 없이 렌더링 중에 즉시 함수를 실행한다. 이는 JSX {} 내부의 JavaScript가 바로 실행되기 때문이다.

코드를 인라인으로 작성하면 동일한 함정이 다른 방식으로 나타난다.

이와 같은 인라인 코드를 전달하면 클릭 시 실행되지 않는다. 컴포넌트가 렌더링 될 때마다 실행된다.

// This alert fires when the component renders, not when clicked!
<button onClick={alert('You clicked me!')}>

이벤트 핸들러를 인라인으로 정의하려면 다음과 같이 익명 함수로 래핑하면 된다.

<button onClick={() => alert('You clicked me!')}>

렌더링할 때마다 내부에서 코드를 실행하는 대신 나중에 호출할 함수를 생성한다.

두 경우 모두 전달하려는 것은 함수이다.

  • <button onClick={handleClick}> 은 handleClick 함수를 전달한다.
  • <button onClick={() => alert('...')}> 는 () => alert('...') 함수를 전달한다.

Reading props in event handlers

이벤트 핸들러는 컴포넌트 내부에 선언되므로 컴포넌트의 props에 엑세스할 수 있다. 클릭하면 message prop과 함께 경고 표시하는 버튼이 있다.

function AlertButton({ message, children }) {
  return (
    <button onClick={() => alert(message)}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div>
      <AlertButton message="Playing!">
        Play Movie
      </AlertButton>
      <AlertButton message="Uploading!">
        Upload Image
      </AlertButton>
    </div>
  );
}

이렇게 하면 두 버튼에 서로 다른 메시지가 표시된다.

Passing event handlers as props

종종 부모 컴포넌트가 자식의 이벤트 핸들러를 지정하기를 원할 것이다. 버튼에서 Button 컴포넌트를 사용하는 위치에 따라 다른 기능을 실행하고 싶을 수 있다. 예를 들어 하나는 영화를 재생하고 다른 하나는 이미지를 업로드하는 것이다.

이렇게 하려면 컴포넌트가 부모로부터 받는 prop을 다음과 같이 이벤트 핸들러로 전달한다.

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

function PlayButton({ movieName }) {
  function handlePlayClick() {
    alert(`Playing ${movieName}!`);
  }

  return (
    <Button onClick={handlePlayClick}>
      Play "{movieName}"
    </Button>
  );
}

function UploadButton() {
  return (
    <Button onClick={() => alert('Uploading!')}>
      Upload Image
    </Button>
  );
}

export default function Toolbar() {
  return (
    <div>
      <PlayButton movieName="Kiki's Delivery Service" />
      <UploadButton />
    </div>
  );
}

Toolbar 컴포넌트는 PlayButtonUploadButton 을 렌더링한다.

  • PlayButtonButton 내부에 대한 onClick prop으로 handlePlayClick 을 전달한다.
  • UploadButtonButton 내부에 대한 onClick prop으로 () => alert('Uploading!') 을 전달한다.

마지막으로 Button 컴포넌트는 onClick 이라는 prop을 허용한다. onClick={onClick} 을 사용하여 해당 prop을 내장 브라우저 <button> 에 직접 전달한다. 이는 리액트가 클릭 시 전달된 함수를 호출하도록 지시한다.

디자인 시스템을 사용하는 경우 버튼과 같은 컴포넌트가 스타일을 포함하지만 동작을 지정하지 않는 것이 일반적이다. 대신 PlayButtonUploadButton 과 같은 컴포넌트는 이벤트 핸들러를 전달한다.

Naming event handler props

<button><div> 와 같은 빌트인 컴포넌트는 onClick 과 같은 브라우저 이벤트 네임만 지원한다. 그러나 자신만의 컴포넌터를 만들 때 원하는 방식으로 이벤트 핸들러 이름을 지정할 수 있다.

관례적으로 이벤트 핸들러 props는 on 으로 시작하고 그 뒤에 대문자가 와야한다.

예를 들어 Button 컴포넌트의 onClick prop은 onSmash 라고 불릴 수 있다.

function Button({ onSmash, children }) {
  return (
    <button onClick={onSmash}>
      {children}
    </button>
  );
}

export default function App() {
  return (
    <div>
      <Button onSmash={() => alert('Playing!')}>
        Play Movie
      </Button>
      <Button onSmash={() => alert('Uploading!')}>
        Upload Image
      </Button>
    </div>
  );
}

이 예에서, <button onClick={onSmash}> 는 브라우저 <button> 에 여전히 onClick 이라는 prop이 필요하지만 사용자 정의 Button 컴포넌트에서 수신하는 prop 이름은 사용자에게 달려있음을 보여준다.

컴포넌트가 여러 상호 작용을 지원하는 경우 앱 별 개념에 대해 이벤트 핸들러 prop의 이름을 지정할 수 있다. 예를 들어, Toolbar 컴포넌트는 onPlayMovieonUploadImage 이벤트 핸들러를 수신한다.

export default function App() {
  return (
    <Toolbar
      onPlayMovie={() => alert('Playing!')}
      onUploadImage={() => alert('Uploading!')}
    />
  );
}

function Toolbar({ onPlayMovie, onUploadImage }) {
  return (
    <div>
      <Button onClick={onPlayMovie}>
        Play Movie
      </Button>
      <Button onClick={onUploadImage}>
        Upload Image
      </Button>
    </div>
  );
}

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

App 컴포넌트는 ToolbaronPlayMovie 이나 onUploadImage 에서 수행할 작업을 알 필요가 없다. Toolbar 의 구현 세부사항이다. 여기서 Toolbar 는 이를 Button 에 대한 onClick 핸들러로 전달하지만 나중에 키보드 단축키로 트리거할 수도 있다. onPlayMovie 와 같은 앱 별 상호 작용 후에 이름을 지정하면 나중에 사용 방법을 유연하게 변경할 수 있다.

💡 Note

이벤트 핸들러에 적절한 HTML 태그를 사용하는 지 확인해라. 예를 들어 클릭을 처리하려면 <div onClick={handleClick}> 대신 [<button onClick={handleClick}>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button) 을 사용 해라. 실제 브라우저 <button> 을 사용하면 키보드 탐색과 같은 내장 브라우저 동작이 가능해진다. 버튼의 기본 브라우저 스타일이 마음에 들지 않고 링크나 다른 UI 요소처럼 보이도록 만들고 싶다면 CSS를 사용하면 된다. Learn more about writing accessible markup.

Event propagation

이벤트 핸들러는 컴포넌트에 있을 수 있는 모든 하위 항목에서도 이벤트를 포착한다. 이벤트 트리 위로 “버블” 또는 “전파”된다고 말한다. 이벤트가 발생한 곳에서 시작하여 트리 위로 올라간다.

<div> 에는 두 개의 버튼이 있다. <div> 와 각 버튼에는 모두 자체 onClick 핸들러가 있다. 버튼을 클릭하면 어떤 핸들러가 실행될 것이라고 생각하나?

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('You clicked on the toolbar!');
    }}>
      <button onClick={() => alert('Playing!')}>
        Play Movie
      </button>
      <button onClick={() => alert('Uploading!')}>
        Upload Image
      </button>
    </div>
  );
}

두 버튼 중 하나를 클릭하면 해당 버튼의 onClick 이 먼저 실행되고 그 다음 상위 <div>onClick 이 실행된다. 그러면 두 개의 메시지가 나타난다. 툴바 자체를 클릭하면 상위 <div>onClick 만 실행된다.

💡 Pitfall

onScroll 을 제외한 모든 이벤트는 리액트에서 전파된다. onScroll 은 연결된 JSX 태그에서만 작동한다.

Stopping propagation

이벤트 핸들러는 이벤트 객체를 유일한 인수로 받는다. 관례적으로 이는 “이벤트”를 의미하는 e 라고 불린다. 이 개체를 사용하여 이벤트에 대한 정보를 읽을 수 있다.

해당 이벤트 개체를 사용하면 전파를 중지할 수도 있다. 이벤트가 상위 컴포넌트에 도달하는 것을 방지하려면 다음 Button 컴포넌트처럼 e.stopPropagation() 를 호출해야 한다.

function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('You clicked on the toolbar!');
    }}>
      <Button onClick={() => alert('Playing!')}>
        Play Movie
      </Button>
      <Button onClick={() => alert('Uploading!')}>
        Upload Image
      </Button>
    </div>
  );
}

버튼을 클릭하면:

  1. 리액트는  <button> 에 전달된 onClick 핸들러를 호출한다..
  2. Button 에 정의된 핸들러는 다음을 수행한다:
    • e.stopPropagation() 을 호출하여 이벤트가 더 이상 버블링되는 것을 방지한다.
    • Toolbar 컴포넌트에서 전달된 prop인 onClick 함수를 호출한다.
  3.  Toolbar 컴포넌트에 정의된 이 기능은 버튼 자체의 경고를 표시한다.
  4.  전파가 중지되었으므로 상위 <div> 의 onClick 핸들러가 실행되지 않는다.

e.stopPropagation() 의 결과로, 버튼을 클릭하면 이제 두 개의 경고 ( <button> 및 상위 툴바 <div> 에서)가 아닌 단일 경고 (<button> 에서 )만 표시된다. 버튼을 클릭하는 것은 주변 툴바를 클릭하는 것과 동일하지 않으므로 이 UI에서는 전파를 중지하는 것이 좋다.

💡 DEEP DIVE

Capture phase events
드문 경우지만 전파가 중지된 경우에도 하위 요소에 대한 모든 이벤트를 포착해야 할 수도 있다. 예를 들어 전파 논리에 관계없이 모든 클릭을 분석에 기록하려고 할 수 있다. 이벤트 이름 끝에 Capture 를 추가하면 된다.

<div onClickCapture={() => { /* this runs first */ }}>
  <button onClick={e => e.stopPropagation()} />
  <button onClick={e => e.stopPropagation()} />
</div>

각 이벤트는 세 단계로 전파된다.

  1. 아래로 이동하여 모든 onClickCapture 핸들러를 호출한다.
  2. 클릭된 요소의 onClick 핸들러를 실행한다.
  3. 위쪽으로 이동하여 모든 onClick 핸들러를 호출한다.

캡처 이벤트는 라우터나 분석과 같은 코드에 유용하지만 앱 코드에서는 사용하지 않을 것이다.

Passing handlers as alternative to propagation

이 클릭 핸들러가 코드에 실행한 다음 부모가 전달한 onClick prop을 호출하는 방법에 주목해라.

function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>
      {children}
    </button>
  );
}

상위 onClick 이벤트 핸들러를 호출하기 전에 이 핸들러에 더 많은 코드를 추가할 수도 있다. 이 패턴은 전파에 대한 대안을 제공한다. 하위 컴포넌트가 이벤트를 처리할 수 있도록 하는 동시에 상위 컴포넌트가 몇 가지 추가 동작을 지정할 수도 있다. 전파와 달리 자동이 아니다. 하지만 이 패턴의 이점은 일부 이벤트의 결과로 실행되는 전체 코드 체인을 명확하게 따라갈 수 있다는 것이다.

전파에 의존하고 어떤 처리기가 실행되고 왜 실행되는지 추적하기 어려운 경우 대신 이 접근 방식을 시도해봐라.

Preventing default behavior

일부 브라우저 이벤트에는 이와 관련된 기본 동작이 있다. 예를 들어, 내부의 버튼을 클릭할 때 발생하는 <form> 제출 이벤트는 기본적으로 전체 페이지를 다시 로드한다.

export default function Signup() {
  return (
    <form onSubmit={() => alert('Submitting!')}>
      <input />
      <button>Send</button>
    </form>
  );
}

이벤트 객체에서 e.preventDefault() 를 호출하면 이러한 일이 발생하지 않도록 할 수 있다.

export default function Signup() {
  return (
    <form onSubmit={e => {
      e.preventDefault();
      alert('Submitting!');
    }}>
      <input />
      <button>Send</button>
    </form>
  );
}

e.stopPropagation()e.preventDefault()를 혼동하지 마라. 둘 다 유용하지만 관련이 없다.

  • e.stopPropagation()은 위 태그에 연결된 이벤트 핸들러의 실행을 중지한다.
  • e.preventDefault() 는 몇 가지 이벤트에 대한 기본 브라우저 동작을 중지한다.

Can event handlers have side effects?

이벤트 핸들러는 부작용이 발생하기 가장 좋은 것이다.

렌더링 함수와 달리 이벤트 핸들러는 순수할 필요가 없으므로 무언가를 변경하기에 좋은 장소이다. 예를 들어 입 력에 대한 응답으로 입력 값을 변경하거나 버튼 누름에 대한 응답으로 목록을 변경한다. 그러나 일부 정보를 변경하려면 먼저 해당정보를 저장할 방법이 필요하다. 리액트에서는 컴포넌트의 메모리인 상태를 사용하여 이를 수행한다. 다음 페이지에서 이에 대한 모든 내용을 배우게 된다.

Recap

  • <button> 과 같은 요소에 함수를 prop으로 전달하여 이벤트를 처리할 수 있다.
  • 이벤트 핸들러는 호출되지 않고 전달되어야 한다. onClick={handleClick()} 이 아니라 onClick={handleClick} 이다.
  • 이벤트 핸들러 함수를 별도로 정의하거나 인라인으로 정의할 수 있다.
  • 이벤트 핸들러는 컴포넌트 내부에 정의도어 있으므로 props에 액세스할 수 있다.
  • 부모에서 이벤트 핸들러를 선언하고 이를 자식에게 prop으로 전달할 수 있다.
  • 애플리케이션별 이름을 사용하여 고유한 이벤트 핸들러를 정의할 수 있다.
  • 이벤트는 위쪽으로 전파된다. 이를 방지하려면 첫 번째 인수에서 e.stopPropagation() 을 호출해라.
  • 이벤트에는 원치 않는 기본 브라우저 동작이 있을 수 있다. 이를 방지하려면 e.preventDefault() 를 호출해라.
  • 하위 핸들러에서 이벤트 핸들러 prop을 명시적으로 호출하는 것은 전파에 대한 좋은 대안이다.
profile
반갑습니다.

0개의 댓글