header

React의 컴포넌트 최적화 방법과 커스텀 훅에 대하여 알아보자

💡 새롭게 알게된 점

React에서 컴포넌트를 최적화 할 수 있는 방법은 대표적으로 useMemo, React.memo, useCallback이 존재한다.

컴포넌트의 리렌더링 조건은 다음과 같다.

  1. 함수 컴포넌트는 자신의 상태가 변경될 때 리렌더링된다.
  2. 부모 컴포넌트로 부터 받는 prop이 변경될 때 리렌더링된다.
  3. 부모 컴포넌트의 상태가 변경되면 리렌더링된다.

만약 연산의 속도가 느린 컴포넌트라면….??

위 조건에 따라 리렌더링 될 때, 동일한 컴포넌트에 대해 불필요한 시간이 많이 소요되어 사용자들에게 불편함을 가져올 것이다.

useMemo

이를 방지하기 위해 useMemo를 사용해 특정 조건이 변경될 때만 리렌더링되도록 할 수 있다.

useMemo는 특정 결과값을 재사용할 때 사용한다. 예시를 살펴보자.

// ShowSum

import { useMemo } from 'react';

function sum(n) {
  console.log('start');
  let res = 0;

  for (let i = 0; i <= n; i += 1) {
    res += i;
  }

  console.log('finished');
  return res;
}

const ShowSum = ({ label, n }) => {
  // const result = sum(n);
  const result = useMemo(() => sum(n), [n]);
  return (
    <span>
      {label}: {result}
    </span>
  );
};

export default ShowSum;

React.memo

memo는 컴포넌트의 props가 변경되지 않았을 경우, 리렌더링을 방지하여 컴포넌트의 리렌더링 성능을 최적화할 수 있는 함수이다.

자식 컴포넌트가 변경되지 않았을 경우에는 리렌더링되지 않도록 한다. 즉, 리렌더링이 필요한 컴포넌트에 대해서만 리렌더링을 할 수 있도록 설정할 수 있다. 예시를 살펴보자.

// Box

import styled from '@emotion/styled';
import { memo } from 'react';

const BoxWrapper = styled.div`
  width: 100px;
  height: 100px;
  background-color: cyan;
`;

const Box = memo(() => {
  console.log('Box rendered');
  return <BoxWrapper />;
});

export default Box;

현재는 단순한 예시로 props가 존재하지 않는 컴포넌트에 대해 React.memo를 사용해 처음 렌더링 이후 리렌더링이 이뤄지지 않지만, props가 존재할 경우에도 이전과 동일한 props가 전달된 경우에는 리렌더링을 방지하여 최적화할 수 있다.

useCallback

useMemo와 달리, useCallback은 특정 함수를 새로 만들지 않고 재사용하고 싶을 경우 사용한다. 예시를 살펴보자.

// Checkbox

import { memo } from 'react';

const Checkbox = memo(({ label, checked, onChange }) => {
  console.log(label, checked);
  return (
    <label>
      {label}
      <input type="checkbox" defaultChecked={checked} onChange={onChange} />
    </label>
  );
});

export default Checkbox;
// App

import { useCallback, useState } from 'react';
import Checkbox from './components/Checkbox';

function App() {
  const [foodChecked, setFoodChecked] = useState(false);
  const [clothesChecked, setClothesChecked] = useState(false);
  const [shelterChecked, setShelterChecked] = useState(false);

  const foodChange = (e) => setFoodChecked(e.target.checked);
  const clothesChange = (e) => setClothesChecked(e.target.checked);
  const shelterChange = (e) => setShelterChecked(e.target.checked);

  return (
    <div>
      <Checkbox label="Food" checked={foodOn} onChange={foodChange} />
      <Checkbox label="Clothes" checked={clothesOn} onChange={clothesChange} />
      <Checkbox label="Shelter" checked={shelterOn} onChange={shelterChange} />
    </div>
  );
}

export default App;

위와 같은 코드를 정의했을 때, 체크 박스를 클릭 시 클릭한 체크 박스 외에도 나머지 두 체크박스까지 리렌더링되는 모습을 볼 수 있다. 이는 foodChange, clothesChange, shelterChange 함수들이 재정의 되면서 새로운 메모리에 할당된 함수가 onChange로 넘어가게 되어 리렌더링 되는 것이다. 이를 해결하기 위해 useCallback을 사용할 수 있다.

// App

const foodChange = useCallback((e) => setFoodOn(e.target.checked), []);
const clothesChange = useCallback((e) => setClothesOn(e.target.checked), []);
const shelterChange = useCallback((e) => setShelterOn(e.target.checked), []);

useCallback으로 감싸주고, 현재 할당할 디펜던시가 따로 없기 때문에 빈 배열로 할당하면 다시 렌더링 되는 현상을 막을 수 있다.

Custom Hook

앞서 정의한 Checkbox의 state, setState와 같은 부분들이 여러 곳에서 사용된다면 매번 이런 로직들을 반복해서 작성하는 것은 재사용성이 매우 떨어지는 일이다. 따라서 Custom Hook을 사용함으로 중복되는 로직을 최소화 할 수 있다.

// useToggle

import { useCallback, useState } from 'react';

const useToggle = (initialState = false) => {
  const [state, setState] = useState(initialState);
  const toggle = useCallback(() => setState((state) => !state), []);

  return [state, toggle];
};

export default useToggle;
// App

import Checkbox from './components/Checkbox';
import useToggle from './hooks/useToggle';

const App = () => {
  const [foodChecked, foodToggle] = useToggle();
  const [clothesChecked, clothesToggle] = useToggle();
  const [shelterChecked, shelterToggle] = useToggle();

  return (
    <div>
      <Checkbox label='Food' checked={foodChecked} onChange={foodToggle} />
			<Checkbox label='Clothes' checked={clothesChecked} onChange={clothesToggle} />
			<Checkbox label='Shelter' checked={shelterChecked} onChange={shelterToggle} />
    </div>
  );
};

export default App;

이를 통해 코드를 절반 가량 줄일 수 있고, 더 가독성 있는 코드를 작성할 수 있다.

이 외에도 여러가지 Custom Hook을 작성할 수 있는데, 특정 element를 hover할 때 동작하는 Custom Hook을 만들어보자.

// useHover

import { useCallback, useEffect, useRef, useState } from 'react';

const useHover = () => {
  const [state, setState] = useState(false);
  const ref = useRef(null);

  const handleMouseOver = useCallback(() => setState(true), []);
  const handleMouseOut = useCallback(() => setState(false), []);

  useEffect(() => {
    const ele = ref.current;
    if (ele) {
      ele.addEventListener('mouseover', handleMouseOver);
      ele.addEventListener('mouseout', handleMouseOut);

      return () => {
        ele.removeEventListener('mouseover');
        ele.removeEventListener('mouseout');
      };
    }
  }, [ref, handleMouseOver, handleMouseOut]);

  return [ref, state];
};

export default useHover;
// App

import Box from './components/Box';
import useHover from './hooks/useHover';

const App = () => {
  const [ref, isHover] = useHover();

  return (
    <div>
      {isHover ? 'mouseover' : 'mouseout'}
      <Box ref={ref} />
    </div>
  );
};

export default App;

useHover라는 Custom Hook을 정의해서 사용한다면 hover 이벤트가 필요한 컴포넌트마다 이벤트를 매번 정의할 필요가 없어진다. 단순히 hover 이벤트를 적용하고자 하는 element에 ref를 걸어주고, hover의 여부에 따라 로직을 작성하면 된다.

위와 같은 맥락으로 Custom Hook을 사용한다면, 중복 로직의 사용을 최소화하여 코드의 양이 적어질 뿐만 아니라 다른 컴포넌트에서도 동일한 커스텀 훅을 사용할 수 있기에 동일한 로직을 수행하는 컴포넌트가 많을 경우 더욱 빛을 발할 수 있게 된다.


❗️ 느낀점

React의 컴포넌트 최적화 방법 및 커스텀 훅에 대해서 정리해보았다.

여태까지 컴포넌트 최적화 방법은 알고 있었지만 잘 사용하진 않았고, 커스텀 훅도 마찬가지였다.

오늘 내용을 정리하면서 보니 여태까지 왜 적용하지 않았을까 하는 후회가 남았고, 앞으로는 생각만 하지 않고 코드의 유지보수성을 위해 더 나아가 서비스 최적화를 위해 오늘 배운 것들을 잘 활용해야겠다.