Dev Thinking
32완료

클릭에서 이벤트까지 - 리액트 이벤트 처리의 우아함

2025-08-11
7분 읽기

리액트 공식문서 기반의 리액트 입문기

들어가며

개발자라면 사용자 인터페이스(UI)에서 사용자의 클릭이나 키보드 입력과 같은 다양한 이벤트에 반응하여 동적인 경험을 제공하는 것이 얼마나 중요한지 잘 알고 있을 것입니다. 지금까지 우리는 바닐라 자바스크립트에서 addEventListener와 같은 메서드를 활용하여 이러한 상호작용을 구현해왔습니다. 하지만 리액트에서는 이벤트를 처리하는 방식이 조금 다릅니다.

이번 편에서는 우리가 늘 사용해온 자바스크립트 방식의 이벤트 처리와 리액트 방식의 이벤트 처리를 비교하며, 리액트가 제공하는 효율적인 이벤트 관리 방법을 탐색할 것입니다. 특히 리액트에서 이벤트를 어떻게 정의하고 활용하는지에 대한 구체적인 방법을 중심으로 알아보겠습니다.

자바스크립트로 클릭 이벤트 만들기

먼저 우리가 늘 사용해온 방식으로 버튼 클릭 이벤트를 처리하는 자바스크립트 코드를 살펴보겠습니다. 간단한 카운터 버튼을 만들고, 이 버튼을 클릭할 때마다 숫자가 증가하도록 구현해 보겠습니다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vanilla JS Event Handling</title>
  </head>
  <body>
    <div id="root">
      <p>카운트: <span id="count">0</span></p>
      <button id="incrementButton">증가</button>
    </div>
 
    <script>
      (function () {
        let count = 0;
        const countElement = document.getElementById("count");
        const incrementButton = document.getElementById("incrementButton");
 
        function updateCount() {
          countElement.textContent = count;
        }
 
        if (incrementButton) {
          incrementButton.addEventListener("click", function () {
            count++;
            updateCount();
            console.log("현재 카운트 (JS):", count);
          });
        }
      })();
    </script>
  </body>
</html>

자바스크립트 방식의 특징

  1. 명령형 DOM 조작: document.getElementByIdaddEventListener를 사용하여 특정 DOM 요소를 직접 선택하고 이벤트를 연결합니다. UI의 변경도 textContent 속성을 직접 조작하여 이루어집니다.
  2. 이벤트 리스너 직접 부착: 각 이벤트 대상에 대해 addEventListener를 호출하여 이벤트 핸들러를 개별적으로 등록합니다. 이는 많은 요소에 이벤트를 걸어야 할 경우 코드가 길어질 수 있습니다.
  3. 데이터와 UI의 수동 동기화: count 변수를 수동으로 증가시키고, 이 변경된 값을 updateCount 함수를 통해 UI에 반영해야 합니다. 개발자가 데이터와 UI의 동기화 책임을 직접 가집니다.
  4. 스코프 관리: IIFE(즉시 실행 함수)를 활용하여 전역 스코프 오염을 방지하고 변수 count를 보호합니다. 이는 리액트의 컴포넌트 스코프와 유사한 목적을 가집니다.

리액트로 동일한 클릭 이벤트 만들기

이제 리액트 방식으로 동일한 카운터 버튼을 만들어 보겠습니다. 리액트에서는 useState 훅을 사용하여 상태를 관리하고, JSX 문법 내에서 이벤트를 선언적으로 처리합니다. useState에 대한 자세한 내용은 다음 편에서 다룰 예정이니, 여기서는 단순히 컴포넌트가 값을 기억하고 업데이트할 수 있게 해주는 도구라고 이해하며 넘어가겠습니다.

import { useState } from 'react';
 
function Counter() {
  const [count, setCount] = useState(0);
 
  const handleClick = () => {
    setCount(count + 1);
    console.log('현재 카운트 (React):', count + 1); // setCount는 비동기적으로 작동하므로, 즉시 업데이트된 값을 보려면 count + 1 사용
  };
 
  return (
    <div>
      <p>
        카운트: <span>{count}</span>
      </p>
      <button onClick={handleClick}>증가</button>
    </div>
  );
}
 
export default Counter;

잠깐 살펴보자면, setCount는 비동기적으로 동작하기 때문에 클릭 직후 콘솔에서 확인하는 count 값은 이전 렌더링의 스냅샷일 수 있습니다. 최신 값을 안전하게 계산해야 할 때는 함수형 업데이트(3-5편) 방식을 고려하고, 값 확인은 화면(UI)과 상태 흐름을 기준으로 판단하는 편이 더 예측 가능합니다. 이 동작의 배경은 3-4편에서 '스냅샷으로서의 State'로 자세히 다룹니다.

리액트에서 이벤트 적용하는 다양한 방법

리액트에서는 JSX 내에 이벤트를 직접 작성하여 컴포넌트의 상호작용을 선언적으로 정의합니다. 이는 마치 HTML 태그의 속성처럼 보이지만, 실제로는 자바스크립트 함수를 연결하는 방식입니다. 여러 가지 방법을 통해 이벤트를 효율적으로 적용할 수 있습니다.

1. 이벤트 핸들러 정의 방식

이벤트 핸들러 함수를 정의하는 주요 방식은 크게 두 가지입니다.

가. 인라인 함수로 정의하기

가장 간단한 방법으로, JSX 내에서 직접 익명 함수를 사용하여 이벤트 핸들러를 정의하는 방식입니다. 간단한 동작에 유용합니다.

function MyButton() {
  return <button onClick={() => alert('버튼이 클릭되었습니다!')}>클릭하세요</button>;
}

나. 외부 함수를 참조하여 정의하기

재사용성이 높거나 로직이 복잡한 경우, 컴포넌트 내부 또는 외부에 별도의 함수를 정의하고 이를 onClick 등의 prop으로 참조하는 방식이 권장됩니다. 이는 코드의 가독성을 높이고 로직을 분리하는 데 도움을 줍니다.

function MyButton() {
  const handleClick = () => {
    alert('외부 함수로 처리된 클릭 이벤트!');
  };
 
  return <button onClick={handleClick}>클릭하세요</button>;
}

2. 이벤트 객체(Event Object) 활용

모든 이벤트 핸들러 함수는 첫 번째 인자로 **이벤트 객체(Event Object)**를 전달받습니다. 이 객체는 발생한 이벤트에 대한 다양한 정보를 담고 있으며, 특정 동작을 제어하는 데 사용됩니다.

function MyForm() {
  const handleSubmit = e => {
    e.preventDefault(); // 폼의 기본 제출 동작 방지
    console.log('폼이 제출되었습니다!');
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" />
      <button type="submit">제출</button>
    </form>
  );
}

여기서 e.preventDefault()는 폼 제출 시 페이지가 새로고침되는 기본 동작을 막아줍니다. 이는 단일 페이지 애플리케이션(SPA)에서 매우 중요한 기능입니다.

3. 이벤트 핸들러에 매개변수 전달하기

때로는 이벤트 핸들러에 추가적인 데이터를 전달해야 할 때가 있습니다. 이때는 주로 화살표 함수를 사용하여 매개변수를 전달합니다.

function ItemList() {
  const items = ['사과', '바나나', '딸기'];
 
  const handleItemClick = itemName => {
    alert(`${itemName}이(가) 선택되었습니다.`);
  };
 
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index} onClick={() => handleItemClick(item)}>
          {item}
        </li>
      ))}
    </ul>
  );
}

onClick={() => handleItemClick(item)}과 같이 화살표 함수 내에서 이벤트 핸들러를 호출하면서 매개변수를 전달할 수 있습니다. 이렇게 하면 item 값이 handleItemClick 함수로 전달됩니다.

4. 다양한 이벤트 타입

리액트에서는 onClick 외에도 다양한 DOM 이벤트에 대응하는 이벤트를 제공합니다. 몇 가지 예시는 다음과 같습니다.

  • onChange: <input>, <textarea>, <select> 요소의 값이 변경될 때 발생합니다.
  • onSubmit: <form>이 제출될 때 발생합니다.
  • onMouseEnter, onMouseLeave: 요소에 마우스 커서가 올라가거나 벗어날 때 발생합니다.
  • onFocus, onBlur: 요소가 포커스를 얻거나 잃을 때 발생합니다.

이러한 이벤트들은 자바스크립트의 DOM 이벤트와 거의 동일하게 동작하지만, JSX 문법 내에서 카멜케이스(camelCase)로 작성된다는 점이 다릅니다.

리액트 방식의 특징 (이벤트 처리)

  • 1. 선언적 이벤트 처리와 마크업/로직 응집: 리액트에서는 HTML 속성처럼 onClick prop에 이벤트 핸들러 함수를 직접 연결하는 선언적인 방식으로 이벤트를 처리합니다. 이는 UI 마크업(JSX)과 해당 UI의 동작 로직을 하나의 컴포넌트 안에서 결합하여 코드의 **응집도(cohesion)**를 높여줍니다. 개발자는 button을 클릭했을 때 '무엇을 할지'(handleClick)만 선언하고, '어떻게' 이벤트가 연결되고 실행될지는 리액트가 관리하므로, 코드의 가독성이 향상되고 개발자가 UI 로직에 더 집중할 수 있게 됩니다.
  • 2. 상태 기반 UI 업데이트와 자동 동기화: useState 훅을 사용하여 count 상태를 정의하고 setCount 함수로 상태를 변경합니다. 리액트의 핵심 철학은 **상태(State)**가 변경되면 리액트가 자동으로 컴포넌트를 다시 렌더링하여 UI를 업데이트한다는 것입니다. 이는 개발자가 document.textContent와 같이 DOM을 직접 조작하며 데이터와 UI를 수동으로 동기화해야 하는 부담에서 벗어나게 하여, UI 버그 발생 가능성을 줄이고 개발 생산성을 크게 향상시킵니다. (useState에 대한 자세한 내용은 다음 편에서 다룹니다.)
  • 3. 합성 이벤트 시스템과 효율적인 이벤트 위임: 리액트는 브라우저의 네이티브 DOM 이벤트를 직접 사용하는 대신, 자체적인 **합성 이벤트 시스템(Synthetic Event System)**을 구축하여 효율적으로 이벤트를 처리합니다. 이 시스템은 내부적으로 브라우저의 이벤트 위임(Event Delegation) 메커니즘을 활용하여, 모든 이벤트 리스너를 document와 같은 최상위 노드에 단 하나만 등록하고 이벤트가 발생했을 때 적절한 컴포넌트의 핸들러를 호출합니다. 이 덕분에 우리는 개별 DOM 요소에 직접 이벤트 리스너를 등록할 필요 없이 선언적으로 이벤트를 다룰 수 있으며, 대규모 애플리케이션에서도 뛰어난 성능을 유지할 수 있습니다.
  • 4. 컴포넌트 중심 개발과 캡슐화: 리액트에서는 모든 로직과 UI가 Counter 컴포넌트 내에 캡슐화되어 관리됩니다. 이벤트 핸들러 또한 컴포넌트의 일부로 정의되어 해당 컴포넌트의 상태에만 영향을 미치도록 설계됩니다. 이러한 컴포넌트 중심의 캡슐화는 컴포넌트의 재사용성을 높이고, 특정 기능의 변경이 필요한 경우 해당 컴포넌트만 수정하면 되므로 코드의 유지보수성을 크게 향상시킵니다. 이는 리액트의 모듈화 철학을 반영하는 핵심적인 특징입니다.

자바스크립트 vs 리액트 차이

두 방식의 주요 차이점을 표로 정리해 보겠습니다.

구분자바스크립트리액트
상태 정의변수(let, const 등)를 사용하고 개발자가 직접 관리useState 훅을 사용하여 선언적으로 상태 정의 (자세한 내용은 다음 편에서)
상태 변경변수를 직접 수정하고 DOM을 수동으로 조작setCount 함수를 통해 상태를 업데이트하면 리액트가 자동 렌더링
UI 업데이트textContent와 같은 DOM API로 직접 UI 변경상태 변경 시 Virtual DOM 비교 후 필요한 부분만 효율적으로 업데이트
이벤트 연결addEventListener로 이벤트 리스너 직접 부착JSX 속성으로 onClick={handler}와 같이 선언적으로 연결
이벤트 시스템브라우저의 네이티브 DOM 이벤트브라우저 이벤트를 래핑하여 표준화된 인터페이스 제공 및 이벤트 위임 활용

요약

이전 섹션에서 우리는 사용자의 상호작용에 응답하는 기본적인 방법인 이벤트 처리 방식을 자바스크립트와 리액트 각각의 관점에서 살펴보았습니다. 바닐라 자바스크립트가 명령형 방식으로 DOM을 직접 조작하며 이벤트를 처리하는 반면, 리액트는 선언적이고 상태 기반의 접근 방식을 통해 UI 업데이트의 복잡성을 줄여준다는 것을 알 수 있었습니다. 이번에 공부한 내용을 정리해 보겠습니다.

  • 이벤트 처리 방식: 자바스크립트는 명령형 DOM 조작, 리액트는 선언형 JSX(onClick)
  • UI 업데이트 책임: 자바스크립트는 수동 DOM 변경, 리액트는 상태 기반 자동 렌더링
  • 이벤트 시스템: 자바스크립트는 네이티브 DOM 이벤트, 리액트는 래핑/위임으로 표준화
  • 컴포넌트 중심 개발: 이벤트 로직과 UI를 컴포넌트에 캡슐화하여 재사용성과 유지보수성 향상

참고문서

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