[React] 제어 컴포넌트 리렌더링 성능 이슈 해결책

Bewell·2024년 3월 9일
0
post-thumbnail

https://velog.io/@taewo/React-제어-비제어-컴포넌트
글에 이어서...

제어 컴포넌트에서 발생하는 리렌더링 성능 이슈를 어떻게 해결할 수 있을까?


1. Debounce

사용자 입력마다 트리거 되는 이벤트레 맞춰 핸들러를 실행하는 것과 달리 일정 시간(time window)안에 발생하는 이벤트를 모아서 한번만 실행하는 debounce를 적용하는 방법

경험적으로 100ms~150ms 정도가 사용자 경험의 저하를 최소화 하면서, 성능을 개선할 수 있는 효과적인 시간이다.

debounce를 아래와 같이 직접 작성할 수 있다

import { useEffect, useRef } from "react"

/**
 * @callback callbackFunc
 * @param {any[]} args - arguments passed into callback
 */
/**
 * Debounce function to reduce number executions
 * @param {callbackFunc} cb - callback function to be executed
 * @param {number} wait - number of milliseconds to delay function execution
 * @param {any[]} deps - dependencies array
 */
const useDebounce = (cb, wait = 500, deps = []) => {
  const timerRef = useRef(null)

  useEffect(() => {
    clearTimeout(timerRef.current)

    timerRef.current = setTimeout(() => {
      cb.apply(this, args)
    }, wait)

    return () => clearTimeout(timerRef.current)
    /** used JSON.stringify(deps) instead of just deps
      * because passing an array as a dependency causes useEffect 
re-render infinitely
      * @see {@link https://github.com/facebook/react/issues/14324}
      */
    /* eslint-disable react-hooks/exhaustive-deps */
  }, [cb, wait, JSON.stringify(deps)])
}
function Controlled() {
  const [value, setValue] = useState()
  const [user, setUser] = useState()

  // Debounce our search
  useDebounce(async () => {
    try {
      const [userDetails] = await fetch(`${API}?name=${value}`)
                                                             .then(res => res.json())

      setUserDetails(prevDetails => ({ ...prevDetails, ...userDetails }))
    } catch (error) {
      console.log(error)
    }
  }, 500, [value])

  const handleChange = event => {
    setValue(event.target.value)
  }

  return (
    <form id="search">
      <label id="search-label" htmlFor="search-input">
        Search for user details
      </label>
      <input
        name="search"
        type="search"
        id="search-input"
        value={value}
        onChange={handleChange}
      />
    </form>
  )
}




lodash의 debounce를 사용하는 방법도 알아보자

import debounce from 'lodash-es/debounce';

function App({ initialValue }) {
  const [value, setValue] = useState(initialValue);
  
  const handleChange = debounce((e) => {
    setValue(e.target?.value)
  }, 150);
  
  return <input type="text" value={value} onChange={handleChange} />;
}




memoization 적용

memoization은 이전 값과 현재 값의 차이를 비교하기 때문에 계산 비용이 추가되는 trade-off가 있기에, 성능을 다소 향상시켜주는 것은 사실이지만 그 효과의 한계는 분명하다

function Button({ disabled = false, onClick, children }) {
  return (
    <button type="button" disabled={disabled} onClick={onClick}>
      {children}
    </button>
  );
}

const MemoButton = React.memo(Button);

// -----------

function App({ initialValue }) {
  const [value, setValue] = useState(initialValue);
  const isDisabled = useMemo(() => {
    return !value;
  }, [value]);
  
  const handleClick = useCallback(() => {
    console.log('버튼을 클릭합니다');
  }, []);
  
  const handleChange = (e) => {
    setValue(e.target?.value)
  };
  
  return (
    <>
      <input type="text" value={value} onChange={handleChange} />
      <MemoButton disabled={isDisabled} onClick={handleClick}>
        버튼
      </MemoButton>
    </>
  );
}





참고

  1. https://dev.to/bugs_bunny/debouncing-react-controlled-components-588i
  2. https://blog.leaphop.co.kr/blogs/33

0개의 댓글