본문 바로가기

이벤트 처리

2. 이벤트 처리

React에서 사용자의 상호작용을 처리하는 방법을 배워보겠습니다. 클릭, 입력, 폼 제출 등 다양한 이벤트를 어떻게 처리하는지 알아봅시다.

2.1 React에서의 이벤트

React는 브라우저의 네이티브 이벤트를 감싸는 SyntheticEvent를 사용합니다. 이를 통해 브라우저 간 일관된 동작을 보장합니다.

2.1.1 기본 이벤트 처리

function Button() {
  const handleClick = () => {
    alert('버튼이 클릭되었습니다!');
  };

  return (
    <button onClick={handleClick}>
      클릭하세요
    </button>
  );
}

2.1.2 인라인 이벤트 핸들러

function App() {
  return (
    <div>
      <button onClick={() => alert('인라인 핸들러!')}>
        인라인 핸들러
      </button>
      
      <button onClick={() => console.log('콘솔에 출력')}>
        콘솔 출력
      </button>
    </div>
  );
}

2.2 다양한 이벤트 타입

2.2.1 마우스 이벤트

import { useState } from 'react';

function MouseEvents() {
  const [message, setMessage] = useState('마우스를 움직여보세요');

  return (
    <div>
      <div
        onClick={() => setMessage('클릭됨!')}
        onDoubleClick={() => setMessage('더블클릭됨!')}
        onMouseEnter={() => setMessage('마우스 진입!')}
        onMouseLeave={() => setMessage('마우스 벗어남!')}
        onMouseMove={() => setMessage('마우스 이동 중...')}
      >
        <p>{message}</p>
      </div>
      
      <button onClick={() => setMessage('마우스를 움직여보세요')}>
        초기화
      </button>
    </div>
  );
}

2.2.2 키보드 이벤트

import { useState } from 'react';

function KeyboardEvents() {
  const [keyInfo, setKeyInfo] = useState('키를 눌러보세요');

  const handleKeyDown = (event) => {
    setKeyInfo(`눌린 키: ${event.key} (코드: ${event.keyCode})`);
  };

  const handleKeyPress = (event) => {
    if (event.key === 'Enter') {
      alert('Enter 키가 눌렸습니다!');
    }
  };

  return (
    <div>
      <input
        type="text"
        placeholder="여기에 타이핑하세요"
        onKeyDown={handleKeyDown}
        onKeyPress={handleKeyPress}
      />
      <p>
        {keyInfo}
      </p>
      <p>
        Enter 키를 누르면 알림이 표시됩니다.
      </p>
    </div>
  );
}

2.2.3 폼 이벤트

import { useState } from 'react';

function FormEvents() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });

  const handleSubmit = (event) => {
    event.preventDefault(); // 페이지 새로고침 방지
    alert(`제출된 데이터:\n이름: ${formData.name}\n이메일: ${formData.email}\n메시지: ${formData.message}`);
  };

  const handleChange = (event) => {
    const { name, value } = event.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const handleReset = () => {
    setFormData({
      name: '',
      email: '',
      message: ''
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>
          이름:
          <input
            type="text"
            name="name"
            value={formData.name}
            onChange={handleChange}
            required
          />
        </label>
      </div>

      <div>
        <label>
          이메일:
          <input
            type="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
            required
          />
        </label>
      </div>

      <div>
        <label>
          메시지:
          <textarea
            name="message"
            value={formData.message}
            onChange={handleChange}
            rows="4"
          />
        </label>
      </div>

      <div>
        <button type="submit">
          제출
        </button>
        <button
          type="button"
          onClick={handleReset}
        >
          초기화
        </button>
      </div>
    </form>
  );
}

2.3 이벤트 객체 활용

2.3.1 SyntheticEvent 속성들

import { useState } from 'react';

function EventInfo() {
  const [eventData, setEventData] = useState('이벤트가 발생하지 않았습니다');

  const handleDetailedEvent = (event) => {
    const info = {
      type: event.type,
      target: event.target.tagName,
      clientX: event.clientX,
      clientY: event.clientY,
      key: event.key,
      ctrlKey: event.ctrlKey,
      shiftKey: event.shiftKey,
      altKey: event.altKey
    };

    setEventData(JSON.stringify(info, null, 2));
  };

  return (
    <div>
      <h3>이벤트 정보 확인하기</h3>
      
      <div>
        <button 
          onClick={handleDetailedEvent}
          onMouseMove={handleDetailedEvent}
        >
          버튼 (클릭/호버)
        </button>
        
        <input
          type="text"
          placeholder="여기서 키 입력"
          onKeyDown={handleDetailedEvent}
        />
      </div>

      <pre>
        {eventData}
      </pre>
    </div>
  );
}

2.4 이벤트 전파 제어

2.4.1 event.stopPropagation()

import { useState } from 'react';

function EventPropagation() {
  const [clickLog, setClickLog] = useState([]);

  const addLog = (message) => {
    setClickLog(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`]);
  };

  const handleParentClick = () => {
    addLog('부모 요소 클릭됨');
  };

  const handleChildClick = (event) => {
    addLog('자식 요소 클릭됨');
    // event.stopPropagation(); // 주석을 해제하면 부모로 이벤트가 전파되지 않음
  };

  const handleStopPropagationClick = (event) => {
    event.stopPropagation();
    addLog('전파 차단된 자식 요소 클릭됨');
  };

  return (
    <div>
      <h3>이벤트 전파 테스트</h3>
      
      <div onClick={handleParentClick}>
        <p>부모 요소 (파란색 영역)</p>
        
        <button onClick={handleChildClick}>
          일반 자식 버튼
        </button>
        
        <button onClick={handleStopPropagationClick}>
          전파 차단 버튼
        </button>
      </div>

      <div>
        <h4>클릭 로그:</h4>
        <button onClick={() => setClickLog([])}>
          로그 지우기
        </button>
        <ul>
          {clickLog.map((log, index) => (
            <li key={index}>
              {log}
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

2.4.2 event.preventDefault()

import { useState } from 'react';

function PreventDefaultExample() {
  const [submitted, setSubmitted] = useState(false);

  const handleNormalSubmit = (event) => {
    alert('폼이 제출됩니다! (페이지가 새로고침될 수 있습니다)');
  };

  const handlePreventDefaultSubmit = (event) => {
    event.preventDefault();
    setSubmitted(true);
    alert('preventDefault()로 기본 동작을 차단했습니다!');
  };

  const handleLinkClick = (event) => {
    event.preventDefault();
    alert('링크 클릭이 차단되었습니다!');
  };

  return (
    <div>
      <h3>preventDefault() 예제</h3>
      
      <div>
        <h4>폼 제출 테스트</h4>
        <form onSubmit={handleNormalSubmit}>
          <input type="text" placeholder="일반 폼" />
          <button type="submit">일반 제출</button>
        </form>
        
        <form onSubmit={handlePreventDefaultSubmit}>
          <input type="text" placeholder="차단된 폼" />
          <button type="submit">차단된 제출</button>
        </form>
        {submitted && <p>폼 제출이 처리되었습니다!</p>}
      </div>

      <div>
        <h4>링크 클릭 테스트</h4>
        <p>
          <a href="https://example.com" target="_blank" rel="noopener noreferrer">
            일반 링크 (새 창에서 열림)
          </a>
        </p>
        <p>
          <a href="https://example.com" onClick={handleLinkClick}>
            차단된 링크 (클릭해도 이동하지 않음)
          </a>
        </p>
      </div>
    </div>
  );
}

2.5 실습: 인터랙티브 계산기

이벤트 처리를 활용한 간단한 계산기를 만들어봅시다:

import { useState } from 'react';

function Calculator() {
  const [display, setDisplay] = useState('0');
  const [previousValue, setPreviousValue] = useState(null);
  const [operation, setOperation] = useState(null);
  const [waitingForOperand, setWaitingForOperand] = useState(false);

  const inputNumber = (num) => {
    if (waitingForOperand) {
      setDisplay(String(num));
      setWaitingForOperand(false);
    } else {
      setDisplay(display === '0' ? String(num) : display + num);
    }
  };

  const inputOperation = (nextOperation) => {
    const inputValue = parseFloat(display);

    if (previousValue === null) {
      setPreviousValue(inputValue);
    } else if (operation) {
      const currentValue = previousValue || 0;
      const newValue = calculate(currentValue, inputValue, operation);

      setDisplay(String(newValue));
      setPreviousValue(newValue);
    }

    setWaitingForOperand(true);
    setOperation(nextOperation);
  };

  const calculate = (firstValue, secondValue, operation) => {
    switch (operation) {
      case '+':
        return firstValue + secondValue;
      case '-':
        return firstValue - secondValue;
      case '*':
        return firstValue * secondValue;
      case '/':
        return firstValue / secondValue;
      case '=':
        return secondValue;
      default:
        return secondValue;
    }
  };

  const performCalculation = () => {
    const inputValue = parseFloat(display);

    if (previousValue !== null && operation) {
      const newValue = calculate(previousValue, inputValue, operation);
      setDisplay(String(newValue));
      setPreviousValue(null);
      setOperation(null);
      setWaitingForOperand(true);
    }
  };

  const clear = () => {
    setDisplay('0');
    setPreviousValue(null);
    setOperation(null);
    setWaitingForOperand(false);
  };

  const handleKeyPress = (event) => {
    const { key } = event;
    
    if (key >= '0' && key <= '9') {
      inputNumber(parseInt(key));
    } else if (key === '+' || key === '-' || key === '*' || key === '/') {
      inputOperation(key);
    } else if (key === 'Enter' || key === '=') {
      performCalculation();
    } else if (key === 'Escape' || key === 'c' || key === 'C') {
      clear();
    }
  };

  // 스타일 객체들은 제거됨 (교육 목적으로 단순화)

  return (
    <div 
      onKeyDown={handleKeyPress}
      tabIndex="0"
    >
      <h3>계산기</h3>
      <p>
        키보드로도 조작 가능합니다
      </p>
      
      <div>
        {display}
      </div>

      <div>
        <button onClick={clear}>
          C
        </button>
        <button onClick={() => inputOperation('/')}>÷</button>
        <button onClick={() => inputOperation('*')}>×</button>
        <button onClick={() => inputOperation('-')}>-</button>

        <button onClick={() => inputNumber(7)}>7</button>
        <button onClick={() => inputNumber(8)}>8</button>
        <button onClick={() => inputNumber(9)}>9</button>
        <button onClick={() => inputOperation('+')}>+</button>

        <button onClick={() => inputNumber(4)}>4</button>
        <button onClick={() => inputNumber(5)}>5</button>
        <button onClick={() => inputNumber(6)}>6</button>
        <button onClick={performCalculation}>
          =
        </button>

        <button onClick={() => inputNumber(1)}>1</button>
        <button onClick={() => inputNumber(2)}>2</button>
        <button onClick={() => inputNumber(3)}>3</button>

        <button onClick={() => inputNumber(0)}>
          0
        </button>
        <button onClick={() => {
          if (display.indexOf('.') === -1) {
            setDisplay(display + '.');
          }
        }}>
          .
        </button>
      </div>
    </div>
  );
}

export default Calculator;

2.6 이벤트 처리 Best Practices

2.6.1 이벤트 핸들러 네이밍

// ✅ 좋은 예
const handleSubmit = () => { /* ... */ };
const handleButtonClick = () => { /* ... */ };
const handleInputChange = () => { /* ... */ };

// ❌ 나쁜 예
const submit = () => { /* ... */ };
const click = () => { /* ... */ };
const change = () => { /* ... */ };

2.6.2 이벤트 핸들러 분리

// ✅ 좋은 예 - 핸들러 분리
function TodoItem({ todo, onToggle, onDelete }) {
  const handleToggle = () => {
    onToggle(todo.id);
  };

  const handleDelete = () => {
    onDelete(todo.id);
  };

  return (
    <div>
      <input type="checkbox" checked={todo.completed} onChange={handleToggle} />
      <span>{todo.text}</span>
      <button onClick={handleDelete}>삭제</button>
    </div>
  );
}

// ❌ 나쁜 예 - 인라인 핸들러
function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <div>
      <input 
        type="checkbox" 
        checked={todo.completed} 
        onChange={() => onToggle(todo.id)} 
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>삭제</button>
    </div>
  );
}

2.6.3 useCallback으로 최적화

import { useState, useCallback } from 'react';

function OptimizedComponent() {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState([]);

  // useCallback으로 핸들러 메모이제이션
  const handleIncrement = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  const handleAddItem = useCallback(() => {
    setItems(prev => [...prev, `아이템 ${prev.length + 1}`]);
  }, []);

  return (
    <div>
      <p>카운트: {count}</p>
      <button onClick={handleIncrement}>증가</button>
      <button onClick={handleAddItem}>아이템 추가</button>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

2.7 정리

React에서의 이벤트 처리를 학습했습니다:

  1. SyntheticEvent로 브라우저 간 일관된 이벤트 처리
  2. 다양한 이벤트 타입 (마우스, 키보드, 폼 등) 활용
  3. 이벤트 객체의 속성들을 통한 상세 정보 접근
  4. event.stopPropagation()과 preventDefault()로 이벤트 제어
  5. 이벤트 핸들러 최적화 및 Best Practices

다음 장에서는 조건부 렌더링에 대해 알아보겠습니다.

이벤트 처리 - 리엑트 베이스캠프 | 위니버시티