리액트 기초 강좌 #6 이벤트 핸들링

7 분 소요

지난 시간에는 state와 useState를 배우면서 자연스럽게 onClick이라는 이벤트 핸들러를 사용했습니다. 그 자체로도 동작은 했지만, 리액트의 이벤트 처리 방식에는 몇 가지 알아둘 것들이 있습니다. 이번 시간에는 이벤트 핸들링을 본격적으로 다뤄보겠습니다.

리액트에서 이벤트 처리하는 법 #

리액트에서 이벤트는 JSX 속성으로 핸들러 함수를 전달하는 방식으로 처리합니다. HTML의 onclick이 아니라 camelCase의 **onClick**을 사용한다는 점만 다릅니다.

src/App.jsx
function App() {
  function handleClick() {
    alert('버튼이 클릭되었습니다!');
  }

  return <button onClick={handleClick}>클릭</button>;
}

export default App;

여기서 한 가지 자주 하는 실수가 있습니다. 함수를 호출(handleClick())하면 안 되고, 함수 자체(handleClick)를 전달해야 합니다.

잘못된 예
<button onClick={handleClick()}>클릭</button>

이렇게 쓰면 컴포넌트가 렌더링되는 즉시 handleClick()이 실행되어 alert이 떠버립니다. 게다가 onClick에는 handleClick의 반환값(undefined)이 등록되므로 진짜 클릭에는 아무 일도 일어나지 않습니다.

올바른 예
<button onClick={handleClick}>클릭</button>

함수 참조만 넘기고 호출은 리액트가 클릭 시점에 합니다. 이 차이를 꼭 기억해두세요.

인라인 핸들러 #

간단한 핸들러는 아예 JSX 안에 화살표 함수로 직접 작성하기도 합니다.

src/Counter.jsx
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      카운트: {count}
    </button>
  );
}

() => setCount(count + 1)은 클릭 시점에 호출되는 익명 함수입니다. 한 줄짜리 단순한 핸들러는 이렇게 인라인으로 두는 게 편하고, 로직이 길어지면 별도 함수로 빼는 게 가독성에 좋습니다. 정해진 규칙은 없고 팀이나 본인의 취향에 따릅니다.

함수에 인자 전달하기 #

핸들러 함수에 인자를 넘겨야 할 때는 인라인 화살표 함수로 감싸야 합니다.

src/App.jsx
function App() {
  function handleClick(name) {
    alert(`${name}님 안녕하세요!`);
  }

  return (
    <>
      <button onClick={() => handleClick('철수')}>철수에게 인사</button>
      <button onClick={() => handleClick('영희')}>영희에게 인사</button>
    </>
  );
}

다음과 같이 쓰면 안 됩니다.

잘못된 예
<button onClick={handleClick('철수')}>...</button>

위에서 봤듯이 이건 렌더링 즉시 호출되어 버립니다. 인자를 넘기려면 반드시 화살표 함수로 한 번 감싸서 “클릭될 때 호출하라"는 의미를 만들어야 합니다.

이벤트 객체 #

이벤트 핸들러는 첫 번째 매개변수로 이벤트 객체를 받습니다. 이 객체에는 어떤 요소에서 어떤 이벤트가 일어났는지에 대한 정보가 담겨 있습니다.

src/InputDemo.jsx
import { useState } from 'react';

function InputDemo() {
  const [text, setText] = useState('');

  function handleChange(e) {
    setText(e.target.value);
  }

  return (
    <div>
      <input type="text" value={text} onChange={handleChange} />
      <p>입력값: {text}</p>
    </div>
  );
}

export default InputDemo;

매개변수 이름은 보통 e 또는 event로 짓습니다. e.target은 이벤트가 발생한 DOM 요소이고, e.target.value로 입력값을 꺼낼 수 있습니다.

노트
리액트의 이벤트 객체는 정확히는 브라우저의 네이티브 이벤트가 아니라 **합성 이벤트(SyntheticEvent)**입니다. 리액트가 모든 브라우저에서 동일하게 동작하도록 한 번 감싼 객체입니다. API는 네이티브 이벤트와 거의 같아서 평소에는 신경 쓸 일이 없습니다. e.preventDefault(), e.target, e.key 같은 익숙한 속성/메서드를 그대로 사용할 수 있습니다.

자주 쓰는 이벤트들 #

가장 많이 사용하는 이벤트 핸들러를 정리하면:

  • onClick — 클릭
  • onChange — 입력 요소(input, textarea, select)의 값이 바뀔 때
  • onSubmit — 폼이 제출될 때
  • onKeyDown / onKeyUp — 키보드 키가 눌리거나 떼질 때
  • onMouseEnter / onMouseLeave — 마우스가 요소 위에 들어오거나 벗어날 때
  • onFocus / onBlur — 포커스 진입/해제

각각의 이벤트는 그에 맞는 정보를 이벤트 객체에 담아줍니다. onChange라면 e.target.value를, onKeyDown이라면 e.key(누른 키 이름)를 보면 됩니다.

src/SearchBox.jsx
function SearchBox() {
  function handleKeyDown(e) {
    if (e.key === 'Enter') {
      alert('엔터 키가 눌렸습니다');
    }
  }

  return <input type="text" onKeyDown={handleKeyDown} />;
}

기본 동작 막기 #

브라우저는 어떤 이벤트에 대해 기본 동작을 가지고 있습니다. 폼이 제출되면 페이지가 새로고침되고, 링크를 클릭하면 페이지가 이동합니다. 이 기본 동작을 막으려면 이벤트 객체의 **preventDefault()**를 호출합니다.

src/LoginForm.jsx
import { useState } from 'react';

function LoginForm() {
  const [email, setEmail] = useState('');

  function handleSubmit(e) {
    e.preventDefault();  // 폼 제출로 인한 페이지 새로고침을 막음
    console.log('제출된 이메일:', email);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button type="submit">로그인</button>
    </form>
  );
}

export default LoginForm;

폼은 제출 버튼을 누르거나 입력창에서 엔터를 치면 자동으로 onSubmit이 발화됩니다. 이때 e.preventDefault()가 없으면 브라우저가 페이지를 새로고침해버려서, 우리가 작성한 처리 로직이 의미를 잃습니다.

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

이벤트 핸들러도 그냥 함수일 뿐이므로, props로 자식 컴포넌트에 전달할 수 있습니다. 이 패턴은 자식이 일으킨 이벤트를 부모가 처리해야 할 때 매우 자주 사용됩니다.

src/Button.jsx
function Button({ label, onClick }) {
  return (
    <button onClick={onClick} style={{ padding: '8px 16px' }}>
      {label}
    </button>
  );
}

export default Button;
src/App.jsx
import Button from './Button';

function App() {
  function handleSave() {
    alert('저장되었습니다');
  }

  function handleCancel() {
    alert('취소되었습니다');
  }

  return (
    <>
      <Button label="저장" onClick={handleSave} />
      <Button label="취소" onClick={handleCancel} />
    </>
  );
}

부모는 어떤 행동을 할지(handler)를 전달하고, 자식은 언제 그 행동이 일어나는지(클릭)를 알려주는 구조입니다. 핸들러 prop의 이름은 관례상 on으로 시작하게 짓습니다 (onClick, onSave, onItemSelect …).

핸들러 안에서 state 변경하기 #

지난 글에서 본 패턴인데, 이벤트 핸들러 안에서 state를 변경하는 것이 가장 흔한 패턴입니다.

src/Toggle.jsx
import { useState } from 'react';

function Toggle() {
  const [isOn, setIsOn] = useState(false);

  function handleToggle() {
    setIsOn(prev => !prev);
  }

  return (
    <div>
      <p>현재 상태: {isOn ? 'ON' : 'OFF'}</p>
      <button onClick={handleToggle}>토글</button>
    </div>
  );
}

export default Toggle;

이벤트가 일어나면 → 핸들러가 실행되고 → state가 갱신되고 → 화면이 다시 그려집니다. 이 흐름이 리액트 앱의 가장 기본적인 패턴입니다.

직접 해보기 #

간단한 입력 폼을 만들어보겠습니다. 사용자가 이름과 메시지를 입력하고 “추가"를 누르면 화면 아래에 메시지가 추가되는 컴포넌트입니다 (다음 글 #7, #8에서 이걸 더 확장합니다).

src/MessageForm.jsx를 만듭니다.

src/MessageForm.jsx
import { useState } from 'react';

function MessageForm() {
  const [name, setName] = useState('');
  const [message, setMessage] = useState('');
  const [lastSubmitted, setLastSubmitted] = useState(null);

  function handleSubmit(e) {
    e.preventDefault();
    if (!name || !message) return;
    setLastSubmitted({ name, message });
    setName('');
    setMessage('');
  }

  return (
    <div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
      <form onSubmit={handleSubmit}>
        <div>
          <input
            type="text"
            placeholder="이름"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
        </div>
        <div style={{ marginTop: '8px' }}>
          <input
            type="text"
            placeholder="메시지"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
          />
        </div>
        <button type="submit" style={{ marginTop: '8px' }}>추가</button>
      </form>
      {lastSubmitted && (
        <p style={{ marginTop: '12px' }}>
          마지막 입력: <strong>{lastSubmitted.name}</strong>  {lastSubmitted.message}
        </p>
      )}
    </div>
  );
}

export default MessageForm;

src/App.jsx에 연결합니다.

src/App.jsx
import MessageForm from './MessageForm';

function App() {
  return (
    <>
      <h1>메시지 </h1>
      <MessageForm />
    </>
  );
}

export default App;

이름과 메시지를 입력하고 엔터나 “추가” 버튼을 눌러보세요. 마지막에 제출된 값이 아래에 표시되고, 입력 필드는 비워집니다. e.preventDefault()를 빼면 폼이 새로고침되어 입력값이 사라지는 것도 한 번 실험해보세요.

위 코드에서 lastSubmitted && (...) 부분이 처음 보이실 텐데, 이것은 조건부 렌더링입니다. 값이 있으면 보여주고 없으면 안 보여 주는 패턴입니다. 다음 글인 #7에서 자세히 다루겠습니다.

마무리 #

이번 글에서는 리액트의 이벤트 처리를 살펴봤습니다. 핵심은:

  • onClick처럼 camelCase 속성으로 핸들러를 등록한다
  • 함수를 호출하지 말고 전달한다 ({handleClick}이지 {handleClick()}이 아니다)
  • 인자를 넘기려면 화살표 함수로 감싼다
  • 핸들러는 첫 매개변수로 이벤트 객체(e)를 받는다
  • e.preventDefault()로 브라우저 기본 동작을 막을 수 있다
  • 핸들러도 props로 자식에 내려보낼 수 있다 (on으로 시작하는 이름)

또한 MessageForm 예제에서 슬쩍 본 {lastSubmitted && ...} 패턴이 바로 다음 주제로 이어집니다. 다음 글인 “리액트 기초 강좌 #7 조건부 렌더링"에서는 화면의 일부를 상태에 따라 보였다 안 보였다 하거나 다른 모습으로 바꾸는 다양한 패턴을 정리해보겠습니다.

X