Adding Interactivity - Responding to Events

hyocho·2023년 5월 31일
0

React

목록 보기
20/24
post-thumbnail

화면의 일부 내용은 사용자 입력에 따라 업데이트 된다. 예를 들어 이미지 갤러리를 클릭하면 활성 이미지가 변경된다. 리액트에서 시간에 따라 변경되는 데이터를 상태라고 한다. 상태는 어떤 컴포넌트에도 추가할 수 있고 필요시에 업데이트 할 수 있다. 이번 챕터에서는 상호작용을 다루고 상태를 업데이트하며 시간에 따라 다른 결과를 나타내는 방법을 알아볼 것이다.

리액트는 JSX에 이벤트 핸들러를 추가할 수 있게 한다. 이벤트 핸들러는 클릭, 호버, 입력창 포커스와 같은 상호작용에 대한 응답을 트리거되는 함수이다.

배울 것

  • 이벤트 핸들러를 작성하는 다른 방법
  • 부모 컴포넌트에서 이벤트 핸들링 로직을 전달해주는 방법
  • 이벤트 전파 방법 및 이벤트 중지 방법

Adding event handlers

이벤트 핸들러를 추가하기 위해서는 먼저 함수를 정의하고 적절한 JSX에 prop으로 전달 해 주어야 한다. 예를 들어, 여기 아직은 아무 기능도 없는 버튼이 있다.

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

사용자가 클릭했을 때 메세지가 보여지도록 하려면 다음 세 단계를 따르면 된다.

  1. handleClick이라고 하는 함수를 Button컴포넌트 내부에 선언한다.
  2. 함수 내부에 로직을 만든다.
  3. <button>JSX에 onClick={handleClick} 을 추가한다.
export default function Button() {
  function handleClick() {
    alert('You clicked me!');
  }

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

handleClick 함수를 선언하고 prop으로 <button>에 전달한다. handleClick은 이벤트 핸들러이다. 이벤트 핸들러 함수는

  • 대개 컴포넌트 내부에 정의된다.
  • 이름은 handle로 시작하고 뒤에는 이벤트 이름을 붙인다.

이름은 handle로 시작하고 뒤에는 이벤트 이름을 붙이는 것은 컨벤션에 따라 일반적이다. onClick={handleClick} 이나 onMouseEnter={handleMouseEnter}등을 자주 보게 될 것이다.

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

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

또는 더욱 간결하게, 화살표 함수를 쓸 수도 있다.

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

모든 스타일은 동일하다. 인라인 이벤트 핸들러는 짧은 함수 사용할 때 편리하다.

🕳️ pitfall

이벤트 핸들러에 전달된 함수는 호출되는 것이 아니라 반드시 전달되어야 한다.

  • 전달된 함수의 바른 예 <button onClick={handleClick}>
  • 함수를 호출하는 올바르지 않은 예 <button onClick={handleClick()}>

차이점은 미묘하다. 첫 번째 예시는 handleClick함수는 onClick라는 이벤트 핸들러로 전달되었다. 이것은 리액트가 기억하고, 사용자가 버튼을 누를때만 호출하게 된다.

반면에 두 번째 예시는 handleClick()의 마지막에 있는 ()가 렌더링 하는 동안 어떤 클릭도 없는데도 즉시 함수를 실행하게 한다. 이것은 JSX내부의 { 와 } 가 바로 실행하게 하기 때문이다.
인라인으로 코드를 작성할 때 같은 함정이 다른 방식으로 나타난다.

  • 전달된 함수의 바른 예 <button onClick={() => alert('...')}>
  • 함수를 호출하는 올바르지 않은 예 <button onClick={alert('...')}>

인라인 코드를 이렇게 전달하면 클릭에 실행되지 않고 컴포넌트가 렌더될 때마다 실행될 것이다.

<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와 함께 alert되는 버튼이다.

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을 렌더링한다:

  • PlayButtononClick의 prop으로 handlePlayClickButton안에 전달한다.
  • UploadButtononClick의 prop으로 () => alert('Uploading!')Button안에 전달한다.

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

디자인 시스템을 사용한다면 특정한 기능을 하지않고 스타일만 포함된 버튼같은 컴포넌트는 일반적이다. 대신 PlayButtonUploadButton같은 컴포넌트는 이벤트 핸들러를 전달한다.

Naming event handler props

<button>, <div>와 같은 컴포넌트들은 onClick과 같은 브라우저 이벤트 이름을 제공한다. 그러나 컴포넌트를 따로 만든다면 그 이벤트 핸들러 props에는 좋아하는 어떤 방식으로나 이름을 붙일 수 있다.

컨벤션에 따르면 이벤트 핸들러 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 컴포넌트는 PlayMovieonUploadImage이벤트 핸들러를 받는다.

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 컴포넌트는 ToolbaronPlayMovieonUploadImage로 어떤 일을 하는지 알 필요가 없다. 그것은 Toolbar의 세부 구현 사항이다. ToolbaronClick핸들러를 Button에 전달하지만, 나중에 키보드 단축키로 트리거 할 수도 있다. onPlayMovie와 같은 앱 별 상호작용의 이름을 prop의 이름을 정하면 나중에 변경하는 것이 쉬워진다.

Note
이벤트 핸들러에 적절한 HTML 태그를 사용하도록 해라. 클릭을 제어하기 위해 <div onClick={handleClick}>대신 <button onClick={handleClick}>을 사용해라. 브라우저의 <button>을 사용하는 것은 키보드 조작과 같은 내장 브라우저 기능을 가능하게 한다. 버튼의 기본 브라우저 스타일이 마음에 들지 않고 링크나 다른 UI 요소처럼 보이게 하려면 CSS로 가능하다.

Event propagation

이벤트 핸들러는 컴포넌트가 가질 수 있는 모든 자식 컴포넌트의 이벤트도 캡쳐한다. 우리는 어떤 이벤트가 "bubble" 또는 "전파"되어 트리를 타고 올라간다고 말한다. 이벤트가 일어난 곳에서 시작하여 트리 위로 올라간다.

<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이 실행된다. 그래서 두개의 메시지가 나타날 것이다. toolbar를 클릭하면 <div>onClick만 실행될 것이다.

🕳️pitfall

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

Stopping propagation

이벤트 핸들러는 단일 인자로 이벤트 객체를 받는다. 관습적으로 'event'의 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. 리액트는 onClick핸들러가 <button>으로 전달된 것을 호출한다.
2. Button 내부에서 정의된 핸들러는 아래를 따른다.

  • 이벤트 버블링을 막기 위해 e.stopPropagation()을 호출한다.
  • Toolbar 컴포넌트에서 전달된 prop인 onClick 함수를 호출한다.
  1. Toolbar 컴포넌트 내부에서 정의된 함수는 버튼 자신의 알람을 보여준다.
  2. 이벤트 전파가 막혔기 때문에 <div>onClick 핸들러는 실행하지 않을 것이다.

e.stopPropagation()의 결과로 버튼을 누르면 <button>Toolbar의 알람이 모두 뜨는 것이 아니라 <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?

물론이다. 이벤트 핸들러는 side effects에 가장 적합한 장소이다.

렌더링 함수와 달리 이벤트 핸들러는 순수할 필요가 없어 어떤 것이 변화하기에 가장 좋은 장소이다. 예를 들어, 입력에 따라 인풋창의 값을 변화하거나 버튼 누르는 것에 따라 목록을 변화시키는 것이 있다. 하지만 몇 정보를 바꾸기 위해서는 먼저 정보를 저장할 수 있는 방법이 필요하다. 리액트에서는 컴포넌트의 메모리인 상태를 사용하여 이 작업을 수행한다. 다음 페이지에서 그것에 대한 모든 것을 배울 것이다.

RECAP

  • button과 같은 요소에 함수를 prop으로 전달하여 이벤트를 제어할 수 있다.
  • 모든 핸들러는 전달될 수 있으며 호출해서는 안된다. onClick={handleClick()} 이 아닌 onClick={handleClick} 이다.
  • 이벤트 핸들러 함수는 따로 혹은 인라인 스타일로 정의될 수 있다.
  • 이벤트 핸들러는 컴포넌트 안에 정의되어 props에 접근할 수 있다.
  • 부모 컴포넌트 내에 정의하고 자식에게 props로 전달할 수 있다.
  • 애플리케이션별 이름을 사용하여 고유한 이벤트 핸들러 props를 정의할 수 있다.
  • 이벤트 전파는 위로 올라간다. 막고 싶다면 첫 번째 인자로 e.stopPropagation()를 호출하라.
  • 이벤트는 원하지 않는 기본 브라우저 동작을 실행할 수도 있다. 막기 위해서는 e.preventDefault()을 호출하라.
  • 자식 핸들러에서 이벤트 핸들러 prop을 명시적으로 호출하는 것이 전파의 좋은 대안이다.
profile
기록하는 습관을 기르고 있습니다.

0개의 댓글