리액트 리렌더링 관련 공부

리액트의 리렌더링을 유발하는 경우

-> 리액트의 리렌더링을 유발하는 경우는 크게 3가지이다.

  1. 컴포넌트의 props가 변경될 때
  2. 컴포넌트의 state가 변경될 때
  3. 부모 컴포넌트의 리렌더링이 발생할 때

컴포넌트의 props, state가 변경될 때에는 당연히 해당 값들을 통해 화면을 다시 그려야 하기 때문에 리렌더링이 발생하는 것을 막을 필요는 없다.

하지만 3번째 케이스는 조금 다르다. 부모 컴포넌트의 리렌더링이 발생하면 자식 컴포넌트의 props가 바뀌지 않았더라도 항상 리렌더링이 발생해야 하는가?

React.memo

이를 막기 위해 존재하는 개념은 바로 memo이다.

예를 들어, A(부모), B(자식) 컴포넌트라고 하자. 그리고, 자식 컴포넌트를 React.memo로 감싸서 부모 컴포넌트에서 호출하는 상황이다.

이렇게 될 경우, A 컴포넌트가 리렌더링 되더라도, B 컴포넌트는 props가 바뀌지 않으면 리렌더링되지 않는다.

props가 바뀌는 것을 파악하는 기준

리액트 렌더링에 있어서 props가 바뀌는 기준은 Object.is 메서드와 같은 방식의 얕은 비교를 통해 파악한다.

해당 비교 방식은 props 값이 원시 값인가, 참조 값인가에 따라 다르다.

  • props가 원시 값인 경우 ( number, string, null, undefined 등등.. )
    • 해당 값이 같은지를 비교하고, 같으면. (call by value와 비슷한 느낌 )
  • props가 참조 값인 경우 ( object, array, function )
    • 해당 값이 같은지를 비교함. 해당 값의 주소 공간도 같은 지를 비교함 ( call by reference와 유사한 느낌 )

React.memo로 감싸고, props로 원시 값

지금까지 정리한 것을 봤을 때, React.memo로 자식 컴포넌트를 감싸면, 부모 컴포넌트의 리렌더링이 유발되더라도 props가 같을 경우 자식 컴포넌트의 리렌더링이 발생하지 않는다고 한다.

하지만 이는 props가 원시값일 경우일 때 한정이다. <예시 코드 - props가 원시값>

import React, { useState } from "react";  
  
const Child = React.memo(function Child({ count }) {  
  console.log("Child render");  
  return <div>count: {count}</div>;  
});  
  
export default function Parent() {  
  const [count] = useState(1);  
  const [text, setText] = useState("");  
  
  console.log("Parent render");  
  
  return (  
    <div>  
      <Child count={count} />  
  
      <input  
        value={text}  
        onChange={(e) => setText(e.target.value)}  
        placeholder="type something"  
      />  
    </div>  
  );  
}

이를 코더패드로 실행한 결과는 다음과 같다.

  • 첫 렌더링 시에 찍히는 콘솔은 다음과 같다.

    • parent render
    • child render
  • 이후, input창에 텍스트를 계속 입력하더라도 다음 값만 콘솔에 찍힌다.

    • parent render
  • 즉, Child에 props로 넘겨주는 count 값은 변하지 않기 때문에 리렌더링이 발생하지 않은 것이다.

React.memo로 감싸고, props로 참조 값

<예시 코드 - props가 참조값>

const Child = function Child({ func }: { func: () => {} }) {  
  console.log("Child render");  
  return <div></div>;  
};  
  
function Parent() {  
  const [count] = useState(1);  
  const [text, setText] = useState("");  
  function anyFunction(){return true}
  console.log("Parent render");  
  
  return (  
    <div>  
      <Child func={anyFunction} />  
  
      <input  
        value={text}  
        onChange={(e) => setText(e.target.value)}  
        placeholder="type something"  
      />  
    </div>  
  );  
}

이를 코더패드로 실행하고 input에 타이핑한 결과는 다음과 같다.

왜 props가 참조값일 경우에는 React.memo로 감싸더라도 부모 리렌더링 -> 자식 리렌더링이 유발될까?

이는 리액트가 함수형 컴포넌트를 실행하는 방식으로 인해 생기는 이슈이다. 컴포넌트가 리렌더링되면서, 컴포넌트 내부적으로 선언된 객체값들은 다시 초기화가 실행되면서, 다른 주소를 부여받게 된다.

따라서, props를 비교했을 때, 내용은 같지만 참조 값이 다르기 때문에 props가 변했다고 인식하게 되고, 리렌더링이 발생하게 된다.

자식 컴포넌트의 props로 참조 값을 전달해야 하면서, props가 변하지 않고 & 부모 컴포넌트가 리렌더링 되었을 때의 자식 컴포넌트의 리렌더링을 막으려면?

useMemo, useCallback을 활용할 수 있다.

  • useMemo
    • 계산 결과 값의 참조를 유지한다.
  • useCallback
    • 함수를 메모이제이션한다.

useMemo, useCallback이 해당 목적만을 위해 사용되는 것은 아니다. useMemo로 참조를 유지하는 값은 원시값일 수도 있다. 왜냐하면 값의 계산이 오래 걸리고 복잡한 경우에는 , 리렌더링될 때마다 시행될 경우 성능 문제가 발생할 수 있기 때문이다.

useMemo, useCallback은 계속 참조를 유지하는가? useMemo, useCallback은 둘 다 의존성 배열을 갖는데, 의존성 배열의 값이 변경된 경우에는 다시 선언되어 참조 값이 바뀌게 된다.

자식 컴포넌트에 props로 참조값을 넘기면서, props 변화가 없으면 부모 컴포넌트의 리렌더링이 자식 컴포넌트의 리렌더링을 유발하지 않았으면 하는 경우

바로 이런 케이스가 useMemo, useCallback 등을 사용하면서 ( 상황에 맞게 ), memo로 컴포넌트를 감싸줘야 하는 경우이다.

아까, 함수를 props로 넘기고, useCallback으로 감싸지 않았던 코드에서는 지속적으로 리렌더링이 발생했다. 이제 해당 코드를 개선해보면 다음과 같다.

< useCallback 활용해서 함수 참조값을 유지한 상태로 props로 넘겨주는 코드 >

const Child = React.memo(function Child({ func }: { func: () => {} }) {  
  console.log("Child render");  
  return <div></div>;  
});  
  
function Parent() {  
  const [count] = useState(1);  
  const [text, setText] = useState("");
  const anyFunction = useCallback(() => { return true }, [])
  console.log("Parent render");  
  
  return (  
    <div>  
      <Child func={anyFunction} />  
  
      <input  
        value={text}  
        onChange={(e) => setText(e.target.value)}  
        placeholder="type something"  
      />  
    </div>  
  );  
}

<coderpad 실행 결과>