frontend

React re-rendering 방지 디자인 패턴(3)

하리하링웹 2023. 1. 16. 10:43

React에서 re-rendering이 일어나는 조건은 크게 보면 3가지정도가 있다.

  1. 함수 내의 State가 변경되었을 때
  2. 부모 컴포넌트가 re-render되었을 때
  3. 컴포넌트의 Props값이 변경되었을 때

자세하게 설명하면 수많은 조건이 있겠지만 대부분의 re-rendering는 저 3가지 조건의 상호작용으로 일어나게 된다.

이번 글에서는 3.컴포넌트의 Props값이 변경되었을 때 발생할 수 있는 re-rendering을 방지하는 방법에 대해 작성할 예정이다.

 

 

import { Button } from '@/components/ui';
import { memo, useCallback, useState } from 'react';

function ButtonComponent({ onClick }: { onClick: () => void }) {
  console.log('re-render');
  return <Button onClick={onClick}>Increase Count</Button>;
}

const MemoizedButtonComponent = memo(ButtonComponent);

export default function TestPage() {
  const [count, setCount] = useState(0);

  const handleOnClick = () => {
    setCount((prev) => prev + 1);
    console.log('click');
  };

  return (
    <div>
      <p>num: {count}</p>
      <MemoizedButtonComponent onClick={handleOnClick} />
    </div>
  );
}

 위 코드는 Button Component를 Memo를 사용하여 감싸서 Parent 컴포넌트가 re-render되더라도 Button Component는 re-render가 되지 않도록 만든 코드이다.

 

re-render

하지만 이는 매 클릭마다 버튼 컴포넌트를 re-render 하여 원하는대로 동작하지 않게된다. 

 

re-render가 발생하는 이유는 ButtonComponent가 Props로 받는 onClick으로 넘겨준 handleOnClick 함수 때문이다.

 

 자바스크립트에서 함수는 새로 생성될때마다 새로운 메모리에 할당이 될텐데 이 때 일반적인 경우라면 매 생성마다 함수간 서로 다른 메모리 주소 값을 가지게 된다.

 또한 함수들을 비교할 때 자바스크립트는 함수의 메모리 주소에 의한 비교를 진행하기때문에 동일한 동작을 하는 함수여도 따로 선언을 하게되면 메모리 값이 달라 이를 다르다고 판단하여 아래의 코드처럼 false를 출력하게 되는것이다.

> const a = () => 10
undefined
> const b = () => 10
undefined
> a===b
false
>

 

 

이를통해 아래의 코드에서 매 클릭마다 re-render가 일어나는 이유를 설명할 수 있게된다.

 

import { Button } from '@/components/ui';
import { memo, useCallback, useState } from 'react';

function ButtonComponent({ onClick }: { onClick: () => void }) {
  console.log('re-render');
  return <Button onClick={onClick}>Increase Count</Button>;
}

const MemoizedButtonComponent = memo(ButtonComponent);

export default function TestPage() {
  const [count, setCount] = useState(0);

  const handleOnClick = () => {
    setCount((prev) => prev + 1);
    console.log('click');
  };

  return (
    <div>
      <p>num: {count}</p>
      <MemoizedButtonComponent onClick={handleOnClick} />
    </div>
  );
}

일단 버튼을 클릭하면 handleOnClick 함수에 의해 count state의 값이 1증가하게되고 이로인해 TestPage함수를 다시 실행하게 된다.

 

이 과정에서  handleOnClick을 새로 만드는데 이는 이전에 있던 handleOnClick과 다른 함수라고 판단하여 Button Component의 Props가 변경되었다고 판단하게되고 memoized 되어있음에도 불구하고 ButtonComponent의 re-render가 일어나게 되는것이다.

 

그렇다면 이를 어떻게 방지할까? 아마 많은 사람들이 알고있겠지만 정답은 간단하다.

 

  const handleOnClick = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

위와같이 useCallback을 사용하여 handleOnClick을 감싸주면된다.

 

re-render2

그러면 위 영상처럼 Button Component에서 re-render가 발생하지 않게된다. 

 

useCallback hook은어떠한 함수를 memoization하는 react의 내부 hook이다.

 

첫 번째 인자로 함수를 받고 두 번째 인자로 배열을 받는다.

 

이를통해서 react의 함수가 re-render 될 때 useCallback으로 감싸져 있는 함수에 대해서는 두 번째 인자 흔히들 말하는

dependency array에 속해있는 값의 이전 값과 현재 값을 비교하여 값이 같은 경우에는 첫 번째 함수를 다시 생성하는 과정을 건너뛰게된다.

 

  const handleOnClick = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

위의 함수는 dependency array에 아무런 값도 들어가있지 않으므로 TestPage가 실행될 때 단 한 번만 함수를 생성하며 이후로는 handleOnClick 함수의 값이 변하지 않게된다.

 

따라서 handleOnClick의 값이 변하지 않으므로 Button Component의 Props가 변하지 않았다고 판단하여 re-render를 발생시키지 않는것이다.

 

물론 그렇다고 항상 useCallback을 사용하면 안되고 이로인해 re-render가 발생하는 부분이 어떤부분일까, dependency array가 매 번 바뀌어 항상 함수를 재 생성하지 않을까라는 생각을 한 번쯤은 해보면서 사용하는것을 추천한다.

 

dependency array의 비교 또한 성능에 충분히 영향을 미칠 수 있는 부분이기에 위와같은 부분을 고려하지 않는다면 성능상 손해를 볼 가능성이 있게되니 조심하여 사용하길 바란다.