icon안동민 개발노트

불필요한 리렌더링 이해와 방지


 React 애플리케이션의 성능을 최적화하는 데 있어 불필요한 리렌더링을 이해하고 방지하는 것은 매우 중요합니다.

 이 절에서는 리렌더링의 원인과 이를 최적화하는 방법에 대해 알아보겠습니다.

컴포넌트 리렌더링의 조건

 React 컴포넌트가 리렌더링되는 주요 조건은 다음과 같습니다.

  1. 컴포넌트의 상태(state)가 변경될 때
  2. 부모 컴포넌트가 리렌더링될 때
  3. 컴포넌트에 전달되는 props가 변경될 때

 예를 들어,

function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

 이 예제에서 setCount가 호출될 때마다 Counter 컴포넌트가 리렌더링됩니다.

부모 컴포넌트 리렌더링의 영향

 부모 컴포넌트가 리렌더링되면, 기본적으로 모든 자식 컴포넌트도 리렌더링됩니다.

function Parent() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <Child />
    </div>
  );
}
 
function Child() {
  console.log('Child rendered');
  return <div>Child</div>;
}

 이 경우, Parentcount 상태가 변경될 때마다 Child 컴포넌트도 리렌더링됩니다.

Props 변경에 따른 리렌더링

 Props가 변경되면 해당 컴포넌트는 리렌더링됩니다.

function Parent() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <Child count={count} />
    </div>
  );
}
 
function Child({ count }) {
  return <div>Count in Child: {count}</div>;
}

 count prop이 변경될 때마다 Child 컴포넌트가 리렌더링됩니다.

React.memo를 사용한 최적화

 React.memo는 고차 컴포넌트(HOC)로, props가 변경되지 않으면 컴포넌트의 리렌더링을 방지합니다.

const MemoizedChild = React.memo(function Child({ name }) {
  console.log('Child rendered');
  return <div>{name}</div>;
});
 
function Parent() {
  const [count, setCount] = useState(0);
  const [name] = useState('John');
 
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <MemoizedChild name={name} />
    </div>
  );
}

 이 경우, count가 변경되어도 MemoizedChild는 리렌더링되지 않습니다.

useCallback 사용법

 useCallback은 함수를 메모이제이션하여, 불필요한 리렌더링을 방지합니다.

function Parent() {
  const [count, setCount] = useState(0);
 
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []); // 의존성 배열이 비어있으므로 함수는 한 번만 생성됩니다.
 
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <MemoizedChild onButtonClick={handleClick} />
    </div>
  );
}
 
const MemoizedChild = React.memo(function Child({ onButtonClick }) {
  console.log('Child rendered');
  return <button onClick={onButtonClick}>Click me</button>;
});

 handleClick 함수가 메모이제이션되어 MemoizedChild의 불필요한 리렌더링을 방지합니다.

useMemo 사용법

 useMemo는 계산 비용이 큰 값을 메모이제이션합니다.

function ExpensiveComponent({ data }) {
  const expensiveResult = useMemo(() => {
    return data.reduce((acc, item) => acc + item, 0);
  }, [data]); // data가 변경될 때만 재계산합니다.
 
  return <div>Result: {expensiveResult}</div>;
}

 data가 변경되지 않으면 expensiveResult는 재계산되지 않습니다.

리렌더링 최적화 전략

  1. 상태를 적절한 레벨에 위치시키기
  2. 불변성 유지하기
  3. 리스트 렌더링 시 적절한 key 사용하기
  4. 대규모 리스트는 가상화 기법 사용하기 (예 : react-window)
  5. 이벤트 핸들러를 인라인으로 정의하지 않기
  6. Context API 사용 시 Provider 최적화하기

불필요한 최적화의 위험성

 과도한 최적화는 코드의 복잡성을 증가시키고 가독성을 해칠 수 있습니다.

 따라서 다음 경우에만 최적화를 고려하세요.

  1. 성능 문제가 실제로 발생했을 때
  2. 컴포넌트가 자주 리렌더링될 때
  3. 컴포넌트의 리렌더링 비용이 클 때

적절한 사용 시기에 대한 가이드라인

  1. 프로파일링을 통해 실제 성능 병목지점 식별하기
  2. 사용자 경험에 직접적인 영향을 미치는 부분부터 최적화하기
  3. 데이터 흐름과 상태 관리 구조를 먼저 개선하기
  4. 최적화 전후의 성능을 측정하고 비교하기

 불필요한 리렌더링을 방지하는 것은 React 애플리케이션의 성능을 크게 향상시킬 수 있습니다. 그러나 모든 컴포넌트를 무조건적으로 최적화하는 것은 바람직하지 않습니다. 성능 최적화는 항상 측정 가능한 문제에 대한 해결책으로 접근해야 합니다.

 React의 기본적인 렌더링 메커니즘은 이미 꽤 효율적이므로, 대부분의 경우 추가적인 최적화 없이도 충분한 성능을 발휘합니다. 따라서 실제 성능 문제가 발생했을 때, 그리고 그 원인이 불필요한 리렌더링임이 확인되었을 때 최적화 기법을 적용하는 것이 좋습니다.

 마지막으로, 최적화 기법을 적용할 때는 항상 코드의 가독성과 유지보수성을 고려해야 합니다. 때로는 약간의 성능 저하를 감수하고 코드를 더 명확하고 이해하기 쉽게 유지하는 것이 장기적으로 더 유리할 수 있습니다. 팀의 다른 개발자들과 최적화 전략에 대해 논의하고 합의하는 것도 중요한 과정입니다.