본문 바로가기

카테고리 없음

[React] React 이벤트 처리의 성능 최적화 팁: 상세 가이드

반응형

 

React 애플리케이션에서 이벤트 처리는 사용자 상호작용의 핵심입니다. 하지만 이벤트 처리를 비효율적으로 구현하면 애플리케이션의 성능이 저하될 수 있습니다. 이 가이드에서는 React에서 이벤트 처리와 관련된 성능을 최적화하는 여러 가지 기법을 자세히 살펴보겠습니다.


1. 이벤트 위임 (Event Delegation) 활용

개념 설명

이벤트 위임은 여러 요소에 대해 각각 이벤트 리스너를 추가하는 대신, 공통 조상 요소에 하나의 이벤트 리스너를 추가하는 기법입니다. 이 방식은 메모리 사용량을 줄이고 동적으로 추가되는 요소들에 대해서도 이벤트 처리가 가능하게 합니다.

자세한 예제와 설명

// 비효율적인 방법
function IneffientList({ items }) {
  const handleClick = (id) => {
    console.log(`Item ${id} clicked`);
  };

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

// 최적화된 방법
function OptimizedList({ items }) {
  const handleClick = (e) => {
    if (e.target.tagName === 'LI') {
      const id = e.target.dataset.id;
      console.log(`Item ${id} clicked`);
    }
  };

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

위의 최적화된 방법에서는 <ul> 요소에 단 하나의 이벤트 리스너만 추가합니다. 클릭 이벤트가 발생하면, 이벤트 리스너는 클릭된 요소가 <li>인지 확인하고, 맞다면 해당 요소의 data-id 속성을 통해 아이템 ID를 얻습니다.

이 방식의 장점:

  1. 메모리 사용량 감소: 각 <li> 요소마다 개별 함수를 생성하지 않습니다.
  2. 동적 요소 처리: 나중에 리스트에 새 항목이 추가되어도 별도의 이벤트 리스너 추가가 필요 없습니다.
  3. 성능 향상: 특히 리스트 항목이 많을 때 성능 차이가 두드러집니다.

2. 디바운싱 (Debouncing)과 쓰로틀링 (Throttling) 사용

개념 설명

  • 디바운싱: 연이어 발생하는 이벤트 중 마지막 (또는 첫) 이벤트만 처리하는 기법입니다.
  • 쓰로틀링: 일정 시간 간격으로 이벤트 처리를 제한하는 기법입니다.

이 두 기법은 특히 검색 입력, 윈도우 리사이징, 스크롤 이벤트 등 빈번하게 발생하는 이벤트를 다룰 때 유용합니다.

자세한 예제와 설명

import React, { useState, useCallback } from 'react';
import { debounce, throttle } from 'lodash';

function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);

  // 디바운스된 검색 함수
  const debouncedSearch = useCallback(
    debounce((term) => {
      console.log(`Searching for: ${term}`);
      // 여기에 실제 API 호출 로직이 들어갑니다
      setResults([`Result for ${term}`]);
    }, 300),
    []
  );

  const handleInputChange = (e) => {
    const term = e.target.value;
    setSearchTerm(term);
    debouncedSearch(term);
  };

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={handleInputChange}
        placeholder="Search..."
      />
      <ul>
        {results.map((result, index) => (
          <li key={index}>{result}</li>
        ))}
      </ul>
    </div>
  );
}

function ScrollComponent() {
  const [scrollPosition, setScrollPosition] = useState(0);

  // 쓰로틀된 스크롤 핸들러
  const throttledScrollHandler = useCallback(
    throttle(() => {
      const position = window.pageYOffset;
      setScrollPosition(position);
      console.log(`Scroll position: ${position}`);
    }, 200),
    []
  );

  React.useEffect(() => {
    window.addEventListener('scroll', throttledScrollHandler);
    return () => window.removeEventListener('scroll', throttledScrollHandler);
  }, [throttledScrollHandler]);

  return <div>Scroll Position: {scrollPosition}</div>;
}

이 예제에서:

  • SearchComponent는 디바운싱을 사용합니다. 사용자가 입력을 멈춘 후 300ms가 지나면 검색이 실행됩니다. 이는 사용자가 빠르게 타이핑할 때 불필요한 API 호출을 방지합니다.
  • ScrollComponent는 쓰로틀링을 사용합니다. 스크롤 이벤트는 최대 200ms마다 한 번씩만 처리됩니다. 이는 스크롤 중 과도한 상태 업데이트를 방지합니다.

이러한 기법들의 장점:

  1. 성능 향상: 불필요한 연산과 렌더링을 줄입니다.
  2. 네트워크 요청 감소: API 호출 횟수를 줄일 수 있습니다.
  3. 부드러운 사용자 경험: 특히 스크롤이나 리사이징과 같은 연속적인 이벤트에서 효과적입니다.

3. useCallback을 사용한 이벤트 핸들러 메모이제이션

개념 설명

useCallback은 의존성 배열이 변경되지 않는 한, 동일한 함수 인스턴스를 반환하는 React 훅입니다. 이를 통해 불필요한 리렌더링을 방지할 수 있습니다.

자세한 예제와 설명

import React, { useState, useCallback } from 'react';

// 자식 컴포넌트
const Button = React.memo(({ onClick, children }) => {
  console.log(`Button rendered: ${children}`);
  return <button onClick={onClick}>{children}</button>;
});

// 부모 컴포넌트
function Parent() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // count1에 의존하는 콜백
  const incrementCount1 = useCallback(() => {
    setCount1(c => c + 1);
  }, []);  // 빈 의존성 배열

  // 매번 새로운 함수가 생성됨
  const incrementCount2 = () => {
    setCount2(c => c + 1);
  };

  return (
    <div>
      <Button onClick={incrementCount1}>
        Increment Count1: {count1}
      </Button>
      <Button onClick={incrementCount2}>
        Increment Count2: {count2}
      </Button>
    </div>
  );
}

이 예제에서:

  • incrementCount1useCallback으로 메모이제이션되어 있어, 컴포넌트가 리렌더링되어도 항상 같은 함수 인스턴스를 유지합니다.
  • incrementCount2는 매 렌더링마다 새로운 함수 인스턴스가 생성됩니다.
  • Button 컴포넌트는 React.memo로 감싸져 있어, props가 변경되지 않으면 리렌더링되지 않습니다.

결과적으로:

  • count1을 증가시키는 버튼은 클릭해도 리렌더링되지 않습니다 (콘솔에 로그가 출력되지 않음).
  • count2를 증가시키는 버튼은 클릭할 때마다 리렌더링됩니다 (매번 콘솔에 로그가 출력됨).

이 기법의 장점:

  1. 불필요한 리렌더링 방지: 특히 자식 컴포넌트에 전달되는 콜백 함수에 유용합니다.
  2. 성능 최적화: 복잡한 컴포넌트 트리에서 더욱 효과적입니다.

주의사항:

  • 모든 함수에 useCallback을 사용하는 것은 오히려 성능을 저하시킬 수 있습니다. 필요한 경우에만 사용하세요.
  • 의존성 배열을 올바르게 설정하는 것이 중요합니다. 잘못 설정하면 버그의 원인이 될 수 있습니다.

이러한 최적화 기법들을 적절히 사용하면 React 애플리케이션의 성능을 크게 향상시킬 수 있습니다.

하지만 항상 실제 성능 문제가 있는 부분에 대해서만 최적화를 적용해야 합니다.

불필요한 최적화는 코드를 복잡하게 만들 수 있으므로, 성능 프로파일링을 통해 실제 병목 지점을 파악하고 그에 맞는 최적화 기법을 적용하는 것이 중요합니다.

반응형