Responding to Events

김동현·2026년 3월 15일

이벤트에 응답하기 (Responding to Events)

React를 사용하면 JSX에 이벤트 핸들러(event handlers)를 추가할 수 있어요. 이벤트 핸들러라는 건, 사용자가 버튼을 클릭하거나, 마우스를 올리거나(hover), 입력창에 포커스를 맞추는 등의 다양한 상호작용(interaction)을 했을 때 그에 반응하여 실행되도록 우리가 직접 만드는 함수를 말한답니다. 자, 그럼 어떻게 이벤트를 다루는지 함께 알아볼까요?

이 단원에서 배울 내용:

  • 이벤트 핸들러를 작성하는 다양한 방법
  • 부모 컴포넌트에서 이벤트 처리 로직을 전달하는 방법
  • 이벤트가 어떻게 전파(propagate)되는지, 그리고 그 전파를 어떻게 멈출 수 있는지

이벤트 핸들러 추가하기

이벤트 핸들러를 추가하려면 먼저 함수를 정의한 다음, 그 함수를 적절한 JSX 태그에 prop으로 전달(pass it as a prop)해야 해요.

예를 들어볼게요. 아래에는 아직 아무 동작도 하지 않는 깡통 버튼이 하나 있습니다.

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>
  );
}
button { margin-right: 10px; }

자, 방금 여러분은 handleClick 함수를 정의하고, 그걸 <button>prop으로 전달했어요! 여기서 handleClick이 바로 이벤트 핸들러랍니다.

👨‍🏫 팁: 이벤트 핸들러 함수들은 보통 다음과 같은 특징을 가져요.

  • 주로 컴포넌트 내부에서 정의됩니다.
  • 이름은 handle로 시작하고, 그 뒤에 이벤트의 이름이 붙는 형태를 띱니다.

관례적으로 이벤트 핸들러 이름은 handle 뒤에 이벤트 이름을 붙여서 짓는 것이 아주 일반적이에요. 실무에서도 onClick={handleClick}, onMouseEnter={handleMouseEnter} 같은 형태를 아주 자주 보시게 될 거예요. 코드를 읽기 쉽게 만들어주는 좋은 습관이죠!

이벤트 핸들러를 JSX 안에 인라인(inline)으로 바로 정의할 수도 있어요.

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

조금 더 깔끔하게 작성하고 싶다면 화살표 함수(arrow function)를 사용하면 됩니다.

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

위의 방식들은 모두 똑같이 동작해요. 인라인 이벤트 핸들러는 함수 안의 내용이 아주 짧을 때 무척 편리하답니다.

⚠️ 주의하세요! (Pitfall)
이벤트 핸들러에 함수를 전달할 때는 함수 그 자체를 전달해야지, 함수를 호출해서는 안 됩니다! 초보자분들이 정말 많이 하는 실수 중 하나예요.

함수 전달하기 (올바른 방법)함수 호출하기 (잘못된 방법)
<button onClick={handleClick}><button onClick={handleClick()}>

차이점이 아주 미묘해 보이죠? 첫 번째 예시에서는 handleClick 함수 자체가 onClick 이벤트 핸들러로 전달됩니다. 이건 React에게 "이 함수를 기억해뒀다가, 사용자가 버튼을 클릭할 때만 실행해줘!"라고 말하는 거예요.

반면 두 번째 예시에서는 handleClick() 끝에 붙은 괄호 () 때문에 렌더링 과정에서 함수가 즉시 호출(실행)되어 버려요. 클릭도 안 했는데 말이죠! 렌더링(rendering)이 일어날 때, JSX {} 안의 JavaScript 코드는 즉시 실행되기 때문이에요.

코드를 인라인으로 작성할 때도 모양은 다르지만 같은 함정에 빠질 수 있어요.

함수 전달하기 (올바른 방법)함수 호출하기 (잘못된 방법)
<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('...') 라는 새로운 함수를 만들어 전달합니다.

화살표 함수에 대해 더 알아보기

이벤트 핸들러에서 props 읽기

이벤트 핸들러는 컴포넌트 내부에 선언되기 때문에, 컴포넌트가 가지고 있는 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>
  );
}
button { margin-right: 10px; }

이렇게 하면 두 버튼이 똑같은 AlertButton 컴포넌트임에도 서로 다른 메시지를 보여줄 수 있어요. 전달되는 메시지를 한번 바꿔보세요!

이벤트 핸들러를 props로 전달하기

개발을 하다 보면, 부모 컴포넌트에서 자식 컴포넌트의 이벤트 핸들러를 지정해주고 싶을 때가 아주 많습니다.

👨‍🏫 부연설명: 예를 들어, 우리가 버튼 모양을 예쁘게 하나 만들어 놨다고 쳐볼게요. 이 버튼을 어떤 곳에서는 '영화 재생'용으로 쓰고, 어떤 곳에서는 '이미지 업로드'용으로 쓰고 싶을 거예요. 버튼의 생김새는 똑같지만, 클릭했을 때의 동작은 버튼을 사용하는 부모 컴포넌트가 결정하도록 위임하는 거죠. 이렇게 하면 컴포넌트의 재사용성이 훌륭해진답니다.

이를 위해 부모가 자식 컴포넌트에게 함수 자체를 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>
  );
}
button { margin-right: 10px; }

위 코드의 흐름을 살펴볼까요? Toolbar 컴포넌트는 PlayButtonUploadButton을 화면에 그립니다.

  • PlayButton은 내부의 Button에게 handlePlayClick 함수를 onClick prop으로 넘겨줍니다.
  • UploadButton은 내부의 Button에게 () => alert('Uploading!') 함수를 onClick prop으로 넘겨줍니다.

최종적으로 제일 아래에 있는 여러분의 커스텀 Button 컴포넌트는 onClick이라는 이름의 prop을 받아서, 브라우저에 내장된 실제 <button> 태그에 onClick={onClick}으로 그대로 전달해요. 이것이 React에게 "버튼이 클릭되면 이 함수를 실행해!"라고 알려주는 역할을 합니다.

실제로 디자인 시스템(design system)을 사용하거나 구축할 때, 버튼 같은 기본 컴포넌트들은 스타일링만 포함하고 동작(behavior)은 포함하지 않는 경우가 흔해요. 대신 PlayButton이나 UploadButton 같은 구체적인 컴포넌트들이 위에서부터 이벤트 핸들러를 아래로 전달해주는 방식을 사용한답니다.

이벤트 핸들러 props 이름 짓기

<button>이나 <div> 같은 HTML 내장 요소들은 onClick처럼 정해진 브라우저 이벤트 이름만 지원해요. 하지만 여러분이 직접 만드는 커스텀 컴포넌트라면, 이벤트 핸들러 prop의 이름을 여러분 마음대로 자유롭게 지을 수 있어요!

관례적으로, 이벤트 핸들러 prop의 이름은 on으로 시작하고 그 뒤에 대문자가 오는 카멜 케이스(CamelCase) 형식을 사용합니다.

예를 들어볼게요. 우리가 만든 Button 컴포넌트의 prop 이름을 onClick이 아니라 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 { margin-right: 10px; }

이 예시를 보면, 진짜 브라우저 요소인 <button>(소문자)은 여전히 onClick이라는 이름의 prop이 필요하지만, 여러분의 커스텀 컴포넌트인 Button(대문자)이 받는 prop의 이름은 전적으로 여러분의 자유라는 걸 알 수 있죠.

컴포넌트가 여러 종류의 상호작용을 지원한다면, 앱의 비즈니스 로직에 맞는 이름으로 이벤트 핸들러 props를 지어주는 것도 좋은 방법이에요. 예를 들어, 아래의 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>
  );
}
button { margin-right: 10px; }

잘 보세요! 최상위 컴포넌트인 AppToolbaronPlayMovieonUploadImage를 가지고 구체적으로 무엇을 할지 알 필요가 전혀 없습니다. 그건 Toolbar 컴포넌트가 내부적으로 알아서 처리할 구현의 세부 사항일 뿐이에요.

현재 Toolbar는 그 핸들러들을 Button들의 onClick으로 전달하고 있지만, 나중에는 단축키를 눌렀을 때 실행되도록 변경할 수도 있겠죠? 이렇게 onPlayMovie처럼 앱의 동작에 맞는 의미 있는 이름을 지어주면, 나중에 컴포넌트의 사용 방식을 유연하게 변경할 수 있다는 큰 장점이 있어요.

참고사항:

이벤트 핸들러를 추가할 때는 의미에 맞는 적절한 HTML 태그를 사용해야 합니다. 예를 들어, 클릭 이벤트를 처리할 때는 <div onClick={handleClick}> 보다는 <button onClick={handleClick}>을 사용하는 것이 올바릅니다.

실제 브라우저의 <button> 태그를 사용하면 키보드 네비게이션(Tab 키로 이동 등)과 같은 브라우저 내장 기능들을 기본적으로 지원받을 수 있기 때문이죠. 만약 기본 버튼 스타일이 마음에 들지 않아 링크나 다른 UI 모양으로 바꾸고 싶다면, 태그를 바꾸는 대신 CSS를 사용해서 스타일을 변경하세요. 웹 접근성에 맞는 마크업 작성법에 대해 더 알아보세요.

이벤트 전파 (Event propagation)

이벤트 핸들러는 컴포넌트가 가지고 있는 자식 요소에서 발생한 이벤트도 모두 감지할 수 있어요. 우리는 이벤트가 트리(tree) 구조를 따라 위로 "버블링(bubbles)"되거나 "전파(propagates)"된다고 표현합니다. 즉, 이벤트가 발생한 정확한 지점에서 시작해서, 부모 요소를 타고 쭉쭉 올라가는 형태예요.

아래 코드의 <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>
  );
}
.Toolbar {
  background: #aaa;
  padding: 5px;
}
button { margin: 5px; }

두 버튼 중 하나를 클릭하면, 먼저 그 버튼의 onClick이 실행되고, 그 다음으로 부모 요소인 <div>onClick이 실행됩니다. 결과적으로 메시지 창이 두 번 연속해서 뜨게 되죠! 만약 툴바(회색 배경 부분) 자체를 클릭한다면, 부모 <div>onClick 하나만 실행됩니다.

⚠️ 주의하세요! (Pitfall)

React에서 onScroll을 제외한 모든 이벤트는 전파됩니다. onScroll 이벤트는 오직 해당 이벤트를 붙여둔 JSX 태그에서만 동작한다는 점을 기억하세요!

전파 멈추기 (Stopping propagation)

이벤트 핸들러 함수는 유일한 인자로 이벤트 객체(event object)를 넘겨받습니다. 관례적으로 이 인자는 "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>
  );
}
.Toolbar {
  background: #aaa;
  padding: 5px;
}
button { margin: 5px; }

버튼을 클릭했을 때 어떤 일이 일어나는지 차근차근 짚어볼까요?

  1. React가 <button>에 전달된 onClick 핸들러를 호출합니다.
  2. Button 내부에 정의된 이 핸들러는 다음 두 가지를 실행합니다.
    • e.stopPropagation()을 호출해서 이벤트가 위로 버블링되는 것을 차단합니다.
    • Toolbar 컴포넌트로부터 전달받은 prop인 onClick 함수를 실행합니다.
  3. Toolbar에서 정의된 그 onClick 함수가 실행되며 버튼 고유의 경고창을 띄웁니다.
  4. 이벤트 전파가 멈췄기 때문에, 부모 <div>onClick 핸들러는 실행되지 않습니다.

e.stopPropagation() 덕분에 이제 버튼을 클릭하면 버튼 자체의 경고창 하나만 뜨고, 툴바 <div>의 경고창은 뜨지 않게 되었어요. 사용자가 버튼을 클릭한 것은 전체 툴바 영역을 클릭한 것과는 의미가 다르기 때문에, 이 UI에서는 이벤트 전파를 멈추는 것이 훨씬 자연스럽습니다.

캡처 단계 이벤트 (Capture phase events) {/capture-phase-events/}

드문 경우이긴 하지만, 자식 요소에서 일어나는 모든 이벤트를 싹 다 가로채고 싶을 때가 있어요. 심지어 자식이 이벤트 전파를 멈췄을지라도요! 예를 들어, 자식 요소들의 전파 차단 로직에 상관없이 모든 클릭 기록을 분석 툴(analytics)로 보내고 싶을 수 있겠죠. 이럴 때는 이벤트 이름 끝에 Capture를 붙여주면 된답니다.

<div onClickCapture={() => { /* 이곳이 제일 먼저 실행됩니다 */ }}>
  <button onClick={e => e.stopPropagation()} />
  <button onClick={e => e.stopPropagation()} />
</div>

이벤트는 사실 다음 세 단계를 거치면서 전파됩니다.

  1. 요소 트리를 타고 아래로 내려가면서(travels down), 모든 onClickCapture 핸들러를 호출합니다.
  2. 실제로 클릭된 요소의 onClick 핸들러를 실행합니다.
  3. 요소 트리를 타고 위로 올라가면서(travels upwards), 모든 onClick 핸들러를 호출합니다.

캡처 이벤트는 라우터(router)나 분석(analytics) 같은 핵심 공통 로직을 짤 때는 유용하지만, 일반적인 앱 개발을 할 때는 거의 쓸 일이 없으실 거예요. 참고만 해두세요!

이벤트 전파의 대안: 핸들러 전달하기 {/passing-handlers-as-alternative-to-propagation/}

이 클릭 핸들러 코드를 한 번 자세히 살펴볼까요? 핸들러 내부에서 자체적인 코드(e.stopPropagation())를 한 줄 실행한 다음에 부모가 prop으로 전달해준 onClick을 호출하고 있어요.

// 4, 5번째 줄을 주목해주세요!
function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>
      {children}
    </button>
  );
}

부모의 onClick 이벤트 핸들러를 호출하기 전에, 자식 컴포넌트에서 필요한 코드를 추가로 더 넣을 수도 있습니다. 이런 패턴은 이벤트 전파(propagation)를 대체할 수 있는 훌륭한 대안이 됩니다. 자식 컴포넌트가 이벤트를 자체적으로 처리하는 동시에, 부모 컴포넌트도 추가적인 동작을 지정할 수 있게 해주는 거죠.

이 방식은 자동적으로 일어나는 이벤트 전파와는 다릅니다. 하지만 이벤트 발생 결과로 실행되는 전체 코드의 흐름을 한눈에 명확하게 추적할 수 있다는 엄청난 장점이 있어요.

만약 이벤트 전파에 너무 많이 의존하다가 "도대체 어떤 핸들러가 왜 실행된 거지?" 하고 코드의 흐름을 파악하기 어려워진다면, 이 방식(명시적으로 함수를 넘기고 호출하는 방식)으로 바꿔보는 걸 추천합니다!

기본 동작 방지하기 (Preventing default behavior) {/preventing-default-behavior/}

몇몇 브라우저 이벤트들은 발생 시 브라우저가 기본적으로 수행하는 고유의 동작(default behavior)을 가지고 있어요. 대표적인 예로 <form> 태그의 submit 이벤트가 있죠. 폼 안에 있는 버튼을 클릭해서 제출(submit)이 발생하면, 브라우저는 기본적으로 전체 페이지를 새로고침(reload)해버립니다.

export default function Signup() {
  return (
    <form onSubmit={() => alert('Submitting!')}>
      <input />
      <button>Send</button>
    </form>
  );
}
button { margin-left: 5px; }

이런 기본 동작을 막고 싶다면 이벤트 객체에서 e.preventDefault()를 호출해주면 돼요.

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

👨‍🏫 강사의 당부: 여기서 꼭 짚고 넘어가야 할 점이 있어요. e.stopPropagation()e.preventDefault()를 절대 헷갈리지 마세요! 이름도 길고 비슷해 보여서 초보자분들이 자주 혼동하곤 한답니다. 둘 다 매우 유용하지만, 하는 역할은 완전히 다릅니다.

  • e.stopPropagation() 은 이벤트가 상위 태그에 붙은 핸들러들을 실행시키지 못하도록 버블링(전파)을 막는 역할을 합니다.
  • e.preventDefault() 는 몇몇 특정한 이벤트들이 가지고 있는 브라우저의 기본 동작(페이지 새로고침 등)을 막는 역할을 합니다.

이벤트 핸들러에 부수 효과(side effects)가 있어도 되나요? {/can-event-handlers-have-side-effects/}

그럼요, 당연하죠! 오히려 이벤트 핸들러는 부수 효과(side effects)를 두기 가장 좋은, 아니 가장 완벽한 장소입니다.

컴포넌트를 순수하게 유지해야 하는 렌더링 함수들과는 달리, 이벤트 핸들러는 순수할(pure) 필요가 전혀 없어요. 그래서 무언가를 변경할 때 아주 안성맞춤입니다. 사용자의 타이핑에 반응하여 입력창의 값을 바꾸거나, 버튼 클릭에 반응하여 목록을 수정하는 일들 말이죠.

하지만 정보를 변경하려면, 먼저 그 정보를 어딘가에 저장해둘 곳이 필요하겠죠? React에서는 이 역할을 컴포넌트의 메모리인 state(상태)가 담당합니다. 이에 대해서는 바로 다음 페이지에서 아주 자세히 배우게 될 거예요!

핵심 요약:

  • <button> 같은 엘리먼트에 함수를 prop으로 전달하여 이벤트를 처리할 수 있습니다.
  • 이벤트 핸들러는 반드시 전달해야지, 호출하면 안 됩니다! onClick={handleClick()}이 아니라 onClick={handleClick}입니다.
  • 이벤트 핸들러 함수는 컴포넌트 밖에 따로 정의할 수도 있고, 인라인(inline)으로 바로 작성할 수도 있습니다.
  • 이벤트 핸들러는 컴포넌트 내부에 정의되기 때문에, 해당 컴포넌트의 props나 state에 자유롭게 접근할 수 있습니다.
  • 부모 컴포넌트에서 이벤트 핸들러를 선언한 뒤, 자식 컴포넌트에게 prop으로 전달할 수 있습니다.
  • 이벤트 핸들러 prop의 이름은 애플리케이션의 의미에 맞게 여러분이 직접 지을 수 있습니다 (예: onPlayMovie).
  • 이벤트는 트리를 타고 위로 전파(Propagate/Bubble)됩니다. 이를 막으려면 첫 번째 인자로 받은 이벤트 객체에서 e.stopPropagation()을 호출하세요.
  • 페이지 새로고침 등 원치 않는 브라우저의 기본 동작을 막으려면 e.preventDefault()를 호출하세요.
  • 자식 핸들러 안에서 부모가 넘겨준 핸들러 prop을 명시적으로 호출하는 방법은 이벤트 전파를 막고 커스텀 로직을 넣을 수 있는 훌륭한 대안입니다.

도전 과제에 오신 것을 환영합니다! 배운 것을 직접 코드로 쳐보면서 익혀보세요.

이벤트 핸들러 고치기 {/fix-an-event-handler/}

이 버튼을 클릭하면 페이지의 배경색이 흰색과 검은색 사이를 전환해야 합니다. 그런데 클릭해도 아무 일도 일어나지 않네요. 무엇이 문제인지 찾아서 고쳐보세요. (handleClick 내부의 로직은 완벽하니 건드리지 않으셔도 됩니다!)

// 아래 코드는 에러를 발생시킵니다.
export default function LightSwitch() {
  function handleClick() {
    let bodyStyle = document.body.style;
    if (bodyStyle.backgroundColor === 'black') {
      bodyStyle.backgroundColor = 'white';
    } else {
      bodyStyle.backgroundColor = 'black';
    }
  }

  return (
    <button onClick={handleClick()}>
      Toggle the lights
    </button>
  );
}

정답 및 해설:

문제의 원인은 <button onClick={handleClick()}> 입니다. 이 코드는 함수를 전달(passing) 하는 대신 렌더링 도중에 handleClick 함수를 즉시 호출(calls) 해버리고 있어요. 끝에 있는 괄호 ()를 지워서 <button onClick={handleClick}> 으로 만들어주면 문제가 해결됩니다!

export default function LightSwitch() {
  function handleClick() {
    let bodyStyle = document.body.style;
    if (bodyStyle.backgroundColor === 'black') {
      bodyStyle.backgroundColor = 'white';
    } else {
      bodyStyle.backgroundColor = 'black';
    }
  }

  return (
    <button onClick={handleClick}>
      Toggle the lights
    </button>
  );
}

또 다른 방법으로는, 아래처럼 인라인 익명 함수로 한 번 감싸서 호출하는 방식인 <button onClick={() => handleClick()}> 도 사용할 수 있습니다.

export default function LightSwitch() {
  function handleClick() {
    let bodyStyle = document.body.style;
    if (bodyStyle.backgroundColor === 'black') {
      bodyStyle.backgroundColor = 'white';
    } else {
      bodyStyle.backgroundColor = 'black';
    }
  }

  return (
    <button onClick={() => handleClick()}>
      Toggle the lights
    </button>
  );
}

이벤트 연결하기 {/wire-up-the-events/}

아래의 ColorSwitch 컴포넌트는 버튼을 하나 렌더링하고 있습니다. 이 버튼은 페이지의 배경색을 바꾸는 역할을 해야 해요. 부모 컴포넌트로부터 전달받은 onChangeColor 이벤트 핸들러 prop을 버튼과 연결(wire up)해서, 클릭할 때 색상이 변하도록 만들어보세요.

그렇게 수정하고 나면, 배경색은 변하지만 페이지 클릭 카운터 숫자도 함께 증가하는 것을 볼 수 있을 거예요. 그런데 부모 컴포넌트를 작성한 동료 개발자는 onChangeColor를 실행할 때 카운터가 증가해선 절대 안 된다고 신신당부를 하고 있네요. 대체 무슨 일이 일어나고 있는 걸까요?

버튼을 클릭했을 때 오직 색상만 바뀌고, 카운터 숫자는 증가하지 않도록 코드를 수정해보세요!

// src/ColorSwitch.js
export default function ColorSwitch({
  onChangeColor
}) {
  return (
    <button>
      Change color
    </button>
  );
}
// src/App.js (hidden)
import { useState } from 'react';
import ColorSwitch from './ColorSwitch.js';

export default function App() {
  const [clicks, setClicks] = useState(0);

  function handleClickOutside() {
    setClicks(c => c + 1);
  }

  function getRandomLightColor() {
    let r = 150 + Math.round(100 * Math.random());
    let g = 150 + Math.round(100 * Math.random());
    let b = 150 + Math.round(100 * Math.random());
    return `rgb(${r}, ${g}, ${b})`;
  }

  function handleChangeColor() {
    let bodyStyle = document.body.style;
    bodyStyle.backgroundColor = getRandomLightColor();
  }

  return (
    <div style={{ width: '100%', height: '100%' }} onClick={handleClickOutside}>
      <ColorSwitch onChangeColor={handleChangeColor} />
      <br />
      <br />
      <h2>Clicks on the page: {clicks}</h2>
    </div>
  );
}

정답 및 해설:

우선 가장 먼저 해야 할 일은 이벤트 핸들러를 버튼에 붙여주는 거예요. <button onClick={onChangeColor}> 처럼요.

하지만 이렇게만 하면 이벤트 전파(propagation) 때문에 버튼을 클릭했을 때 클릭 이벤트가 위로 버블링되어서, 부모에 있는 핸들러까지 실행되어 카운터가 증가해버리는 문제가 발생합니다. 동료 개발자가 onChangeColor 내부에는 카운터 증가 로직이 없다고 했으니 범인은 이벤트 버블링이 확실하죠!

이 문제를 해결하려면 전파를 멈춰야 합니다. 그렇다고 원래 실행되어야 할 onChangeColor() 호출을 잊으시면 안 돼요. 아래처럼 코드를 수정해보세요.

// src/ColorSwitch.js
export default function ColorSwitch({
  onChangeColor
}) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onChangeColor();
    }}>
      Change color
    </button>
  );
}
// src/App.js (hidden)
import { useState } from 'react';
import ColorSwitch from './ColorSwitch.js';

export default function App() {
  const [clicks, setClicks] = useState(0);

  function handleClickOutside() {
    setClicks(c => c + 1);
  }

  function getRandomLightColor() {
    let r = 150 + Math.round(100 * Math.random());
    let g = 150 + Math.round(100 * Math.random());
    let b = 150 + Math.round(100 * Math.random());
    return `rgb(${r}, ${g}, ${b})`;
  }

  function handleChangeColor() {
    let bodyStyle = document.body.style;
    bodyStyle.backgroundColor = getRandomLightColor();
  }

  return (
    <div style={{ width: '100%', height: '100%' }} onClick={handleClickOutside}>
      <ColorSwitch onChangeColor={handleChangeColor} />
      <br />
      <br />
      <h2>Clicks on the page: {clicks}</h2>
    </div>
  );
}

사이트맵 (Sitemap)

모든 문서 페이지 개요 살펴보기 (Overview of all docs pages)

profile
프론트에_가까운_풀스택_개발자

0개의 댓글