React Event

김소희·2025년 11월 5일

컴포넌트와 이벤트

JavaScript에서 이벤트(event)는 사용자와의 상호작용에 따라 발생하는 일련의 사건을 의미한다.
마우스를 올리거나 클릭하거나, 키보드로 글자를 입력하는 등의 동작이 모두 이벤트로 간주된다.

이벤트의 기본 형식은 아래와 같다.

<JSXElement 이벤트_속성={이벤트_핸들러} />

이벤트 속성

HTML에서 이벤트 속성은 특정 동작 수행 시 실행할 JavaScript 코드를 지정하는 속성이었다.
리액트에서도 이벤트 속성의 목적은 비슷하지만, HTML과 달리 몇 가지 중요한 차이점이 있다.

HTML vs React 이벤트 속성 차이

구분HTMLReact
네이밍소문자 (onclick)카멜케이스 (onClick)
문자열함수 참조
예시<button><button onClick={handleClick}>
<!-- HTML -->
<button onclick="alert('클릭!')">클릭</button>

<!-- React -->
<button onClick={() => alert('클릭!')}>클릭</button>

DOM 표준 이벤트 vs 커스텀 이벤트

중요한 규칙:

  1. DOM 태그에 이벤트 속성을 쓸 때는 브라우저가 실제로 알고 있는 DOM 표준 이벤트 이름만 사용해야 한다.
// ✅ 올바른 코드 - DOM 표준 이벤트
<button onClick={handleClick}>클릭</button>
<input onChange={handleChange} />

// ❌ 잘못된 코드 - DOM이 모르는 이벤트
<button onCustomClick={handleClick}>클릭</button> // 작동 안 함!
  1. 커스텀 컴포넌트(props 수준)에서는 이벤트 이름을 자유롭게 정의할 수 있다.
// ✅ 커스텀 컴포넌트에서는 자유롭게 정의 가능
function MyButton({ onCustomClick, onSpecialAction }) {
  return (
    <button onClick={onCustomClick}>
      클릭
    </button>
  );
}

// 사용
<MyButton 
  onCustomClick={handleClick}
  onSpecialAction={handleSpecial}
/>

단, 그 props를 실제 DOM 이벤트에 연결하는 시점에는 반드시 DOM 표준 이벤트 이름을 사용해야 한다.


자주 쓰이는 이벤트 속성

마우스 이벤트

이벤트 속성설명
onClick클릭 이벤트 발생 시 호출
onDoubleClick더블 클릭 이벤트 발생 시 호출
onMouseEnter마우스가 요소 위로 올라갈 때 호출
onMouseLeave마우스가 요소에서 이동했을 때 호출
onMouseMove마우스가 요소 위에서 움직일 때 호출
onMouseDown마우스 버튼을 눌렀을 때 호출
onMouseUp마우스 버튼을 눌렀다 뺐을 때 호출
onContextMenu마우스 오른쪽 버튼 클릭 시 호출
onDrag드래그할 때 호출
onDrop드래그한 요소를 놓을 때 호출

키보드 이벤트

이벤트 속성설명
onKeyDown키를 눌렀을 때 호출 (권장)
onKeyUp키를 눌렀다 뺐을 때 호출

참고: onKeyPress는 더 이상 권장되지 않습니다(deprecated). 대신 onKeyDown을 사용하세요.

폼 이벤트

이벤트 속성설명
onChange입력 필드의 값이 변경될 때 호출 (실시간)
onInput입력 필드에서 입력이 발생할 때 호출
onFocus입력 필드에 포커스가 들어올 때 호출
onBlur입력 필드에서 포커스가 빠져나갈 때 호출
onSubmit폼이 제출될 때 호출

onChange vs onInput:

  • React에서 onChange는 실시간으로 동작합니다 (HTML과 다름)
  • onInput은 거의 사용하지 않으며, React에서는 onChange 사용을 권장합니다

터치 이벤트

이벤트 속성설명
onTouchStart터치가 시작될 때 호출
onTouchMove터치 중에 움직일 때 호출
onTouchEnd터치가 끝났을 때 호출

기타 이벤트

이벤트 속성설명
onLoad이미지나 iframe 등이 로드될 때 호출
onError이미지나 iframe 등에서 오류 발생 시 호출
onScroll요소가 스크롤될 때 호출

이벤트 핸들러란?

이벤트 핸들러는 이벤트가 발생했을 때 실행할 함수이다.
JSX 요소에 이벤트 속성을 이용해 이벤트를 등록할 때, 이벤트 속성에는 이벤트 핸들러를 할당해야 한다.

function App() {
  function handleClick() {
    alert("버튼 눌림!");
  }

  return (
    <button onClick={handleClick}>누르기</button>
  );
}
역할이름의미
이벤트 핸들러(함수)handleClick"버튼 눌리면 실행될 함수"
이벤트 속성onClick버튼에 "클릭 이벤트 달아주세요"라는 속성

핸들러 작성 방법

방법 1: 함수 참조 전달 (매개변수 없을 때)

컴포넌트 내부나 외부에 이벤트 핸들러를 정의하고, 해당 함수를 참조하여 이벤트 속성에 할당하는 방식이다.

function App() {
  function handleClick() {
    alert("클릭됨!");
  }

  // ✅ 올바른 방법 - 함수 참조만 전달
  return <button onClick={handleClick}>클릭</button>;
}

주의사항: 함수를 호출하면 안 된다!

// ❌ 잘못된 코드 - 함수가 즉시 실행됨
<button onClick={handleClick()}>클릭</button>

// ✅ 올바른 코드 - 함수 참조만 전달
<button onClick={handleClick}>클릭</button>

handleClick()처럼 괄호를 붙이면 렌더링 시점에 함수가 즉시 실행되고, 그 반환값(보통 undefined)이 onClick에 할당된다.

방법 2: 인라인으로 화살표 함수 작성

이벤트 속성 안에 화살표 함수를 직접 작성하는 방법이다.

function App() {
  return (
    <button onClick={() => alert("클릭됨!")}>클릭</button>
  );
}

장점: 간단한 로직에 적합
단점: 복잡한 로직은 가독성 저하


매개변수가 있는 이벤트 핸들러 처리

이벤트 핸들러에 추가 매개변수를 전달해야 하는 경우가 많다. 여러 가지 방법이 있지만, 실무에서는 주로 화살표 함수로 감싸는 방법을 사용한다.

방법 1: 화살표 함수로 감싸기 ⭐ (가장 일반적)

function App() {
  function handleClick(id, name) {
    alert(`${id}${name} 클릭됨!`);
  }

  return (
    <div>
      <button onClick={() => handleClick(1, "첫번째")}>버튼 1</button>
      <button onClick={() => handleClick(2, "두번째")}>버튼 2</button>
    </div>
  );
}

장점:

  • 직관적이고 읽기 쉬움
  • 가장 많이 사용되는 패턴
  • 매개변수 개수에 제한 없음

단점:

  • 렌더링마다 새 함수가 생성됨 (대부분의 경우 성능 문제 없음)

방법 2: 이벤트 객체와 함께 사용

이벤트 객체(event)가 필요한 경우, 화살표 함수의 매개변수로 받아서 전달할 수 있다.

function App() {
  function handleClick(id, event) {
    event.preventDefault(); // 기본 동작 막기
    alert(`${id}번 클릭됨!`);
    console.log(event.target); // 클릭된 요소
  }

  return (
    <button onClick={(e) => handleClick(1, e)}>클릭</button>
  );
}

이벤트 객체 순서: React는 이벤트 객체를 마지막 매개변수로 자동 전달하지 않으므로, 명시적으로 전달해야 한다.

방법 3: 배열 렌더링에서 활용 (실무 예시)

가장 흔한 사용 사례는 배열을 렌더링할 때 각 항목에 고유한 값을 전달하는 것이다.

function TodoList() {
  const todos = [
    { id: 1, text: "리액트 공부" },
    { id: 2, text: "운동하기" },
    { id: 3, text: "독서하기" }
  ];

  function handleDelete(id) {
    alert(`${id}번 할 일 삭제`);
  }

  function handleEdit(id, currentText) {
    const newText = prompt("수정할 내용", currentText);
    alert(`${id}번을 "${newText}"로 수정`);
  }

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {todo.text}
          <button onClick={() => handleDelete(todo.id)}>삭제</button>
          <button onClick={() => handleEdit(todo.id, todo.text)}>수정</button>
        </li>
      ))}
    </ul>
  );
}

방법 4: bind() 사용 (예전 방식)

function App() {
  function handleClick(id, name, event) {
    alert(`${id}${name} 클릭됨!`);
  }

  return (
    <button onClick={handleClick.bind(null, 1, "버튼")}>클릭</button>
  );
}

장점: 함수 참조가 일정하게 유지됨
단점: 가독성이 떨어지고, 현대적인 React에서는 잘 사용하지 않음

방법 5: 고차 함수 (Currying)

함수가 함수를 반환하는 패턴이다.

function App() {
  function handleClick(id) {
    return function(event) {
      alert(`${id}번 클릭됨!`);
    };
  }

  // handleClick(1)은 함수를 반환하므로 올바른 핸들러가 됨
  return (
    <button onClick={handleClick(1)}>클릭</button>
  );
}

장점: 함수형 프로그래밍 스타일, 메모이제이션에 유리
단점: 초보자에게는 복잡해 보일 수 있음

방법 6: 데이터 속성 활용 (data-* attributes)

DOM 요소에 데이터를 저장하고, 이벤트 객체를 통해 접근하는 방법이다.

function App() {
  function handleClick(event) {
    const id = event.currentTarget.dataset.id;
    const name = event.currentTarget.dataset.name;
    alert(`${id}${name} 클릭됨!`);
  }

  return (
    <div>
      <button onClick={handleClick} data-id="1" data-name="첫번째">버튼 1</button>
      <button onClick={handleClick} data-id="2" data-name="두번째">버튼 2</button>
    </div>
  );
}

장점:

  • 매개변수 없이 이벤트 객체만으로 처리
  • 여러 버튼이 같은 핸들러를 공유할 때 유용

단점:

  • 문자열로만 전달 가능 (숫자나 객체는 변환 필요)
  • DOM에 데이터가 노출됨

이벤트 객체 (Event Object)

React의 이벤트는 브라우저의 네이티브 이벤트를 감싼 합성 이벤트(SyntheticEvent)이다.
크로스 브라우저 호환성을 보장하며, 네이티브 이벤트와 동일한 인터페이스를 제공한다.

주요 속성과 메서드

function App() {
  function handleEvent(event) {
    // 이벤트 정보
    console.log(event.type);           // 이벤트 타입 (예: "click")
    console.log(event.target);         // 이벤트가 발생한 요소
    console.log(event.currentTarget);  // 이벤트 핸들러가 연결된 요소
    
    // 마우스 이벤트 정보
    console.log(event.clientX);        // 마우스 X 좌표
    console.log(event.clientY);        // 마우스 Y 좌표
    
    // 키보드 이벤트 정보
    console.log(event.key);            // 눌린 키
    console.log(event.code);           // 키보드 코드
    
    // 이벤트 제어
    event.preventDefault();   // 기본 동작 막기
    event.stopPropagation();  // 이벤트 버블링 중단
  }

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

target vs currentTarget 차이

function App() {
  function handleClick(event) {
    console.log('target:', event.target);           // 실제 클릭된 요소
    console.log('currentTarget:', event.currentTarget); // 핸들러가 붙은 요소
  }

  return (
    <div onClick={handleClick}>
      <button>버튼</button>
    </div>
  );
}
  • target: 이벤트가 실제로 발생한 요소 (예: <button>)
  • currentTarget: 이벤트 핸들러가 연결된 요소 (예: <div>)

네이티브 이벤트 접근

합성 이벤트 객체를 통해 원본 브라우저 이벤트에 접근할 수 있다.

function App() {
  function handleClick(event) {
    const nativeEvent = event.nativeEvent;
    console.log('React 합성 이벤트:', event);
    console.log('브라우저 네이티브 이벤트:', nativeEvent);
  }

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

대부분의 경우 합성 이벤트만으로 충분하지만, 특수한 경우 네이티브 이벤트가 필요할 수 있다.

preventDefault() 예시

폼 제출이나 링크 클릭 같은 기본 동작을 막을 때 사용한다.

function Form() {
  function handleSubmit(event) {
    event.preventDefault(); // 페이지 새로고침 방지
    console.log("폼 제출됨");
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" />
      <button type="submit">제출</button>
    </form>
  );
}

stopPropagation() 예시

이벤트가 부모 요소로 전파되는 것을 막을 때 사용한다.

function App() {
  function handleParentClick() {
    alert("부모 클릭됨");
  }

  function handleChildClick(event) {
    event.stopPropagation(); // 부모로 이벤트 전파 중단
    alert("자식 클릭됨");
  }

  return (
    <div onClick={handleParentClick}>
      부모 영역
      <button onClick={handleChildClick}>자식 버튼</button>
    </div>
  );
}

버튼을 클릭하면 "자식 클릭됨"만 표시되고, "부모 클릭됨"은 표시되지 않는다.


이벤트 전파 (Event Propagation)

이벤트 전파란 이벤트가 발생했을 때 해당 이벤트가 부모 또는 자식 요소로 전달되는 현상을 의미한다.

이벤트 전파의 3단계

┌─────────────────────────────────┐
│   1. 캡처링 (Capturing)          │
│   최상위 → 타겟 요소             │
└─────────────────────────────────┘
              ↓
┌─────────────────────────────────┐
│   2. 타겟 (Target)               │
│   타겟 요소 자체                 │
└─────────────────────────────────┘
              ↓
┌─────────────────────────────────┐
│   3. 버블링 (Bubbling)           │
│   타겟 요소 → 최상위             │
└─────────────────────────────────┘

1. 캡처링 (Capturing Phase)

캡처링은 최상위 부모 요소에서 타겟 요소 방향으로 내려가며 이벤트가 전파되는 방식이다.

React에서 캡처링 단계 이벤트를 처리하려면 이벤트 이름 뒤에 Capture를 붙인다.

function App() {
  function handleCaptureClick() {
    console.log("캡처링 단계");
  }

  function handleBubbleClick() {
    console.log("버블링 단계");
  }

  return (
    <div onClickCapture={handleCaptureClick}>
      <button onClick={handleBubbleClick}>클릭</button>
    </div>
  );
}

버튼을 클릭하면 콘솔에 다음 순서로 출력된다:
1. "캡처링 단계" (div의 onClickCapture)
2. "버블링 단계" (button의 onClick)

2. 버블링 (Bubbling Phase)

버블링은 타겟 요소에서 최상위 부모 요소 방향으로 올라가며 이벤트가 전파되는 방식이다.

버블링은 기본 동작이므로 별도의 키워드를 붙일 필요가 없다.

function App() {
  function handleDivClick() {
    console.log("div 클릭됨");
  }

  function handleButtonClick() {
    console.log("button 클릭됨");
  }

  return (
    <div onClick={handleDivClick}>
      <button onClick={handleButtonClick}>클릭</button>
    </div>
  );
}

버튼을 클릭하면:
1. "button 클릭됨" (타겟 요소)
2. "div 클릭됨" (부모 요소로 버블링)

이벤트 전파 막기

버블링 막기: stopPropagation()

버블링을 막으려면 이벤트 핸들러 내부에서 stopPropagation() 메서드를 호출한다.

function App() {
  function handleDivClick() {
    console.log("div 클릭됨");
  }

  function handleButtonClick(e) {
    e.stopPropagation(); // 버블링 중단
    console.log("button 클릭됨");
  }

  return (
    <div onClick={handleDivClick}>
      <button onClick={handleButtonClick}>클릭</button>
    </div>
  );
}

이제 버튼을 클릭해도 "button 클릭됨"만 출력되고, div의 핸들러는 실행되지 않는다.

캡처링과 버블링 모두 막기: stopImmediatePropagation()

같은 요소에 여러 핸들러가 있을 때 나머지 핸들러 실행을 막으려면 네이티브 이벤트의 stopImmediatePropagation()을 사용한다.

function App() {
  function handleClick1(e) {
    e.nativeEvent.stopImmediatePropagation();
    console.log("첫 번째 핸들러");
  }

  function handleClick2() {
    console.log("두 번째 핸들러"); // 실행 안 됨
  }

  return (
    <button 
      onClick={handleClick1}
      onClickCapture={handleClick2}
    >
      클릭
    </button>
  );
}

실무 활용 예시

모달 외부 클릭 시 닫기

function Modal({ onClose }) {
  function handleOverlayClick(e) {
    // 오버레이를 직접 클릭했을 때만 닫기
    if (e.target === e.currentTarget) {
      onClose();
    }
  }

  function handleContentClick(e) {
    // 모달 내용 클릭 시 닫히지 않도록 전파 중단
    e.stopPropagation();
  }

  return (
    <div className="overlay" onClick={handleOverlayClick}>
      <div className="modal-content" onClick={handleContentClick}>
        <h2>모달 제목</h2>
        <p>모달 내용</p>
        <button onClick={onClose}>닫기</button>
      </div>
    </div>
  );
}

이벤트 핸들러 네이밍 컨벤션

일관된 네이밍은 코드 가독성을 높인다.

함수 이름: handle로 시작

function handleClick() { }
function handleSubmit() { }
function handleChange() { }
function handleMouseEnter() { }

Props로 전달할 때: on으로 시작

function Button({ onClick, onSubmit }) {
  return <button onClick={onClick}>제출</button>;
}

function App() {
  function handleButtonClick() {
    console.log("클릭됨");
  }

  return <Button onClick={handleButtonClick} />;
}

패턴 요약

위치패턴예시
컴포넌트 내부 함수handle-handleClick, handleSubmit
Props 이름on-onClick, onSubmit
DOM 이벤트 속성on- (카멜케이스)onClick, onChange

성능 최적화 고려사항

문제: 매번 새로운 함수 생성

function List({ items }) {
  function handleClick(id) {
    console.log(`${id} 클릭됨`);
  }

  return (
    <ul>
      {items.map(item => (
        // ⚠️ 렌더링마다 새 함수 생성
        <li key={item.id} onClick={() => handleClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

대부분의 경우 성능 문제가 되지 않지만, 아이템이 수천 개 이상이거나 자식 컴포넌트가 React.memo로 최적화된 경우 문제가 될 수 있다.

해결: useCallback 훅 사용

import { useCallback } from 'react';

function List({ items }) {
  const handleClick = useCallback((id) => {
    console.log(`${id} 클릭됨`);
  }, []); // 의존성 배열

  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => handleClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

주의: 성능 최적화는 실제 성능 문제가 발생했을 때 적용하는 것이 좋다. 조기 최적화는 오히려 코드를 복잡하게 만들 수 있다.


요약

이벤트 속성

  • HTML과 달리 카멜케이스 사용 (onClick, onChange)
  • 값으로 함수 참조 전달 (문자열 아님)
  • DOM 태그에는 표준 이벤트만 사용
  • 커스텀 컴포넌트에는 자유롭게 정의 가능

이벤트 핸들러

  • 함수 참조 전달: 매개변수 없을 때 onClick={handleClick}
  • 즉시 실행 방지: onClick={handleClick()}는 잘못된 코드
  • 매개변수 전달: 화살표 함수로 감싸기 onClick={() => handleClick(id)}

이벤트 객체

  • 합성 이벤트(SyntheticEvent): 크로스 브라우저 호환성 보장
  • target: 이벤트가 실제로 발생한 요소
  • currentTarget: 이벤트 핸들러가 연결된 요소
  • preventDefault(): 기본 동작 막기
  • stopPropagation(): 이벤트 버블링 중단
  • nativeEvent: 원본 브라우저 이벤트 접근

이벤트 전파

  • 캡처링: 최상위 → 타겟 요소 (onClickCapture)
  • 버블링: 타겟 요소 → 최상위 (onClick, 기본 동작)
  • 전파 중단: stopPropagation() 사용

네이밍 컨벤션

  • 함수는 handle-로 시작 (handleClick)
  • Props는 on-으로 시작 (onClick)

성능

  • 대부분 문제없지만, 필요시 useCallback 고려
  • 조기 최적화보다는 실제 문제 발생 시 최적화

실전 예제 모음

폼 입력 처리

import { useState } from 'react';

function LoginForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: ''
  });

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

  function handleSubmit(e) {
    e.preventDefault();
    console.log('로그인 데이터:', formData);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="이메일"
      />
      <input
        type="password"
        name="password"
        value={formData.password}
        onChange={handleChange}
        placeholder="비밀번호"
      />
      <button type="submit">로그인</button>
    </form>
  );
}

키보드 이벤트 처리

function SearchInput() {
  function handleKeyDown(e) {
    if (e.key === 'Enter') {
      console.log('검색:', e.target.value);
    } else if (e.key === 'Escape') {
      e.target.value = '';
    }
  }

  return (
    <input
      type="text"
      placeholder="Enter로 검색, ESC로 초기화"
      onKeyDown={handleKeyDown}
    />
  );
}

마우스 이벤트 처리

import { useState } from 'react';

function HoverCard() {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <div
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      style={{
        padding: '20px',
        backgroundColor: isHovered ? 'lightblue' : 'white',
        transition: 'background-color 0.3s'
      }}
    >
      {isHovered ? '마우스가 올라와 있어요!' : '마우스를 올려보세요'}
    </div>
  );
}

드래그 앤 드롭

function DragDropDemo() {
  function handleDragStart(e) {
    e.dataTransfer.setData('text/plain', e.target.id);
    console.log('드래그 시작:', e.target.id);
  }

  function handleDragOver(e) {
    e.preventDefault(); // 드롭 허용
  }

  function handleDrop(e) {
    e.preventDefault();
    const id = e.dataTransfer.getData('text/plain');
    console.log('드롭됨:', id);
  }

  return (
    <div>
      <div
        id="drag-item"
        draggable
        onDragStart={handleDragStart}
        style={{ padding: '10px', background: 'lightgreen', cursor: 'move' }}
      >
        이것을 드래그하세요
      </div>
      
      <div
        onDragOver={handleDragOver}
        onDrop={handleDrop}
        style={{ 
          marginTop: '20px', 
          padding: '40px', 
          border: '2px dashed gray',
          textAlign: 'center'
        }}
      >
        여기에 드롭하세요
      </div>
    </div>
  );
}

컨텍스트 메뉴 (우클릭 메뉴)

import { useState } from 'react';

function ContextMenuDemo() {
  const [menuPos, setMenuPos] = useState(null);

  function handleContextMenu(e) {
    e.preventDefault();
    setMenuPos({ x: e.clientX, y: e.clientY });
  }

  function handleClick() {
    setMenuPos(null); // 메뉴 닫기
  }

  return (
    <div onClick={handleClick}>
      <div
        onContextMenu={handleContextMenu}
        style={{
          padding: '40px',
          background: 'lightgray',
          textAlign: 'center'
        }}
      >
        여기서 우클릭하세요
      </div>

      {menuPos && (
        <div
          style={{
            position: 'fixed',
            top: menuPos.y,
            left: menuPos.x,
            background: 'white',
            border: '1px solid black',
            padding: '10px',
            zIndex: 1000
          }}
        >
          <div>메뉴 항목 1</div>
          <div>메뉴 항목 2</div>
          <div>메뉴 항목 3</div>
        </div>
      )}
    </div>
  );
}

스크롤 이벤트 (무한 스크롤)

import { useState, useEffect } from 'react';

function InfiniteScroll() {
  const [items, setItems] = useState(Array.from({ length: 20 }, (_, i) => i));

  function handleScroll(e) {
    const { scrollTop, scrollHeight, clientHeight } = e.target;
    
    // 스크롤이 하단에 가까워지면
    if (scrollHeight - scrollTop <= clientHeight + 100) {
      console.log('더 로드!');
      setItems(prev => [
        ...prev,
        ...Array.from({ length: 20 }, (_, i) => prev.length + i)
      ]);
    }
  }

  return (
    <div
      onScroll={handleScroll}
      style={{
        height: '400px',
        overflow: 'auto',
        border: '1px solid black'
      }}
    >
      {items.map(item => (
        <div key={item} style={{ padding: '20px', borderBottom: '1px solid gray' }}>
          아이템 {item}
        </div>
      ))}
    </div>
  );
}

포커스 관리

import { useRef } from 'react';

function FocusDemo() {
  const inputRef = useRef(null);

  function handleFocus() {
    console.log('포커스 들어옴');
  }

  function handleBlur() {
    console.log('포커스 빠져나감');
  }

  function focusInput() {
    inputRef.current.focus();
  }

  return (
    <div>
      <input
        ref={inputRef}
        onFocus={handleFocus}
        onBlur={handleBlur}
        placeholder="포커스 테스트"
      />
      <button onClick={focusInput}>인풋에 포커스</button>
    </div>
  );
}

주의사항 및 팁

1. 이벤트 핸들러에서 async 함수 사용

// ❌ async를 직접 사용하면 안 됨
<button onClick={async () => await fetchData()}>클릭</button>

// ✅ 내부에서 async 함수 호출
function App() {
  async function fetchData() {
    const data = await fetch('/api/data');
    return data.json();
  }

  function handleClick() {
    fetchData().then(data => console.log(data));
  }

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

2. 이벤트 핸들러에서 상태 업데이트

// ❌ 이전 상태를 직접 참조
const [count, setCount] = useState(0);
<button onClick={() => setCount(count + 1)}>+</button>

// ✅ 함수형 업데이트 사용 (권장)
<button onClick={() => setCount(prev => prev + 1)}>+</button>

3. 여러 이벤트 핸들러 결합

function App() {
  function handleClick(e) {
    console.log('클릭됨');
  }

  function handleSpecialClick(e) {
    if (e.shiftKey) {
      console.log('Shift + 클릭');
    } else if (e.ctrlKey) {
      console.log('Ctrl + 클릭');
    } else {
      handleClick(e);
    }
  }

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

4. 조건부 이벤트 핸들러

function Button({ disabled, onClick }) {
  function handleClick(e) {
    if (disabled) {
      e.preventDefault();
      console.log('버튼이 비활성화됨');
      return;
    }
    onClick(e);
  }

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

5. 디바운싱과 스로틀링

import { useState, useCallback } from 'react';

function SearchInput() {
  const [query, setQuery] = useState('');

  // 디바운싱: 입력이 멈춘 후 실행
  const debounce = (func, delay) => {
    let timeoutId;
    return (...args) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => func(...args), delay);
    };
  };

  const handleSearch = useCallback(
    debounce((value) => {
      console.log('검색:', value);
    }, 500),
    []
  );

  function handleChange(e) {
    const value = e.target.value;
    setQuery(value);
    handleSearch(value);
  }

  return (
    <input
      type="text"
      value={query}
      onChange={handleChange}
      placeholder="검색어 입력"
    />
  );
}

자주 하는 실수

1. 함수를 즉시 호출하기

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

// ✅ 올바른 코드
<button onClick={handleClick}>클릭</button>
<button onClick={() => handleClick()}>클릭</button>

2. 이벤트 객체 사용 후 접근

// ❌ 비동기에서 이벤트 객체 직접 사용
function handleClick(e) {
  setTimeout(() => {
    console.log(e.target); // 에러 또는 null
  }, 1000);
}

// ✅ 필요한 값을 미리 저장
function handleClick(e) {
  const target = e.target;
  setTimeout(() => {
    console.log(target); // 정상 작동
  }, 1000);
}

3. preventDefault 위치

// ❌ 조건문 밖에서 호출
function handleSubmit(e) {
  e.preventDefault();
  if (!isValid) return;
  submitForm();
}

// ✅ 필요한 시점에 호출 (위 코드도 괜찮지만, 아래가 더 명확)
function handleSubmit(e) {
  if (!isValid) return;
  e.preventDefault();
  submitForm();
}

참고자료

profile
개발자 소희의 노트

0개의 댓글